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.
Files changed (123) hide show
  1. package/README.md +54 -21
  2. package/docs/CHANGELOG.md +90 -0
  3. package/docs/V3_2_AUDIT.md +82 -0
  4. package/docs/V3_FRONTEND.md +20 -17
  5. package/docs/architecture.md +6 -0
  6. package/latticeai/__init__.py +1 -1
  7. package/latticeai/api/agent_registry.py +103 -0
  8. package/latticeai/api/auth.py +4 -1
  9. package/latticeai/api/hooks.py +113 -0
  10. package/latticeai/api/marketplace.py +13 -0
  11. package/latticeai/api/memory.py +109 -0
  12. package/latticeai/api/search.py +4 -0
  13. package/latticeai/core/agent_registry.py +234 -0
  14. package/latticeai/core/config.py +2 -0
  15. package/latticeai/core/embedding_providers.py +123 -0
  16. package/latticeai/core/hooks.py +284 -0
  17. package/latticeai/core/marketplace.py +87 -2
  18. package/latticeai/core/multi_agent.py +1 -1
  19. package/latticeai/core/workspace_os.py +1 -1
  20. package/latticeai/server_app.py +63 -6
  21. package/latticeai/services/memory_service.py +324 -0
  22. package/package.json +9 -4
  23. package/scripts/build_v3_assets.mjs +164 -0
  24. package/scripts/capture/README.md +28 -0
  25. package/scripts/capture/capture_enterprise.js +8 -0
  26. package/scripts/capture/capture_graph.js +8 -0
  27. package/scripts/capture/capture_onboarding.js +8 -0
  28. package/scripts/capture/capture_page.js +43 -0
  29. package/scripts/capture/capture_release_media.js +125 -0
  30. package/scripts/capture/capture_skills.js +8 -0
  31. package/scripts/capture/capture_workspace.js +8 -0
  32. package/scripts/generate_diagrams.py +513 -0
  33. package/scripts/lint_v3.mjs +33 -0
  34. package/scripts/release-0.3.1.sh +105 -0
  35. package/scripts/take_screenshots.js +69 -0
  36. package/scripts/validate_release_artifacts.py +167 -0
  37. package/static/account.html +9 -9
  38. package/static/activity.html +4 -4
  39. package/static/admin.html +8 -8
  40. package/static/agents.html +4 -4
  41. package/static/chat.html +9 -9
  42. package/static/css/tokens.5a595671.css +260 -0
  43. package/static/css/tokens.css +1 -1
  44. package/static/graph.html +9 -9
  45. package/static/plugins.html +4 -4
  46. package/static/sw.js +3 -1
  47. package/static/v3/asset-manifest.json +55 -0
  48. package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
  49. package/static/v3/css/lattice.components.011e988b.css +447 -0
  50. package/static/v3/css/lattice.components.css +2 -2
  51. package/static/v3/css/lattice.shell.4920f42d.css +407 -0
  52. package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
  53. package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
  54. package/static/v3/index.html +38 -9
  55. package/static/v3/js/app.a5adc0f3.js +26 -0
  56. package/static/v3/js/core/api.603b978f.js +408 -0
  57. package/static/v3/js/core/api.js +132 -51
  58. package/static/v3/js/core/components.4c83e0a9.js +222 -0
  59. package/static/v3/js/core/components.js +9 -2
  60. package/static/v3/js/core/dom.a2773eb0.js +148 -0
  61. package/static/v3/js/core/router.584570f2.js +37 -0
  62. package/static/v3/js/core/routes.07ad6696.js +89 -0
  63. package/static/v3/js/core/routes.js +17 -1
  64. package/static/v3/js/core/shell.ea0b9ae5.js +363 -0
  65. package/static/v3/js/core/store.34ebd5e6.js +113 -0
  66. package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
  67. package/static/v3/js/views/admin-audit.js +1 -1
  68. package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
  69. package/static/v3/js/views/admin-permissions.js +4 -5
  70. package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
  71. package/static/v3/js/views/admin-policies.js +4 -5
  72. package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
  73. package/static/v3/js/views/admin-private-vpc.js +2 -5
  74. package/static/v3/js/views/admin-security.07c66b72.js +180 -0
  75. package/static/v3/js/views/admin-security.js +4 -5
  76. package/static/v3/js/views/admin-users.03bac88c.js +168 -0
  77. package/static/v3/js/views/admin-users.js +6 -6
  78. package/static/v3/js/views/agents.c373d48c.js +293 -0
  79. package/static/v3/js/views/agents.js +101 -2
  80. package/static/v3/js/views/chat.718144ce.js +449 -0
  81. package/static/v3/js/views/chat.js +2 -3
  82. package/static/v3/js/views/files.4935197e.js +186 -0
  83. package/static/v3/js/views/files.js +27 -21
  84. package/static/v3/js/views/home.cdde3b32.js +119 -0
  85. package/static/v3/js/views/hooks.f3edebca.js +99 -0
  86. package/static/v3/js/views/hooks.js +99 -0
  87. package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
  88. package/static/v3/js/views/hybrid-search.js +1 -1
  89. package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
  90. package/static/v3/js/views/knowledge-graph.js +2 -3
  91. package/static/v3/js/views/marketplace.ab0583d4.js +141 -0
  92. package/static/v3/js/views/marketplace.js +141 -0
  93. package/static/v3/js/views/mcp.99b5c6a7.js +114 -0
  94. package/static/v3/js/views/mcp.js +114 -0
  95. package/static/v3/js/views/memory.d2ed7a7c.js +146 -0
  96. package/static/v3/js/views/memory.js +146 -0
  97. package/static/v3/js/views/models.a1ffa147.js +256 -0
  98. package/static/v3/js/views/models.js +17 -8
  99. package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
  100. package/static/v3/js/views/my-computer.js +5 -5
  101. package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
  102. package/static/v3/js/views/pipeline.js +3 -7
  103. package/static/v3/js/views/planning.9ac3e313.js +153 -0
  104. package/static/v3/js/views/planning.js +153 -0
  105. package/static/v3/js/views/settings.4f777210.js +250 -0
  106. package/static/v3/js/views/settings.js +6 -14
  107. package/static/v3/js/views/skills.c6c2f965.js +109 -0
  108. package/static/v3/js/views/skills.js +109 -0
  109. package/static/v3/js/views/tools.e4f11276.js +108 -0
  110. package/static/v3/js/views/tools.js +108 -0
  111. package/static/v3/js/views/workflows.26c57290.js +128 -0
  112. package/static/v3/js/views/workflows.js +128 -0
  113. package/static/workflows.html +4 -4
  114. package/static/workspace.html +5 -5
  115. package/docs/images/tmp_frames/frame_00.png +0 -0
  116. package/docs/images/tmp_frames/frame_01.png +0 -0
  117. package/docs/images/tmp_frames/frame_02.png +0 -0
  118. package/docs/images/tmp_frames/frame_03.png +0 -0
  119. package/docs/images/tmp_frames/hero_00.png +0 -0
  120. package/docs/images/tmp_frames/hero_01.png +0 -0
  121. package/docs/images/tmp_frames/hero_02.png +0 -0
  122. package/docs/images/tmp_frames/hero_03.png +0 -0
  123. 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
@@ -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}
@@ -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
  ]