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
|
@@ -16,6 +16,10 @@ class TemplateInstallRequest(BaseModel):
|
|
|
16
16
|
data: Dict[str, Any] = {}
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class TemplateCloneRequest(BaseModel):
|
|
20
|
+
name: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
def create_marketplace_router(
|
|
20
24
|
*,
|
|
21
25
|
store,
|
|
@@ -72,6 +76,15 @@ def create_marketplace_router(
|
|
|
72
76
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
73
77
|
return {"installed": installed}
|
|
74
78
|
|
|
79
|
+
@router.post("/marketplace/templates/{kind}/{template_id}/clone")
|
|
80
|
+
async def clone_template(kind: str, template_id: str, req: TemplateCloneRequest, request: Request):
|
|
81
|
+
require_user(request)
|
|
82
|
+
gate_read(request)
|
|
83
|
+
try:
|
|
84
|
+
return {"template": catalog.clone_template(kind, template_id, req.name)}
|
|
85
|
+
except MarketplaceError as exc:
|
|
86
|
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
|
87
|
+
|
|
75
88
|
@router.get("/marketplace/templates/registry")
|
|
76
89
|
async def template_registry(request: Request):
|
|
77
90
|
require_user(request)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Memory platform + Memory Manager API router (v3.2.0).
|
|
2
|
+
|
|
3
|
+
Exposes :class:`~latticeai.services.memory_service.MemoryService` so the /app
|
|
4
|
+
Memory view can inspect every memory tier, recall across them, and run manager
|
|
5
|
+
operations (prune / compact / rebuild / clear). Full paths in decorators.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, List, Optional
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from latticeai.services.memory_service import MemoryService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RecallRequest(BaseModel):
|
|
19
|
+
query: str = ""
|
|
20
|
+
limit: int = 20
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PruneRequest(BaseModel):
|
|
24
|
+
ids: List[str] = []
|
|
25
|
+
kind: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RebuildRequest(BaseModel):
|
|
29
|
+
target: str = "vector"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClearRequest(BaseModel):
|
|
33
|
+
scope: str
|
|
34
|
+
confirm: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_memory_router(
|
|
38
|
+
*,
|
|
39
|
+
service: MemoryService,
|
|
40
|
+
require_user: Callable[[Request], str],
|
|
41
|
+
get_current_user: Callable[[Request], Optional[str]],
|
|
42
|
+
gate_read: Callable[[Request], Optional[str]],
|
|
43
|
+
gate_write: Callable[[Request], Optional[str]],
|
|
44
|
+
append_audit_event: Callable[..., None],
|
|
45
|
+
) -> APIRouter:
|
|
46
|
+
router = APIRouter()
|
|
47
|
+
|
|
48
|
+
@router.get("/api/memory/manager")
|
|
49
|
+
async def memory_manager(request: Request):
|
|
50
|
+
user = require_user(request)
|
|
51
|
+
scope = gate_read(request)
|
|
52
|
+
return service.manager(user_email=user, workspace_id=scope)
|
|
53
|
+
|
|
54
|
+
@router.get("/api/memory/tiers")
|
|
55
|
+
async def memory_tiers(request: Request):
|
|
56
|
+
require_user(request)
|
|
57
|
+
return service.tiers()
|
|
58
|
+
|
|
59
|
+
@router.get("/api/memory/inspect")
|
|
60
|
+
async def memory_inspect(request: Request, source: str, limit: int = 50):
|
|
61
|
+
user = require_user(request)
|
|
62
|
+
scope = gate_read(request)
|
|
63
|
+
try:
|
|
64
|
+
return service.inspect(source, user_email=user, workspace_id=scope, limit=limit)
|
|
65
|
+
except KeyError as exc:
|
|
66
|
+
raise HTTPException(status_code=404, detail=f"Unknown memory source: {source}") from exc
|
|
67
|
+
|
|
68
|
+
@router.post("/api/memory/recall")
|
|
69
|
+
async def memory_recall(req: RecallRequest, request: Request):
|
|
70
|
+
user = require_user(request)
|
|
71
|
+
scope = gate_read(request)
|
|
72
|
+
return service.recall(req.query, user_email=user, workspace_id=scope, limit=req.limit)
|
|
73
|
+
|
|
74
|
+
@router.post("/api/memory/prune")
|
|
75
|
+
async def memory_prune(req: PruneRequest, request: Request):
|
|
76
|
+
user = require_user(request)
|
|
77
|
+
gate_write(request)
|
|
78
|
+
result = service.prune(ids=req.ids, kind=req.kind, user_email=user)
|
|
79
|
+
append_audit_event("memory_prune", user_email=user, count=result.get("count", 0))
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
@router.post("/api/memory/compact")
|
|
83
|
+
async def memory_compact(request: Request):
|
|
84
|
+
user = require_user(request)
|
|
85
|
+
gate_write(request)
|
|
86
|
+
result = service.compact(user_email=user)
|
|
87
|
+
append_audit_event("memory_compact", user_email=user, compacted=result.get("compacted", 0))
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
@router.post("/api/memory/rebuild")
|
|
91
|
+
async def memory_rebuild(req: RebuildRequest, request: Request):
|
|
92
|
+
user = require_user(request)
|
|
93
|
+
gate_write(request)
|
|
94
|
+
result = service.rebuild(req.target)
|
|
95
|
+
append_audit_event("memory_rebuild", user_email=user, target=req.target, status=result.get("status"))
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
@router.post("/api/memory/clear")
|
|
99
|
+
async def memory_clear(req: ClearRequest, request: Request):
|
|
100
|
+
user = require_user(request)
|
|
101
|
+
gate_write(request)
|
|
102
|
+
try:
|
|
103
|
+
result = service.clear(scope=req.scope, confirm=req.confirm, user_email=user)
|
|
104
|
+
except ValueError as exc:
|
|
105
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
106
|
+
append_audit_event("memory_clear", user_email=user, scope=req.scope)
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
return router
|
package/latticeai/api/search.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any, Callable, Dict, Optional
|
|
|
7
7
|
from fastapi import APIRouter, HTTPException, Request
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
+
from latticeai.core.embedding_providers import embedding_provider_profiles
|
|
10
11
|
from latticeai.services.search_service import DEFAULT_HYBRID_WEIGHTS, SearchService
|
|
11
12
|
|
|
12
13
|
|
|
@@ -213,9 +214,12 @@ def create_search_router(
|
|
|
213
214
|
async def embeddings_providers(request: Request) -> Dict[str, Any]:
|
|
214
215
|
require_user(request)
|
|
215
216
|
resolved = embedding_info() if embedding_info else {}
|
|
217
|
+
profiles = resolved.get("profiles") or embedding_provider_profiles()
|
|
216
218
|
return {
|
|
217
219
|
"active": resolved.get("active_provider"),
|
|
218
220
|
"requested": resolved.get("requested_provider"),
|
|
221
|
+
"profile": resolved.get("profile") or "",
|
|
222
|
+
"profiles": profiles,
|
|
219
223
|
"providers": [
|
|
220
224
|
{"id": "hash", "label": "Local hash (fallback)", "grade": "fallback",
|
|
221
225
|
"requires": [], "detail": "Deterministic offline vectors — always available."},
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Agent Registry — registration, discovery, metadata, versioning, capabilities.
|
|
2
|
+
|
|
3
|
+
Part 2 of the v3.2.0 platform. Before this module the agent roster was derived
|
|
4
|
+
ad-hoc from the hardcoded ``AGENT_ROLES`` tuple wherever it was needed. The
|
|
5
|
+
registry makes every agent — built-in role and user-registered custom agent — a
|
|
6
|
+
first-class entry with stable metadata: ``id``, ``type``, ``version``,
|
|
7
|
+
``capabilities``, ``description`` and a mutable ``config``. The HTTP surface and
|
|
8
|
+
the /app views read this registry instead of any hardcoded list.
|
|
9
|
+
|
|
10
|
+
Built-in role agents are projected from the single source of truth in
|
|
11
|
+
:mod:`latticeai.core.multi_agent` (so adding a role there flows through here).
|
|
12
|
+
Custom agents and config overrides are persisted to
|
|
13
|
+
``data_dir/agent_registry.json``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import tempfile
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
from latticeai.core.multi_agent import (
|
|
26
|
+
AGENT_ROLES,
|
|
27
|
+
CORE_PIPELINE,
|
|
28
|
+
MULTI_AGENT_VERSION,
|
|
29
|
+
ROLE_AGENT_IDS,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
AGENT_TYPES = ("planner", "researcher", "executor", "reviewer", "release", "custom")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now() -> str:
|
|
36
|
+
return datetime.now().isoformat(timespec="seconds")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Capabilities + descriptions for the built-in role agents. Kept here as the
|
|
40
|
+
# registry's metadata projection of the roles defined in multi_agent.py.
|
|
41
|
+
ROLE_META: Dict[str, Dict[str, Any]] = {
|
|
42
|
+
"researcher": {
|
|
43
|
+
"description": "Gathers workspace context, memory, and graph signal for the goal.",
|
|
44
|
+
"capabilities": ["context-retrieval", "memory-recall", "graph-read", "hybrid-search"],
|
|
45
|
+
},
|
|
46
|
+
"planner": {
|
|
47
|
+
"description": "Decomposes the goal into an ordered, bounded, reviewable plan.",
|
|
48
|
+
"capabilities": ["task-decomposition", "plan-review", "delegation"],
|
|
49
|
+
},
|
|
50
|
+
"executor": {
|
|
51
|
+
"description": "Executes each planned step, invoking tools, workflows, and plugins.",
|
|
52
|
+
"capabilities": ["tool-use", "workflow-run", "plugin-run", "file-write"],
|
|
53
|
+
},
|
|
54
|
+
"reviewer": {
|
|
55
|
+
"description": "Reviews executed work and approves, rejects, or requests a retry.",
|
|
56
|
+
"capabilities": ["verification", "retry-control", "approval"],
|
|
57
|
+
},
|
|
58
|
+
"release": {
|
|
59
|
+
"description": "Finalizes and summarizes the approved outcome.",
|
|
60
|
+
"capabilities": ["summarize", "finalize"],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AgentRegistry:
|
|
66
|
+
"""Persisted registry of built-in role agents + user-registered agents."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, path: Path):
|
|
69
|
+
self.path = Path(path)
|
|
70
|
+
self._state: Dict[str, Any] = self._load()
|
|
71
|
+
|
|
72
|
+
# ── persistence ───────────────────────────────────────────────────────
|
|
73
|
+
def _load(self) -> Dict[str, Any]:
|
|
74
|
+
if self.path.exists():
|
|
75
|
+
try:
|
|
76
|
+
with open(self.path, "r", encoding="utf-8") as fh:
|
|
77
|
+
data = json.load(fh)
|
|
78
|
+
if isinstance(data, dict):
|
|
79
|
+
data.setdefault("custom", [])
|
|
80
|
+
data.setdefault("config_overrides", {})
|
|
81
|
+
return data
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
return {"custom": [], "config_overrides": {}}
|
|
85
|
+
|
|
86
|
+
def _save(self) -> None:
|
|
87
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
fd, tmp = tempfile.mkstemp(dir=str(self.path.parent), suffix=".tmp")
|
|
89
|
+
try:
|
|
90
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
91
|
+
json.dump(self._state, fh, ensure_ascii=False, indent=2)
|
|
92
|
+
os.replace(tmp, self.path)
|
|
93
|
+
finally:
|
|
94
|
+
if os.path.exists(tmp):
|
|
95
|
+
os.unlink(tmp)
|
|
96
|
+
|
|
97
|
+
# ── projection ────────────────────────────────────────────────────────
|
|
98
|
+
def _builtin_agents(self) -> List[Dict[str, Any]]:
|
|
99
|
+
overrides = self._state.get("config_overrides", {})
|
|
100
|
+
agents: List[Dict[str, Any]] = []
|
|
101
|
+
for role in AGENT_ROLES:
|
|
102
|
+
meta = ROLE_META.get(role, {"description": "", "capabilities": []})
|
|
103
|
+
agent_id = ROLE_AGENT_IDS.get(role, f"agent:{role}")
|
|
104
|
+
handoffs: List[str] = []
|
|
105
|
+
if role == "planner":
|
|
106
|
+
handoffs = [ROLE_AGENT_IDS["executor"]]
|
|
107
|
+
elif role == "executor":
|
|
108
|
+
handoffs = [ROLE_AGENT_IDS["reviewer"]]
|
|
109
|
+
ov = overrides.get(agent_id, {})
|
|
110
|
+
agents.append({
|
|
111
|
+
"id": agent_id,
|
|
112
|
+
"name": role.capitalize(),
|
|
113
|
+
"type": role,
|
|
114
|
+
"version": MULTI_AGENT_VERSION,
|
|
115
|
+
"description": meta["description"],
|
|
116
|
+
"capabilities": list(meta["capabilities"]),
|
|
117
|
+
"handoffs": handoffs,
|
|
118
|
+
"in_default_pipeline": role in CORE_PIPELINE,
|
|
119
|
+
"source": "builtin",
|
|
120
|
+
"removable": False,
|
|
121
|
+
"enabled": bool(ov.get("enabled", True)),
|
|
122
|
+
"config": ov.get("config", {}),
|
|
123
|
+
})
|
|
124
|
+
return agents
|
|
125
|
+
|
|
126
|
+
def _custom_agents(self) -> List[Dict[str, Any]]:
|
|
127
|
+
out: List[Dict[str, Any]] = []
|
|
128
|
+
for entry in self._state.get("custom", []):
|
|
129
|
+
agent = dict(entry)
|
|
130
|
+
agent["source"] = "user"
|
|
131
|
+
agent["removable"] = True
|
|
132
|
+
agent.setdefault("enabled", True)
|
|
133
|
+
agent.setdefault("handoffs", [])
|
|
134
|
+
out.append(agent)
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
def all(self) -> List[Dict[str, Any]]:
|
|
138
|
+
return self._builtin_agents() + self._custom_agents()
|
|
139
|
+
|
|
140
|
+
# ── reads ─────────────────────────────────────────────────────────────
|
|
141
|
+
def list(self, agent_type: Optional[str] = None) -> Dict[str, Any]:
|
|
142
|
+
agents = self.all()
|
|
143
|
+
if agent_type:
|
|
144
|
+
agents = [a for a in agents if a["type"] == agent_type]
|
|
145
|
+
counts: Dict[str, int] = {}
|
|
146
|
+
for a in self.all():
|
|
147
|
+
counts[a["type"]] = counts.get(a["type"], 0) + 1
|
|
148
|
+
return {
|
|
149
|
+
"agents": agents,
|
|
150
|
+
"types": list(AGENT_TYPES),
|
|
151
|
+
"counts": counts,
|
|
152
|
+
"total": len(agents),
|
|
153
|
+
"version": MULTI_AGENT_VERSION,
|
|
154
|
+
"default_pipeline": list(CORE_PIPELINE),
|
|
155
|
+
"generated_at": _now(),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
def get(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
159
|
+
return next((a for a in self.all() if a["id"] == agent_id), None)
|
|
160
|
+
|
|
161
|
+
def capabilities(self) -> Dict[str, List[str]]:
|
|
162
|
+
"""Inverted index: capability -> [agent_id, …]."""
|
|
163
|
+
index: Dict[str, List[str]] = {}
|
|
164
|
+
for a in self.all():
|
|
165
|
+
for cap in a.get("capabilities", []):
|
|
166
|
+
index.setdefault(cap, []).append(a["id"])
|
|
167
|
+
return index
|
|
168
|
+
|
|
169
|
+
def discover(self, capability: str) -> List[Dict[str, Any]]:
|
|
170
|
+
cap = str(capability or "").lower().strip()
|
|
171
|
+
return [a for a in self.all() if any(cap == c.lower() for c in a.get("capabilities", []))]
|
|
172
|
+
|
|
173
|
+
# ── mutations ─────────────────────────────────────────────────────────
|
|
174
|
+
def register(
|
|
175
|
+
self,
|
|
176
|
+
*,
|
|
177
|
+
name: str,
|
|
178
|
+
agent_type: str = "custom",
|
|
179
|
+
description: str = "",
|
|
180
|
+
capabilities: Optional[List[str]] = None,
|
|
181
|
+
config: Optional[Dict[str, Any]] = None,
|
|
182
|
+
version: str = "1.0.0",
|
|
183
|
+
) -> Dict[str, Any]:
|
|
184
|
+
if not str(name).strip():
|
|
185
|
+
raise ValueError("name is required")
|
|
186
|
+
if agent_type not in AGENT_TYPES:
|
|
187
|
+
raise ValueError(f"type must be one of {', '.join(AGENT_TYPES)}")
|
|
188
|
+
slug = str(name).strip().lower().replace(" ", "-")
|
|
189
|
+
agent_id = f"agent:custom:{slug}"
|
|
190
|
+
existing = {c["id"] for c in self._state.get("custom", [])}
|
|
191
|
+
if agent_id in existing:
|
|
192
|
+
agent_id = f"agent:custom:{slug}-{len(existing) + 1}"
|
|
193
|
+
entry = {
|
|
194
|
+
"id": agent_id,
|
|
195
|
+
"name": str(name).strip(),
|
|
196
|
+
"type": agent_type,
|
|
197
|
+
"version": str(version or "1.0.0"),
|
|
198
|
+
"description": str(description or "").strip(),
|
|
199
|
+
"capabilities": list(capabilities or []),
|
|
200
|
+
"config": dict(config or {}),
|
|
201
|
+
"enabled": True,
|
|
202
|
+
"created_at": _now(),
|
|
203
|
+
}
|
|
204
|
+
self._state.setdefault("custom", []).append(entry)
|
|
205
|
+
self._save()
|
|
206
|
+
return entry
|
|
207
|
+
|
|
208
|
+
def update_config(self, agent_id: str, config: Dict[str, Any], *, enabled: Optional[bool] = None) -> Dict[str, Any]:
|
|
209
|
+
if self.get(agent_id) is None:
|
|
210
|
+
raise KeyError(agent_id)
|
|
211
|
+
if agent_id.startswith("agent:custom:"):
|
|
212
|
+
for entry in self._state.get("custom", []):
|
|
213
|
+
if entry["id"] == agent_id:
|
|
214
|
+
entry["config"] = dict(config or {})
|
|
215
|
+
if enabled is not None:
|
|
216
|
+
entry["enabled"] = bool(enabled)
|
|
217
|
+
entry["updated_at"] = _now()
|
|
218
|
+
else:
|
|
219
|
+
ov = self._state.setdefault("config_overrides", {}).setdefault(agent_id, {})
|
|
220
|
+
ov["config"] = dict(config or {})
|
|
221
|
+
if enabled is not None:
|
|
222
|
+
ov["enabled"] = bool(enabled)
|
|
223
|
+
self._save()
|
|
224
|
+
return self.get(agent_id) # type: ignore[return-value]
|
|
225
|
+
|
|
226
|
+
def remove(self, agent_id: str) -> Dict[str, Any]:
|
|
227
|
+
if not agent_id.startswith("agent:custom:"):
|
|
228
|
+
raise ValueError("Built-in role agents cannot be removed; disable them via config instead.")
|
|
229
|
+
before = len(self._state.get("custom", []))
|
|
230
|
+
self._state["custom"] = [c for c in self._state.get("custom", []) if c["id"] != agent_id]
|
|
231
|
+
if len(self._state["custom"]) == before:
|
|
232
|
+
raise KeyError(agent_id)
|
|
233
|
+
self._save()
|
|
234
|
+
return {"removed": agent_id}
|
package/latticeai/core/config.py
CHANGED
|
@@ -95,6 +95,7 @@ class Config:
|
|
|
95
95
|
|
|
96
96
|
# ── embeddings (retrieval vector signal) ────────────────────────
|
|
97
97
|
embedding_provider: str
|
|
98
|
+
embedding_profile: str
|
|
98
99
|
embedding_model: str
|
|
99
100
|
embedding_base_url: str
|
|
100
101
|
embedding_api_key: str
|
|
@@ -174,6 +175,7 @@ class Config:
|
|
|
174
175
|
local_draft_model=_value(env, "LATTICEAI_LOCAL_DRAFT_MODEL", ""),
|
|
175
176
|
auto_read_chat_paths=_bool(env, "LATTICEAI_AUTO_READ_CHAT_PATHS", default=False),
|
|
176
177
|
embedding_provider=_value(env, "LATTICEAI_EMBEDDING_PROVIDER", "hash").strip().lower(),
|
|
178
|
+
embedding_profile=_value(env, "LATTICEAI_EMBEDDING_PROFILE", "").strip().lower(),
|
|
177
179
|
embedding_model=_value(env, "LATTICEAI_EMBEDDING_MODEL", ""),
|
|
178
180
|
embedding_base_url=_value(env, "LATTICEAI_EMBEDDING_BASE_URL", ""),
|
|
179
181
|
embedding_api_key=_value(env, "LATTICEAI_EMBEDDING_API_KEY", ""),
|
|
@@ -51,6 +51,7 @@ class EmbeddingUnavailable(RuntimeError):
|
|
|
51
51
|
# identity is stable before the first (possibly remote) call. A configured
|
|
52
52
|
# ``dim`` always wins; an unknown model falls back to a one-time live probe.
|
|
53
53
|
_KNOWN_DIMS = {
|
|
54
|
+
"bge-m3": 1024,
|
|
54
55
|
"nomic-embed-text": 768,
|
|
55
56
|
"mxbai-embed-large": 1024,
|
|
56
57
|
"all-minilm": 384,
|
|
@@ -61,12 +62,131 @@ _KNOWN_DIMS = {
|
|
|
61
62
|
"gte-small": 384,
|
|
62
63
|
"gte-base": 768,
|
|
63
64
|
"gte-large": 1024,
|
|
65
|
+
"e5-large": 1024,
|
|
66
|
+
"multilingual-e5-large": 1024,
|
|
64
67
|
"text-embedding-3-small": 1536,
|
|
65
68
|
"text-embedding-3-large": 3072,
|
|
66
69
|
"text-embedding-ada-002": 1536,
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
|
|
73
|
+
PRODUCTION_PROVIDER_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
74
|
+
"local:bge-m3": {
|
|
75
|
+
"id": "local:bge-m3",
|
|
76
|
+
"provider": "mlx",
|
|
77
|
+
"model": "bge-m3",
|
|
78
|
+
"dimensions": 1024,
|
|
79
|
+
"grade": "production",
|
|
80
|
+
"family": "local",
|
|
81
|
+
"label": "BGE-M3 local",
|
|
82
|
+
"detail": "Multilingual semantic embeddings for local retrieval.",
|
|
83
|
+
},
|
|
84
|
+
"local:nomic-embed-text": {
|
|
85
|
+
"id": "local:nomic-embed-text",
|
|
86
|
+
"provider": "ollama",
|
|
87
|
+
"model": "nomic-embed-text",
|
|
88
|
+
"dimensions": 768,
|
|
89
|
+
"grade": "production",
|
|
90
|
+
"family": "local",
|
|
91
|
+
"label": "Nomic Embed Text local",
|
|
92
|
+
"detail": "General-purpose local semantic embeddings.",
|
|
93
|
+
},
|
|
94
|
+
"local:e5-large": {
|
|
95
|
+
"id": "local:e5-large",
|
|
96
|
+
"provider": "mlx",
|
|
97
|
+
"model": "e5-large",
|
|
98
|
+
"dimensions": 1024,
|
|
99
|
+
"grade": "production",
|
|
100
|
+
"family": "local",
|
|
101
|
+
"label": "E5 Large local",
|
|
102
|
+
"detail": "High-recall local retrieval profile.",
|
|
103
|
+
},
|
|
104
|
+
"local:gte-large": {
|
|
105
|
+
"id": "local:gte-large",
|
|
106
|
+
"provider": "mlx",
|
|
107
|
+
"model": "gte-large",
|
|
108
|
+
"dimensions": 1024,
|
|
109
|
+
"grade": "production",
|
|
110
|
+
"family": "local",
|
|
111
|
+
"label": "GTE Large local",
|
|
112
|
+
"detail": "Large local semantic embedding profile.",
|
|
113
|
+
},
|
|
114
|
+
"ollama:nomic-embed-text": {
|
|
115
|
+
"id": "ollama:nomic-embed-text",
|
|
116
|
+
"provider": "ollama",
|
|
117
|
+
"model": "nomic-embed-text",
|
|
118
|
+
"dimensions": 768,
|
|
119
|
+
"grade": "production",
|
|
120
|
+
"family": "ollama",
|
|
121
|
+
"label": "Ollama Nomic Embed Text",
|
|
122
|
+
"detail": "Production semantic embeddings through Ollama.",
|
|
123
|
+
},
|
|
124
|
+
"ollama:mxbai-embed-large": {
|
|
125
|
+
"id": "ollama:mxbai-embed-large",
|
|
126
|
+
"provider": "ollama",
|
|
127
|
+
"model": "mxbai-embed-large",
|
|
128
|
+
"dimensions": 1024,
|
|
129
|
+
"grade": "production",
|
|
130
|
+
"family": "ollama",
|
|
131
|
+
"label": "Ollama MXBAI Embed Large",
|
|
132
|
+
"detail": "High-quality local semantic embeddings through Ollama.",
|
|
133
|
+
},
|
|
134
|
+
"ollama:bge-m3": {
|
|
135
|
+
"id": "ollama:bge-m3",
|
|
136
|
+
"provider": "ollama",
|
|
137
|
+
"model": "bge-m3",
|
|
138
|
+
"dimensions": 1024,
|
|
139
|
+
"grade": "production",
|
|
140
|
+
"family": "ollama",
|
|
141
|
+
"label": "Ollama BGE-M3-compatible",
|
|
142
|
+
"detail": "BGE-M3-compatible providers exposed through Ollama.",
|
|
143
|
+
},
|
|
144
|
+
"mlx:bge-m3": {
|
|
145
|
+
"id": "mlx:bge-m3",
|
|
146
|
+
"provider": "mlx",
|
|
147
|
+
"model": "bge-m3",
|
|
148
|
+
"dimensions": 1024,
|
|
149
|
+
"grade": "production",
|
|
150
|
+
"family": "mlx",
|
|
151
|
+
"label": "MLX BGE-M3",
|
|
152
|
+
"detail": "Apple Silicon optimized local embeddings.",
|
|
153
|
+
},
|
|
154
|
+
"openai:text-embedding-3-small": {
|
|
155
|
+
"id": "openai:text-embedding-3-small",
|
|
156
|
+
"provider": "openai",
|
|
157
|
+
"model": "text-embedding-3-small",
|
|
158
|
+
"dimensions": 1536,
|
|
159
|
+
"grade": "production",
|
|
160
|
+
"family": "openai-compatible",
|
|
161
|
+
"label": "OpenAI-compatible small",
|
|
162
|
+
"detail": "OpenAI-compatible /v1/embeddings endpoint.",
|
|
163
|
+
},
|
|
164
|
+
"openai:text-embedding-3-large": {
|
|
165
|
+
"id": "openai:text-embedding-3-large",
|
|
166
|
+
"provider": "openai",
|
|
167
|
+
"model": "text-embedding-3-large",
|
|
168
|
+
"dimensions": 3072,
|
|
169
|
+
"grade": "production",
|
|
170
|
+
"family": "openai-compatible",
|
|
171
|
+
"label": "OpenAI-compatible large",
|
|
172
|
+
"detail": "Highest-dimensional OpenAI-compatible embedding profile.",
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def embedding_provider_profiles() -> List[Dict[str, Any]]:
|
|
178
|
+
return [dict(PRODUCTION_PROVIDER_PROFILES[key]) for key in sorted(PRODUCTION_PROVIDER_PROFILES)]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def resolve_embedding_profile(profile: str) -> Dict[str, Any]:
|
|
182
|
+
if not profile:
|
|
183
|
+
return {}
|
|
184
|
+
key = str(profile).strip().lower()
|
|
185
|
+
if key in PRODUCTION_PROVIDER_PROFILES:
|
|
186
|
+
return dict(PRODUCTION_PROVIDER_PROFILES[key])
|
|
187
|
+
raise ValueError(f"unknown embedding profile: {profile!r}")
|
|
188
|
+
|
|
189
|
+
|
|
70
190
|
def _guess_dim(model: str, default: int) -> int:
|
|
71
191
|
key = str(model or "").split("/")[-1].strip().lower()
|
|
72
192
|
key = key.split(":")[0]
|
|
@@ -498,5 +618,8 @@ __all__ = [
|
|
|
498
618
|
"ResolvedEmbedder",
|
|
499
619
|
"build_embedding_provider",
|
|
500
620
|
"resolve_embedder",
|
|
621
|
+
"resolve_embedding_profile",
|
|
622
|
+
"embedding_provider_profiles",
|
|
623
|
+
"PRODUCTION_PROVIDER_PROFILES",
|
|
501
624
|
"PROVIDER_TYPES",
|
|
502
625
|
]
|