ltcai 10.3.0 → 10.4.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 +37 -36
- package/docs/CHANGELOG.md +77 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/MYPY_BACKLOG.md +59 -92
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/SURFACE_PARITY.md +33 -6
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/context.py +23 -3
- package/lattice_brain/graph/_kg_common.py +164 -9
- package/lattice_brain/graph/_kg_contract.py +223 -0
- package/lattice_brain/graph/_kg_fsutil.py +1 -1
- package/lattice_brain/graph/curator.py +4 -4
- package/lattice_brain/graph/discovery.py +12 -1
- package/lattice_brain/graph/discovery_index.py +12 -1
- package/lattice_brain/graph/documents.py +17 -2
- package/lattice_brain/graph/ingest.py +12 -1
- package/lattice_brain/graph/proactive.py +9 -5
- package/lattice_brain/graph/projection.py +12 -1
- package/lattice_brain/graph/provenance.py +12 -1
- package/lattice_brain/graph/retrieval.py +12 -1
- package/lattice_brain/graph/retrieval_docgen.py +12 -1
- package/lattice_brain/graph/retrieval_reads.py +12 -1
- package/lattice_brain/graph/retrieval_vector.py +12 -1
- package/lattice_brain/graph/schema.py +6 -2
- package/lattice_brain/graph/write_master.py +12 -1
- package/lattice_brain/ingestion.py +1 -1
- package/lattice_brain/quality.py +5 -5
- package/lattice_brain/runtime/contracts.py +2 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/workflow.py +10 -7
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +2 -2
- package/latticeai/api/chat.py +19 -15
- package/latticeai/api/chat_agent_http.py +19 -7
- package/latticeai/api/chat_helpers.py +9 -8
- package/latticeai/api/chat_stream.py +2 -2
- package/latticeai/api/computer_use.py +2 -2
- package/latticeai/api/hooks.py +4 -2
- package/latticeai/api/local_files.py +8 -4
- package/latticeai/api/mcp.py +2 -2
- package/latticeai/api/models.py +5 -5
- package/latticeai/api/permissions.py +2 -2
- package/latticeai/api/search.py +5 -3
- package/latticeai/api/security_dashboard.py +3 -5
- package/latticeai/api/setup.py +3 -3
- package/latticeai/api/static_routes.py +7 -2
- package/latticeai/api/tools.py +9 -4
- package/latticeai/api/workspace.py +25 -22
- package/latticeai/app_factory.py +132 -1322
- package/latticeai/cli/entrypoint.py +1 -1
- package/latticeai/core/agent.py +28 -14
- package/latticeai/core/audit.py +5 -3
- package/latticeai/core/embedding_providers.py +19 -6
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +2 -2
- package/latticeai/core/mcp_catalog.py +3 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/plugins.py +1 -1
- package/latticeai/core/project_sessions.py +1 -1
- package/latticeai/core/run_explain.py +6 -4
- package/latticeai/core/workspace_os.py +0 -4
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_state.py +1 -1
- package/latticeai/core/workspace_snapshots.py +3 -3
- package/latticeai/integrations/telegram_bot.py +52 -3
- package/latticeai/models/router.py +61 -34
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/build_phases.py +1417 -0
- package/latticeai/runtime/review_wiring.py +6 -1
- package/latticeai/runtime/runtime_context.py +276 -0
- package/latticeai/runtime/sso_config_runtime.py +1 -1
- package/latticeai/runtime/stages.py +8 -2
- package/latticeai/services/app_context.py +23 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +4 -2
- package/latticeai/services/cloud_streaming.py +5 -3
- package/latticeai/services/command_center.py +1 -1
- package/latticeai/services/evidence_actions.py +2 -1
- package/latticeai/services/hybrid_chat.py +1 -1
- package/latticeai/services/local_knowledge.py +25 -9
- package/latticeai/services/memory_service.py +8 -4
- package/latticeai/services/model_catalog.py +12 -12
- package/latticeai/services/model_engines.py +15 -17
- package/latticeai/services/model_loading.py +1 -1
- package/latticeai/services/model_runtime.py +31 -33
- package/latticeai/services/multimodal_streaming.py +10 -4
- package/latticeai/services/openai_compatible_adapter.py +4 -3
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +12 -3
- package/latticeai/services/search_service.py +1 -1
- package/latticeai/services/triggers.py +1 -1
- package/latticeai/setup/auto_setup.py +1 -1
- package/latticeai/setup/wizard.py +12 -3
- package/latticeai/tools/computer.py +3 -1
- package/latticeai/tools/documents.py +2 -2
- package/package.json +1 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +36 -36
- package/static/app/assets/{Act-DrKIGZeJ.js → Act-BK__Y5VE.js} +1 -1
- package/static/app/assets/{AdminConsole-CEV1VefA.js → AdminConsole-ZrTyN8_F.js} +1 -1
- package/static/app/assets/{Brain-BZSXnwWU.js → Brain-CYHDXpPB.js} +1 -1
- package/static/app/assets/{BrainHome-CEBd86k6.js → BrainHome-bbJulb5g.js} +1 -1
- package/static/app/assets/{BrainSignals-O4s6R7av.js → BrainSignals-CesSPtUP.js} +1 -1
- package/static/app/assets/{Capture-AGbOxsg-.js → Capture-p5upXmz2.js} +1 -1
- package/static/app/assets/{CommandPalette-CeqBjCn2.js → CommandPalette-0zkyC0RY.js} +1 -1
- package/static/app/assets/{Library-CXnbNiQW.js → Library-Dt-vABI2.js} +1 -1
- package/static/app/assets/{LivingBrain-D9Iei2ZJ.js → LivingBrain-Bz-h6LBH.js} +1 -1
- package/static/app/assets/{ProductFlow-JlXiDfMM.js → ProductFlow-DAZ7DJCE.js} +1 -1
- package/static/app/assets/{ReviewCard-QQly2bTV.js → ReviewCard-Bv0WGbY6.js} +1 -1
- package/static/app/assets/{System-Bp9ZY4ER.js → System-BhrJQSD_.js} +1 -1
- package/static/app/assets/{activity-CxRPOQ2F.js → activity-AolpZJCH.js} +1 -1
- package/static/app/assets/{bot-1bcsWB2s.js → bot-CsVsT2nd.js} +1 -1
- package/static/app/assets/{brain-CxaeKCRM.js → brain-vuppco5y.js} +1 -1
- package/static/app/assets/{button-ZYFqGan7.js → button-1BP1PJZ3.js} +1 -1
- package/static/app/assets/{circle-pause-B5NOIqvk.js → circle-pause-5paeCOtq.js} +1 -1
- package/static/app/assets/{circle-play-BiJVFXf7.js → circle-play-By8mFYRV.js} +1 -1
- package/static/app/assets/{cpu-Vvq0OKTr.js → cpu-DvUQxdKe.js} +1 -1
- package/static/app/assets/{download-SEcXzkx1.js → download-CFqYG90a.js} +1 -1
- package/static/app/assets/{folder-open-Dw1ff_Qa.js → folder-open-CVSnp-Dp.js} +1 -1
- package/static/app/assets/{hard-drive-B00fR7_r.js → hard-drive-Ckr4Vd6-.js} +1 -1
- package/static/app/assets/{index-Dm9VXl1o.js → index--0zKF5Ow.js} +3 -3
- package/static/app/assets/{input-DtdWYQEa.js → input-D-JKquXU.js} +1 -1
- package/static/app/assets/{network-CpjBu0kI.js → network-DB88FZ2T.js} +1 -1
- package/static/app/assets/{primitives-CwsudBwc.js → primitives-D8U9OaIo.js} +1 -1
- package/static/app/assets/search-B8jsVTw_.js +1 -0
- package/static/app/assets/{shield-alert-CpjBc-gQ.js → shield-alert-DnsvO79P.js} +1 -1
- package/static/app/assets/{textarea-DUPyjHQs.js → textarea-4RchwbTa.js} +1 -1
- package/static/app/assets/{useFocusTrap-BWcMtCWH.js → useFocusTrap-CFJjy7cS.js} +1 -1
- package/static/app/assets/{useQuery-Dg5fPqTS.js → useQuery-3Reu-A4a.js} +1 -1
- package/static/app/assets/{users-BgcdTQGM.js → users-BTxYksxa.js} +1 -1
- package/static/app/assets/{utils-a3YXFtEO.js → utils-D02Vf-fw.js} +1 -1
- package/static/app/assets/{workspace-BMUEiXUm.js → workspace-CSJNgX-C.js} +1 -1
- package/static/app/index.html +3 -3
- package/static/sw.js +1 -1
- package/static/app/assets/search-CwEJWBEA.js +0 -1
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
# ruff: noqa: F403,F405
|
|
4
6
|
from ._kg_common import * # noqa: F403,F401
|
|
5
7
|
|
|
8
|
+
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
9
|
+
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
10
|
+
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._kg_contract import KnowledgeGraphCore as _Core
|
|
13
|
+
else:
|
|
14
|
+
_Core = object
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
# --- Compat seam (v9.9.5 decomposition) -------------------------------------
|
|
7
18
|
# The non-search read surface (list_documents / workspaces_of /
|
|
8
19
|
# filter_scoped_nodes / neighbors / get_node / relationship_search /
|
|
@@ -46,7 +57,7 @@ def context_quality_signal(
|
|
|
46
57
|
return {"mode": mode, "nodes": nodes, "limited": limited, "reason": reason}
|
|
47
58
|
|
|
48
59
|
|
|
49
|
-
class KnowledgeGraphRetrievalMixin:
|
|
60
|
+
class KnowledgeGraphRetrievalMixin(_Core):
|
|
50
61
|
_GRAPH_VISIBLE_TYPES = (
|
|
51
62
|
"Computer", # 내 컴퓨터
|
|
52
63
|
"Drive", # 드라이브 / 볼륨
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
# ruff: noqa: F403,F405
|
|
4
6
|
from ._kg_common import * # noqa: F403,F401
|
|
5
7
|
|
|
8
|
+
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
9
|
+
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
10
|
+
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._kg_contract import KnowledgeGraphCore as _Core
|
|
13
|
+
else:
|
|
14
|
+
_Core = object
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
|
|
7
|
-
class KnowledgeGraphDocGenMixin:
|
|
18
|
+
class KnowledgeGraphDocGenMixin(_Core):
|
|
8
19
|
"""Multi-hop retrieval specialised for document generation, split out
|
|
9
20
|
of retrieval. Composed into KnowledgeGraphStore alongside the other
|
|
10
21
|
retrieval mixins; shared instance means sibling helpers resolve via MRO.
|
|
@@ -11,11 +11,22 @@ calls (e.g. ``search`` → ``filter_scoped_nodes``) behave exactly as before.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
14
16
|
# ruff: noqa: F403,F405
|
|
15
17
|
from ._kg_common import * # noqa: F403,F401
|
|
16
18
|
|
|
19
|
+
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
20
|
+
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
21
|
+
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ._kg_contract import KnowledgeGraphCore as _Core
|
|
24
|
+
else:
|
|
25
|
+
_Core = object
|
|
26
|
+
|
|
27
|
+
|
|
17
28
|
|
|
18
|
-
class KnowledgeGraphReadsMixin:
|
|
29
|
+
class KnowledgeGraphReadsMixin(_Core):
|
|
19
30
|
def list_documents(self, limit: int = 200) -> Dict[str, Any]:
|
|
20
31
|
"""List ingested ``Document`` nodes with their ingest + index state.
|
|
21
32
|
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
# ruff: noqa: F403,F405
|
|
4
6
|
from ._kg_common import * # noqa: F403,F401
|
|
5
7
|
|
|
8
|
+
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
9
|
+
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
10
|
+
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._kg_contract import KnowledgeGraphCore as _Core
|
|
13
|
+
else:
|
|
14
|
+
_Core = object
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
|
|
7
|
-
class KnowledgeGraphVectorMixin:
|
|
18
|
+
class KnowledgeGraphVectorMixin(_Core):
|
|
8
19
|
"""Vector-embedding index build/status/search, split out of retrieval.
|
|
9
20
|
|
|
10
21
|
Composed into KnowledgeGraphStore alongside KnowledgeGraphRetrievalMixin;
|
|
@@ -48,6 +48,7 @@ import os
|
|
|
48
48
|
import sqlite3
|
|
49
49
|
from contextlib import contextmanager
|
|
50
50
|
from enum import Enum
|
|
51
|
+
from pathlib import Path
|
|
51
52
|
from typing import Any, Dict, Optional
|
|
52
53
|
|
|
53
54
|
from ..quiet import quiet
|
|
@@ -423,8 +424,11 @@ class KGStoreV2:
|
|
|
423
424
|
프로젝션이 담당하므로 native upsert/get/search API 는 두지 않는다.
|
|
424
425
|
"""
|
|
425
426
|
|
|
426
|
-
def __init__(self, db_path: str):
|
|
427
|
-
|
|
427
|
+
def __init__(self, db_path: "str | Path"):
|
|
428
|
+
# Callers pass a Path (the store keeps db_path as a Path); sqlite3
|
|
429
|
+
# accepts either, so the annotation now matches what is actually
|
|
430
|
+
# handed in instead of forcing every call site to str() it.
|
|
431
|
+
self.db_path = str(db_path)
|
|
428
432
|
|
|
429
433
|
@contextmanager
|
|
430
434
|
def _conn(self):
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
3
5
|
# ruff: noqa: F403,F405
|
|
4
6
|
from ._kg_common import * # noqa: F403,F401
|
|
5
7
|
|
|
8
|
+
# The cross-mixin surface (`_connect`, `_upsert_node`, …) is declared in
|
|
9
|
+
# `_kg_contract.KnowledgeGraphCore`. It is a typing-only base: at runtime this
|
|
10
|
+
# is `object`, so the MRO of `KnowledgeGraphStore` is unchanged.
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._kg_contract import KnowledgeGraphCore as _Core
|
|
13
|
+
else:
|
|
14
|
+
_Core = object
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
|
|
7
|
-
class KnowledgeGraphWriteMixin:
|
|
18
|
+
class KnowledgeGraphWriteMixin(_Core):
|
|
8
19
|
def _upsert_node(
|
|
9
20
|
self,
|
|
10
21
|
conn: sqlite3.Connection,
|
|
@@ -408,7 +408,7 @@ class IngestionResult:
|
|
|
408
408
|
quality_gate: Optional[Dict[str, Any]] = None
|
|
409
409
|
|
|
410
410
|
def as_dict(self) -> Dict[str, Any]:
|
|
411
|
-
payload = {
|
|
411
|
+
payload: Dict[str, Any] = {
|
|
412
412
|
"status": self.status,
|
|
413
413
|
"source_type": self.source_type,
|
|
414
414
|
"node_id": self.node_id,
|
package/lattice_brain/quality.py
CHANGED
|
@@ -268,7 +268,7 @@ class MemoryQualityManager:
|
|
|
268
268
|
|
|
269
269
|
def merge(self, cands: List[MemoryCandidate]) -> List[MemoryCandidate]:
|
|
270
270
|
# naive merge by content prefix
|
|
271
|
-
merged = {}
|
|
271
|
+
merged: Dict[str, MemoryCandidate] = {}
|
|
272
272
|
for c in cands:
|
|
273
273
|
key = c.content[:30]
|
|
274
274
|
if key not in merged or c.score > merged[key].score:
|
|
@@ -365,19 +365,19 @@ class GraphEdgeQualityManager:
|
|
|
365
365
|
return GraphEdgeQuality(edge.get("id", "e0"), conf, ev, quality_score=q)
|
|
366
366
|
|
|
367
367
|
def detect_duplicate_edges(self, edges: List[Dict]) -> List[str]:
|
|
368
|
-
seen = {}
|
|
369
|
-
dups = []
|
|
368
|
+
seen: Dict[Any, Any] = {}
|
|
369
|
+
dups: List[str] = []
|
|
370
370
|
for e in edges:
|
|
371
371
|
key = (e.get("source"), e.get("target"), e.get("type"))
|
|
372
372
|
if key in seen:
|
|
373
|
-
dups.append(e.get("id"))
|
|
373
|
+
dups.append(str(e.get("id") or ""))
|
|
374
374
|
else:
|
|
375
375
|
seen[key] = e.get("id")
|
|
376
376
|
return dups
|
|
377
377
|
|
|
378
378
|
def merge_duplicate_edges(self, edges: List[Dict]) -> List[Dict]:
|
|
379
379
|
# keep highest confidence
|
|
380
|
-
best = {}
|
|
380
|
+
best: Dict[Any, Dict[str, Any]] = {}
|
|
381
381
|
for e in edges:
|
|
382
382
|
key = (e.get("source"), e.get("target"), e.get("type"))
|
|
383
383
|
if key not in best or e.get("confidence", 0) > best[key].get("confidence", 0):
|
|
@@ -435,7 +435,8 @@ def realtime_event_contract(event: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
435
435
|
"""
|
|
436
436
|
body = dict(event or {})
|
|
437
437
|
seq = body.get("seq")
|
|
438
|
-
|
|
438
|
+
raw_payload = body.get("payload")
|
|
439
|
+
payload: Dict[str, Any] = raw_payload if isinstance(raw_payload, dict) else {}
|
|
439
440
|
contract_body = {
|
|
440
441
|
"run_id": payload.get("run_id") or body.get("run_id"),
|
|
441
442
|
"agent_id": str(payload.get("agent_id") or payload.get("workflow_id") or f"realtime:{body.get('area') or 'workspace'}"),
|
|
@@ -47,7 +47,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
47
47
|
from ..utils import now_iso as _now
|
|
48
48
|
from .contracts import multi_agent_contract
|
|
49
49
|
|
|
50
|
-
MULTI_AGENT_VERSION = "10.
|
|
50
|
+
MULTI_AGENT_VERSION = "10.4.0"
|
|
51
51
|
|
|
52
52
|
AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
|
|
53
53
|
CORE_PIPELINE = ("planner", "executor", "reviewer")
|
|
@@ -239,7 +239,10 @@ class ApprovalRequired(Exception):
|
|
|
239
239
|
permission: Optional[Dict[str, Any]] = None):
|
|
240
240
|
super().__init__(message)
|
|
241
241
|
self.tool = tool
|
|
242
|
-
|
|
242
|
+
# Deliberately shadows BaseException.args: callers read
|
|
243
|
+
# `exc.args` as the failing tool's argument mapping, not the
|
|
244
|
+
# exception constructor tuple.
|
|
245
|
+
self.args = args or {} # type: ignore[assignment]
|
|
243
246
|
self.permission = permission or {}
|
|
244
247
|
|
|
245
248
|
|
|
@@ -374,7 +377,7 @@ class WorkflowEngine:
|
|
|
374
377
|
while current is not None and steps < _MAX_STEPS:
|
|
375
378
|
steps += 1
|
|
376
379
|
ntype = current.get("type")
|
|
377
|
-
nid = current.get("id")
|
|
380
|
+
nid = str(current.get("id") or "")
|
|
378
381
|
entry: Dict[str, Any] = {
|
|
379
382
|
"node": nid,
|
|
380
383
|
"type": ntype,
|
|
@@ -386,7 +389,7 @@ class WorkflowEngine:
|
|
|
386
389
|
entry["status"] = "ok"
|
|
387
390
|
entry["trigger"] = (current.get("config") or {}).get("trigger", "manual")
|
|
388
391
|
run.timeline.append(entry)
|
|
389
|
-
current = nodes.get(current.get("next")) if current.get("next") else None
|
|
392
|
+
current = nodes.get(str(current.get("next") or "")) if current.get("next") else None
|
|
390
393
|
continue
|
|
391
394
|
|
|
392
395
|
if ntype == "output":
|
|
@@ -395,7 +398,7 @@ class WorkflowEngine:
|
|
|
395
398
|
entry["output"] = payload if payload is not None else context.get("last_output")
|
|
396
399
|
run.outputs[nid] = entry["output"]
|
|
397
400
|
run.timeline.append(entry)
|
|
398
|
-
current = nodes.get(current.get("next")) if current.get("next") else None
|
|
401
|
+
current = nodes.get(str(current.get("next") or "")) if current.get("next") else None
|
|
399
402
|
continue
|
|
400
403
|
|
|
401
404
|
if ntype == "condition":
|
|
@@ -409,14 +412,14 @@ class WorkflowEngine:
|
|
|
409
412
|
continue
|
|
410
413
|
|
|
411
414
|
# Executable node → dispatch to its runner family.
|
|
412
|
-
family = _RUNNER_FOR.get(ntype)
|
|
415
|
+
family = _RUNNER_FOR.get(str(ntype or ""))
|
|
413
416
|
runner = self.runners.get(family) if family else None
|
|
414
417
|
if runner is None:
|
|
415
418
|
entry["status"] = "skipped"
|
|
416
419
|
entry["reason"] = f"no '{family}' runner configured"
|
|
417
420
|
had_skip = True
|
|
418
421
|
run.timeline.append(entry)
|
|
419
|
-
current = nodes.get(current.get("next")) if current.get("next") else None
|
|
422
|
+
current = nodes.get(str(current.get("next") or "")) if current.get("next") else None
|
|
420
423
|
continue
|
|
421
424
|
try:
|
|
422
425
|
result = runner(node=current, context=context)
|
|
@@ -451,7 +454,7 @@ class WorkflowEngine:
|
|
|
451
454
|
entry["reason"] = str(exc)
|
|
452
455
|
had_error = True
|
|
453
456
|
run.timeline.append(entry)
|
|
454
|
-
current = nodes.get(current.get("next")) if current.get("next") else None
|
|
457
|
+
current = nodes.get(str(current.get("next") or "")) if current.get("next") else None
|
|
455
458
|
|
|
456
459
|
if steps >= _MAX_STEPS:
|
|
457
460
|
run.timeline.append({"node": None, "type": "guard", "status": "error", "reason": f"exceeded {_MAX_STEPS} steps (cycle?)", "timestamp": _now()})
|
package/latticeai/__init__.py
CHANGED
package/latticeai/api/admin.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
|
-
from typing import Callable, Dict, List, Optional
|
|
6
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
7
7
|
|
|
8
8
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
9
9
|
from pydantic import BaseModel
|
|
@@ -226,7 +226,7 @@ def create_admin_router(
|
|
|
226
226
|
for email, user in users.items(): # noqa: B007 — the key is the payload; the loop var documents the shape
|
|
227
227
|
role = (get_user_role(email, users) or "user").lower()
|
|
228
228
|
counts[role] += 1
|
|
229
|
-
matrix = policy_matrix() if policy_matrix else [
|
|
229
|
+
matrix: List[Dict[str, Any]] = policy_matrix() if policy_matrix else [
|
|
230
230
|
{"role": "admin", "caps": ["all"]},
|
|
231
231
|
{"role": "user", "caps": ["chat", "search"]},
|
|
232
232
|
]
|
package/latticeai/api/chat.py
CHANGED
|
@@ -94,16 +94,20 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
94
94
|
api_router = APIRouter()
|
|
95
95
|
model_router = context.model_router
|
|
96
96
|
config = context.config
|
|
97
|
-
require_user = context.require_user
|
|
97
|
+
require_user = context.require("require_user")
|
|
98
98
|
workspace_service = context.workspace_service
|
|
99
99
|
allowed_workspaces_for = context.allowed_workspaces_for
|
|
100
|
+
# Bound once so every handler below calls a resolved dependency rather
|
|
101
|
+
# than an Optional field (see AppContext.require).
|
|
102
|
+
enforce_rate_limit = context.require("enforce_rate_limit")
|
|
103
|
+
append_audit_event = context.require("append_audit_event")
|
|
100
104
|
|
|
101
105
|
chat_service = ChatService.coerce(
|
|
102
106
|
context.chat_service,
|
|
103
107
|
store=context.workspace_store,
|
|
104
|
-
get_history=context.get_history,
|
|
105
|
-
save_to_history=context.save_to_history,
|
|
106
|
-
get_history_user=context.get_history_user,
|
|
108
|
+
get_history=context.require("get_history"),
|
|
109
|
+
save_to_history=context.require("save_to_history"),
|
|
110
|
+
get_history_user=context.require("get_history_user"),
|
|
107
111
|
)
|
|
108
112
|
|
|
109
113
|
def notify_chat_message(role: str, text: str, source: Optional[str]) -> None:
|
|
@@ -170,9 +174,9 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
170
174
|
model_router=model_router,
|
|
171
175
|
execute_tool=execute_tool,
|
|
172
176
|
recent_chat_context=recent_chat_context,
|
|
173
|
-
clear_history=context.clear_history,
|
|
177
|
+
clear_history=context.require("clear_history"),
|
|
174
178
|
knowledge_save=knowledge_save,
|
|
175
|
-
audit=context.append_audit_event,
|
|
179
|
+
audit=context.require("append_audit_event"),
|
|
176
180
|
hooks=context.hooks,
|
|
177
181
|
brain_memory=context.brain_memory,
|
|
178
182
|
)
|
|
@@ -247,7 +251,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
247
251
|
@api_router.post("/chat")
|
|
248
252
|
async def chat(req: ChatRequest, request: Request):
|
|
249
253
|
current_user = require_user(request)
|
|
250
|
-
|
|
254
|
+
enforce_rate_limit(current_user, "chat")
|
|
251
255
|
logging.debug(
|
|
252
256
|
"/chat request: stream=%s image_data_len=%s message_len=%s",
|
|
253
257
|
req.stream,
|
|
@@ -352,7 +356,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
352
356
|
for match in file_path_pattern.finditer(req.message or "")
|
|
353
357
|
]
|
|
354
358
|
if requested_paths:
|
|
355
|
-
|
|
359
|
+
append_audit_event(
|
|
356
360
|
"auto_file_context_blocked",
|
|
357
361
|
user_email=effective_email,
|
|
358
362
|
path_count=len(requested_paths),
|
|
@@ -393,7 +397,7 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
393
397
|
if isinstance(trace_seed, dict):
|
|
394
398
|
trace_seed["context_quality"] = context_quality
|
|
395
399
|
if context.funnel_metrics is not None and int(
|
|
396
|
-
context_quality.get("nodes") or 0
|
|
400
|
+
str(context_quality.get("nodes") or 0)
|
|
397
401
|
) > 0:
|
|
398
402
|
context.funnel_metrics.record_recall_success()
|
|
399
403
|
|
|
@@ -542,12 +546,12 @@ def create_chat_router(context: AppContext) -> APIRouter:
|
|
|
542
546
|
chat_service=chat_service,
|
|
543
547
|
require_user=require_user,
|
|
544
548
|
scope_for_user=history_scope_for_user,
|
|
545
|
-
group_conversations=context.group_history_conversations,
|
|
546
|
-
get_conversation_messages=context.get_conversation_messages,
|
|
547
|
-
conversation_title=context.conversation_title,
|
|
548
|
-
clear_conversation=context.clear_conversation,
|
|
549
|
-
clear_history=context.clear_history,
|
|
550
|
-
append_audit_event=context.append_audit_event,
|
|
549
|
+
group_conversations=context.require("group_history_conversations"),
|
|
550
|
+
get_conversation_messages=context.require("get_conversation_messages"),
|
|
551
|
+
conversation_title=context.require("conversation_title"),
|
|
552
|
+
clear_conversation=context.require("clear_conversation"),
|
|
553
|
+
clear_history=context.require("clear_history"),
|
|
554
|
+
append_audit_event=context.require("append_audit_event"),
|
|
551
555
|
),
|
|
552
556
|
)
|
|
553
557
|
agent_controller.register_routes(api_router)
|
|
@@ -11,7 +11,7 @@ import threading
|
|
|
11
11
|
import time
|
|
12
12
|
from datetime import datetime, timedelta, timezone
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Dict, Optional
|
|
14
|
+
from typing import Any, Callable, Dict, Optional
|
|
15
15
|
|
|
16
16
|
from fastapi import APIRouter, HTTPException, Request
|
|
17
17
|
from fastapi.responses import StreamingResponse
|
|
@@ -31,6 +31,20 @@ from latticeai.api.chat_stream import agent_live_stream
|
|
|
31
31
|
from latticeai.core.agent import AgentRunContext, AgentState, normalize_plan
|
|
32
32
|
from latticeai.core.quiet import quiet
|
|
33
33
|
from latticeai.core.run_explain import explain_run
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _bind_case(
|
|
37
|
+
execute_tool: Callable[[str, Dict[str, Any]], Any],
|
|
38
|
+
action_name: str,
|
|
39
|
+
case_input: Dict[str, Any],
|
|
40
|
+
) -> Callable[[], Any]:
|
|
41
|
+
"""Freeze this iteration's action and input into a zero-arg thunk.
|
|
42
|
+
|
|
43
|
+
Previously a lambda with default-argument binding, which is correct but
|
|
44
|
+
invisible to a type checker (`Cannot infer type of lambda`) and one
|
|
45
|
+
refactor away from every case running the last input.
|
|
46
|
+
"""
|
|
47
|
+
return lambda: execute_tool(action_name, case_input)
|
|
34
48
|
from latticeai.core.run_store import (
|
|
35
49
|
AgentRunStore,
|
|
36
50
|
hash_approval_token,
|
|
@@ -200,9 +214,7 @@ class AgentHTTPController:
|
|
|
200
214
|
# Bound as defaults: the lambda is invoked inside this
|
|
201
215
|
# iteration today, so late binding is harmless — until
|
|
202
216
|
# someone defers it and every case runs the last input.
|
|
203
|
-
|
|
204
|
-
action_name, case_input
|
|
205
|
-
),
|
|
217
|
+
_bind_case(self.execute_tool, action_name, case_input),
|
|
206
218
|
source="eval",
|
|
207
219
|
)
|
|
208
220
|
criteria = case.get("pass_criteria", "")
|
|
@@ -509,7 +521,7 @@ class AgentHTTPController:
|
|
|
509
521
|
}
|
|
510
522
|
try:
|
|
511
523
|
for summary in self.run_store.pending_summaries(current_user):
|
|
512
|
-
run_id = summary.get("run_id")
|
|
524
|
+
run_id = str(summary.get("run_id") or "")
|
|
513
525
|
if run_id and run_id not in pending:
|
|
514
526
|
pending[run_id] = {
|
|
515
527
|
"run_id": run_id,
|
|
@@ -801,7 +813,7 @@ class AgentHTTPController:
|
|
|
801
813
|
detail="Agent run belongs to another user.",
|
|
802
814
|
)
|
|
803
815
|
if now_monotonic >= entry["expires_monotonic"]:
|
|
804
|
-
self._approvals.pop(req.run_id, None)
|
|
816
|
+
self._approvals.pop(req.run_id or "", None)
|
|
805
817
|
try:
|
|
806
818
|
self.run_store.delete(req.run_id or "")
|
|
807
819
|
except Exception: # noqa: BLE001
|
|
@@ -820,7 +832,7 @@ class AgentHTTPController:
|
|
|
820
832
|
detail="Invalid approval token for this run.",
|
|
821
833
|
)
|
|
822
834
|
# Token validated — the pending run is consumed either way.
|
|
823
|
-
self._approvals.pop(req.run_id, None)
|
|
835
|
+
self._approvals.pop(req.run_id or "", None)
|
|
824
836
|
try:
|
|
825
837
|
self.run_store.delete(req.run_id or "")
|
|
826
838
|
except Exception as exc: # noqa: BLE001 — consumption must proceed
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import re
|
|
12
|
-
from typing import AsyncIterator, Dict, List, Optional
|
|
12
|
+
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
13
13
|
|
|
14
14
|
from fastapi import Request
|
|
15
15
|
|
|
@@ -289,17 +289,18 @@ def assess_answer_grounding(
|
|
|
289
289
|
them". Annotation only — the answer is never modified or blocked.
|
|
290
290
|
"""
|
|
291
291
|
answer_text = str(answer or "").strip()
|
|
292
|
-
|
|
293
|
-
sources: List[Dict[str,
|
|
292
|
+
trace_map: Dict[str, Any] = trace if isinstance(trace, dict) else {}
|
|
293
|
+
sources: List[Dict[str, Any]] = []
|
|
294
294
|
seen_ids = set()
|
|
295
|
-
for node in list(
|
|
295
|
+
for node in list(trace_map.get("graph_nodes") or []):
|
|
296
296
|
if not isinstance(node, dict):
|
|
297
297
|
continue
|
|
298
298
|
node_id = str(node.get("id") or node.get("node_id") or "")
|
|
299
299
|
if not node_id or node_id in seen_ids:
|
|
300
300
|
continue
|
|
301
301
|
seen_ids.add(node_id)
|
|
302
|
-
|
|
302
|
+
raw_meta = node.get("metadata")
|
|
303
|
+
meta: Dict[str, Any] = raw_meta if isinstance(raw_meta, dict) else {}
|
|
303
304
|
sources.append({
|
|
304
305
|
"id": node_id,
|
|
305
306
|
"title": str(node.get("title") or ""),
|
|
@@ -308,7 +309,7 @@ def assess_answer_grounding(
|
|
|
308
309
|
for part in (node.get("title"), node.get("summary"), meta.get("filename"))
|
|
309
310
|
),
|
|
310
311
|
})
|
|
311
|
-
for src in list(
|
|
312
|
+
for src in list(trace_map.get("source_files") or []):
|
|
312
313
|
if not isinstance(src, dict):
|
|
313
314
|
continue
|
|
314
315
|
node_id = str(src.get("node_id") or src.get("source") or "")
|
|
@@ -349,10 +350,10 @@ def assess_answer_grounding(
|
|
|
349
350
|
|
|
350
351
|
answer_tokens = _grounding_tokens(answer_text)
|
|
351
352
|
answer_lower = answer_text.lower()
|
|
352
|
-
cited: List[Dict[str,
|
|
353
|
+
cited: List[Dict[str, Any]] = []
|
|
353
354
|
best_overlap = 0.0
|
|
354
355
|
for source in sources:
|
|
355
|
-
source_tokens = _grounding_tokens(source["body"])
|
|
356
|
+
source_tokens = _grounding_tokens(str(source["body"]))
|
|
356
357
|
if not source_tokens:
|
|
357
358
|
continue
|
|
358
359
|
shared = answer_tokens & source_tokens
|
|
@@ -57,13 +57,13 @@ def agent_live_stream(
|
|
|
57
57
|
|
|
58
58
|
async def _stream() -> AsyncIterator[str]:
|
|
59
59
|
response_model = model_id or router.current_model_id
|
|
60
|
-
queue: asyncio.Queue = asyncio.Queue()
|
|
60
|
+
queue: "asyncio.Queue[Dict[str, Any]]" = asyncio.Queue()
|
|
61
61
|
|
|
62
62
|
def observer(event: Dict[str, Any]) -> None:
|
|
63
63
|
# Called synchronously from the agent coroutine on this loop.
|
|
64
64
|
queue.put_nowait(event)
|
|
65
65
|
|
|
66
|
-
task = asyncio.
|
|
66
|
+
task = asyncio.ensure_future(start(observer))
|
|
67
67
|
try:
|
|
68
68
|
while True:
|
|
69
69
|
getter = asyncio.create_task(queue.get())
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import re
|
|
7
|
-
from typing import Optional
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
|
|
9
9
|
from fastapi import APIRouter, HTTPException, Request
|
|
10
10
|
from fastapi.responses import StreamingResponse
|
|
@@ -401,7 +401,7 @@ def create_computer_use_router(
|
|
|
401
401
|
yield _send("error", {"error": "No model loaded."})
|
|
402
402
|
return
|
|
403
403
|
|
|
404
|
-
transcript = []
|
|
404
|
+
transcript: List[Dict[str, Any]] = []
|
|
405
405
|
last_screenshot_b64: Optional[str] = None
|
|
406
406
|
max_steps = max(1, min(req.max_steps, 20))
|
|
407
407
|
yield _send("start", {"task": req.task, "max_steps": max_steps})
|
package/latticeai/api/hooks.py
CHANGED
|
@@ -8,7 +8,7 @@ the API.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
-
from typing import Callable, List, Optional
|
|
11
|
+
from typing import Any, Callable, List, Optional
|
|
12
12
|
|
|
13
13
|
from fastapi import APIRouter, HTTPException, Request
|
|
14
14
|
from pydantic import BaseModel
|
|
@@ -46,7 +46,9 @@ def create_hooks_router(
|
|
|
46
46
|
*,
|
|
47
47
|
registry: HooksRegistry,
|
|
48
48
|
require_user: Callable[[Request], str],
|
|
49
|
-
|
|
49
|
+
# Production returns ``(email, users)``; small adapters return just the
|
|
50
|
+
# identity, which ``_require_admin_email`` below handles.
|
|
51
|
+
require_admin: Callable[[Request], Any],
|
|
50
52
|
append_audit_event: Callable[..., None],
|
|
51
53
|
) -> APIRouter:
|
|
52
54
|
router = APIRouter()
|
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
import uuid
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Optional
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
12
|
|
|
13
13
|
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
|
14
14
|
from fastapi.responses import FileResponse
|
|
@@ -132,11 +132,15 @@ def create_local_files_router(
|
|
|
132
132
|
errors.append(f"graph: {exc}")
|
|
133
133
|
|
|
134
134
|
# ── watcher + connected sources (real) ────────────────────────────────
|
|
135
|
-
watch
|
|
136
|
-
|
|
135
|
+
watch: Dict[str, Any] = (
|
|
136
|
+
local_kg_watcher.status()
|
|
137
|
+
if local_kg_watcher
|
|
138
|
+
else {"available": False, "active": {}}
|
|
139
|
+
)
|
|
140
|
+
sources: List[Any] = []
|
|
137
141
|
try:
|
|
138
142
|
if knowledge_graph is not None:
|
|
139
|
-
sources = (knowledge_graph.local_sources() or {}).get("sources"
|
|
143
|
+
sources = list((knowledge_graph.local_sources() or {}).get("sources") or [])
|
|
140
144
|
except Exception as exc:
|
|
141
145
|
errors.append(f"sources: {exc}")
|
|
142
146
|
watched = len(watch.get("active", {}) or {})
|
package/latticeai/api/mcp.py
CHANGED
|
@@ -360,7 +360,7 @@ def create_mcp_router(
|
|
|
360
360
|
require_user(request)
|
|
361
361
|
mcp_registry._SKILLS_MARKETPLACE_FETCHED_AT = None
|
|
362
362
|
skills = await _fetch_skills_marketplace()
|
|
363
|
-
by_author = {}
|
|
363
|
+
by_author: Dict[str, int] = {}
|
|
364
364
|
for s in skills:
|
|
365
365
|
by_author[s["author"]] = by_author.get(s["author"], 0) + 1
|
|
366
366
|
return {"status": "ok", "total": len(skills), "by_author": by_author}
|
|
@@ -399,7 +399,7 @@ def create_mcp_router(
|
|
|
399
399
|
require_user(request)
|
|
400
400
|
mcp_registry._PLUGIN_DIRECTORY_FETCHED_AT = None
|
|
401
401
|
plugins = await _fetch_plugin_directory()
|
|
402
|
-
by_license = {}
|
|
402
|
+
by_license: Dict[str, int] = {}
|
|
403
403
|
for p in plugins:
|
|
404
404
|
lic = p.get("license", "unknown")
|
|
405
405
|
by_license[lic] = by_license.get(lic, 0) + 1
|
package/latticeai/api/models.py
CHANGED
|
@@ -155,25 +155,25 @@ def create_models_router(
|
|
|
155
155
|
|
|
156
156
|
def _recommended_with_engine_options(
|
|
157
157
|
items: List[Dict[str, object]],
|
|
158
|
-
engines: Optional[List[Dict[str,
|
|
158
|
+
engines: Optional[List[Dict[str, Any]]] = None,
|
|
159
159
|
loaded_ids: Optional[List[str]] = None,
|
|
160
160
|
current_id: Optional[str] = None,
|
|
161
161
|
) -> List[Dict[str, object]]:
|
|
162
162
|
from latticeai.core.model_compat import model_runtime_compatibility
|
|
163
163
|
|
|
164
164
|
engine_lookup = {str(engine.get("id") or ""): engine for engine in engines or []}
|
|
165
|
-
model_lookup: Dict[str, Dict[str,
|
|
165
|
+
model_lookup: Dict[str, Dict[str, Any]] = {}
|
|
166
166
|
for engine in engines or []:
|
|
167
167
|
engine_id = str(engine.get("id") or "")
|
|
168
|
-
for model in engine.get("models") or []:
|
|
168
|
+
for model in list(engine.get("models") or []):
|
|
169
169
|
if isinstance(model, dict):
|
|
170
170
|
model_lookup[str(model.get("id") or "")] = {**model, "_engine": engine_id}
|
|
171
171
|
loaded = set(loaded_ids or [])
|
|
172
|
-
out: List[Dict[str,
|
|
172
|
+
out: List[Dict[str, Any]] = []
|
|
173
173
|
for item in items:
|
|
174
174
|
short_id = str(item["id"]).lower()
|
|
175
175
|
aliases = MODEL_ENGINE_ALIASES.get(short_id) or {}
|
|
176
|
-
options: List[Dict[str,
|
|
176
|
+
options: List[Dict[str, Any]] = []
|
|
177
177
|
for engine_name in ("local_mlx", "ollama", "lmstudio", "llamacpp", "vllm"):
|
|
178
178
|
real = aliases.get(engine_name)
|
|
179
179
|
if not real:
|