ltcai 9.7.0 → 9.8.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 +98 -316
- package/docs/CHANGELOG.md +37 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- 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/graph/retrieval.py +93 -4
- package/lattice_brain/graph/retrieval_vector.py +43 -0
- package/lattice_brain/ingestion.py +399 -14
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +12 -0
- package/latticeai/api/chat.py +17 -0
- package/latticeai/api/chat_helpers.py +48 -0
- package/latticeai/api/chat_stream.py +9 -1
- package/latticeai/api/local_files.py +120 -2
- package/latticeai/core/agent_eval.py +123 -0
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/automation_intelligence.py +151 -14
- package/latticeai/services/brain_intelligence.py +72 -0
- package/latticeai/services/product_readiness.py +1 -1
- 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 +11 -11
- package/static/app/assets/{Act-B6c39ays.js → Act-Dd3z8AzF.js} +2 -2
- package/static/app/assets/Brain-BMkgdWnI.js +321 -0
- package/static/app/assets/Capture-D2Aw9gkv.js +1 -0
- package/static/app/assets/Library-Yreq-KW5.js +1 -0
- package/static/app/assets/System-CXNmmtEo.js +1 -0
- package/static/app/assets/{index-85wQvEie.css → index-7gY9t9Sd.css} +1 -1
- package/static/app/assets/index-CndfILiF.js +18 -0
- package/static/app/assets/primitives-DxsIXb6G.js +1 -0
- package/static/app/assets/textarea-DH7ne8VI.js +1 -0
- package/static/app/index.html +2 -2
- package/static/sw.js +1 -1
- package/static/app/assets/Brain-D7Qg4k6M.js +0 -321
- package/static/app/assets/Capture-VF_di68r.js +0 -1
- package/static/app/assets/Library-D_Gis2PA.js +0 -1
- package/static/app/assets/System-C5s5H2ov.js +0 -1
- package/static/app/assets/index-DJC_2oub.js +0 -18
- package/static/app/assets/primitives-DL4Nip8C.js +0 -1
- package/static/app/assets/textarea-woZfCXHy.js +0 -1
|
@@ -5,6 +5,40 @@ from __future__ import annotations
|
|
|
5
5
|
from ._kg_common import * # noqa: F403,F401
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
def context_quality_signal(
|
|
9
|
+
mode: str,
|
|
10
|
+
nodes: int,
|
|
11
|
+
*,
|
|
12
|
+
reason: Optional[str] = None,
|
|
13
|
+
) -> Dict[str, Any]:
|
|
14
|
+
"""Honest RAG context-quality signal (v9.8.0, additive contract).
|
|
15
|
+
|
|
16
|
+
Shape consumed by the chat metadata channel:
|
|
17
|
+
``{"mode": "hybrid"|"lexical_only"|"none", "nodes": int, "limited": bool,
|
|
18
|
+
"reason": str|None}``. ``nodes == 0`` always collapses ``mode`` to
|
|
19
|
+
``"none"``; ``limited`` is true whenever the context is thin (0–1 nodes)
|
|
20
|
+
or the vector side fell back to lexical-only retrieval. ``reason`` is a
|
|
21
|
+
short human-readable Korean phrase, only present when limited.
|
|
22
|
+
"""
|
|
23
|
+
nodes = max(0, int(nodes or 0))
|
|
24
|
+
mode = str(mode or "none")
|
|
25
|
+
if nodes == 0:
|
|
26
|
+
mode = "none"
|
|
27
|
+
if mode not in ("hybrid", "lexical_only", "none"):
|
|
28
|
+
mode = "lexical_only"
|
|
29
|
+
limited = nodes <= 1 or mode != "hybrid"
|
|
30
|
+
if reason is None and limited:
|
|
31
|
+
if nodes == 0:
|
|
32
|
+
reason = "그래프에서 관련 지식을 찾지 못했습니다"
|
|
33
|
+
elif mode == "lexical_only":
|
|
34
|
+
reason = "벡터 검색을 사용할 수 없어 키워드 검색 결과만 사용했습니다"
|
|
35
|
+
else:
|
|
36
|
+
reason = "그래프 기반 컨텍스트가 제한적입니다"
|
|
37
|
+
if not limited:
|
|
38
|
+
reason = None
|
|
39
|
+
return {"mode": mode, "nodes": nodes, "limited": limited, "reason": reason}
|
|
40
|
+
|
|
41
|
+
|
|
8
42
|
class KnowledgeGraphRetrievalMixin:
|
|
9
43
|
_GRAPH_VISIBLE_TYPES = (
|
|
10
44
|
"Computer", # 내 컴퓨터
|
|
@@ -604,26 +638,45 @@ class KnowledgeGraphRetrievalMixin:
|
|
|
604
638
|
allowed_workspaces=None,
|
|
605
639
|
include_legacy_global: bool = False,
|
|
606
640
|
use_hybrid: bool = False,
|
|
607
|
-
|
|
641
|
+
with_meta: bool = False,
|
|
642
|
+
):
|
|
608
643
|
"""Return compact graph-backed RAG context for chat generation.
|
|
609
644
|
|
|
610
645
|
``use_hybrid=True`` sources the matches from :meth:`hybrid_search`
|
|
611
646
|
(lexical + vector fusion) instead of the lexical-only :meth:`search`.
|
|
612
647
|
Default behavior is unchanged, and any hybrid failure silently falls
|
|
613
648
|
back to the legacy lexical path.
|
|
649
|
+
|
|
650
|
+
``with_meta=True`` (additive, v9.8.0) returns
|
|
651
|
+
``{"context": str, "quality": {...}}`` instead of the bare string.
|
|
652
|
+
``quality`` follows :func:`context_quality_signal` and honestly
|
|
653
|
+
reports how the context was retrieved (hybrid vs lexical-only
|
|
654
|
+
fallback vs nothing). The ``context`` value is byte-identical to the
|
|
655
|
+
default ``with_meta=False`` return for the same arguments.
|
|
614
656
|
"""
|
|
615
657
|
query = str(query or "").strip()
|
|
616
658
|
if not query:
|
|
659
|
+
if with_meta:
|
|
660
|
+
return {
|
|
661
|
+
"context": "",
|
|
662
|
+
"quality": context_quality_signal(
|
|
663
|
+
"none", 0, reason="질의가 비어 있습니다"
|
|
664
|
+
),
|
|
665
|
+
}
|
|
617
666
|
return ""
|
|
618
667
|
matches: List[Dict[str, Any]] = []
|
|
668
|
+
retrieval_mode = "none"
|
|
619
669
|
if use_hybrid:
|
|
620
670
|
try:
|
|
621
|
-
|
|
671
|
+
hybrid = self.hybrid_search(
|
|
622
672
|
query,
|
|
623
673
|
top_k=limit,
|
|
624
674
|
allowed_workspaces=allowed_workspaces,
|
|
625
675
|
include_legacy_global=include_legacy_global,
|
|
626
|
-
)
|
|
676
|
+
)
|
|
677
|
+
matches = hybrid.get("matches", [])
|
|
678
|
+
if matches:
|
|
679
|
+
retrieval_mode = str(hybrid.get("mode") or "hybrid")
|
|
627
680
|
except Exception: # noqa: BLE001 — context building must never fail
|
|
628
681
|
matches = []
|
|
629
682
|
if not matches:
|
|
@@ -633,6 +686,8 @@ class KnowledgeGraphRetrievalMixin:
|
|
|
633
686
|
allowed_workspaces=allowed_workspaces,
|
|
634
687
|
include_legacy_global=include_legacy_global,
|
|
635
688
|
).get("matches", [])
|
|
689
|
+
if matches:
|
|
690
|
+
retrieval_mode = "lexical_only"
|
|
636
691
|
if not matches:
|
|
637
692
|
topics = _topic_candidates(query, limit=4)
|
|
638
693
|
if topics:
|
|
@@ -675,6 +730,8 @@ class KnowledgeGraphRetrievalMixin:
|
|
|
675
730
|
allowed_workspaces,
|
|
676
731
|
include_legacy_global=include_legacy_global,
|
|
677
732
|
)
|
|
733
|
+
if matches:
|
|
734
|
+
retrieval_mode = "lexical_only"
|
|
678
735
|
lines = []
|
|
679
736
|
for match in matches[:limit]:
|
|
680
737
|
meta = match.get("metadata") or {}
|
|
@@ -689,7 +746,39 @@ class KnowledgeGraphRetrievalMixin:
|
|
|
689
746
|
lines.append(
|
|
690
747
|
f"- [{match['type']}] {match['title']} | source={source} | {summary}"
|
|
691
748
|
)
|
|
692
|
-
|
|
749
|
+
context = "\n".join(lines)
|
|
750
|
+
if not with_meta:
|
|
751
|
+
return context
|
|
752
|
+
return {
|
|
753
|
+
"context": context,
|
|
754
|
+
"quality": context_quality_signal(retrieval_mode, len(matches[:limit])),
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
def context_for_query_with_meta(
|
|
758
|
+
self,
|
|
759
|
+
query: str,
|
|
760
|
+
limit: int = 6,
|
|
761
|
+
*,
|
|
762
|
+
allowed_workspaces=None,
|
|
763
|
+
include_legacy_global: bool = False,
|
|
764
|
+
use_hybrid: bool = True,
|
|
765
|
+
) -> Dict[str, Any]:
|
|
766
|
+
"""Additive companion to :meth:`context_for_query` (v9.8.0).
|
|
767
|
+
|
|
768
|
+
Always returns ``{"context": str, "quality": {...}}`` so chat callers
|
|
769
|
+
can surface an honest retrieval signal without changing the legacy
|
|
770
|
+
string-returning contract. Defaults to hybrid retrieval because meta
|
|
771
|
+
consumers want the vector-fallback signal; pass ``use_hybrid=False``
|
|
772
|
+
for the legacy lexical-only sourcing.
|
|
773
|
+
"""
|
|
774
|
+
return self.context_for_query(
|
|
775
|
+
query,
|
|
776
|
+
limit,
|
|
777
|
+
allowed_workspaces=allowed_workspaces,
|
|
778
|
+
include_legacy_global=include_legacy_global,
|
|
779
|
+
use_hybrid=use_hybrid,
|
|
780
|
+
with_meta=True,
|
|
781
|
+
)
|
|
693
782
|
|
|
694
783
|
def neighbors(
|
|
695
784
|
self,
|
|
@@ -470,6 +470,49 @@ class KnowledgeGraphVectorMixin:
|
|
|
470
470
|
],
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
def vector_freshness(self) -> Dict[str, Any]:
|
|
474
|
+
"""Compact vector-index freshness summary for API surfaces (v9.8.0).
|
|
475
|
+
|
|
476
|
+
Reduces :meth:`index_status` (``pending = missing + stale``) to the
|
|
477
|
+
fixed contract ``{"status", "pending_items", "total_items", "detail"}``
|
|
478
|
+
with ``status`` in ``ready`` / ``pending`` / ``unavailable``.
|
|
479
|
+
|
|
480
|
+
Never raises: environments where the embedding provider or index
|
|
481
|
+
storage cannot be used report ``"unavailable"`` with the cause in
|
|
482
|
+
``detail`` instead of surfacing an exception to the API layer.
|
|
483
|
+
"""
|
|
484
|
+
try:
|
|
485
|
+
status = self.index_status()
|
|
486
|
+
except Exception as exc: # noqa: BLE001 — freshness must degrade, not fail
|
|
487
|
+
return {
|
|
488
|
+
"status": "unavailable",
|
|
489
|
+
"pending_items": 0,
|
|
490
|
+
"total_items": 0,
|
|
491
|
+
"detail": f"vector index status unavailable: {exc}",
|
|
492
|
+
}
|
|
493
|
+
pending = int(status.get("pending_items") or 0)
|
|
494
|
+
total = int(status.get("source_items") or 0)
|
|
495
|
+
if pending > 0:
|
|
496
|
+
return {
|
|
497
|
+
"status": "pending",
|
|
498
|
+
"pending_items": pending,
|
|
499
|
+
"total_items": total,
|
|
500
|
+
"detail": (
|
|
501
|
+
f"{pending} of {total} items are missing or stale in the vector index"
|
|
502
|
+
),
|
|
503
|
+
}
|
|
504
|
+
detail = (
|
|
505
|
+
"vector index is up to date"
|
|
506
|
+
if total
|
|
507
|
+
else "vector index is empty (no indexable items yet)"
|
|
508
|
+
)
|
|
509
|
+
return {
|
|
510
|
+
"status": "ready",
|
|
511
|
+
"pending_items": 0,
|
|
512
|
+
"total_items": total,
|
|
513
|
+
"detail": detail,
|
|
514
|
+
}
|
|
515
|
+
|
|
473
516
|
def vector_search(
|
|
474
517
|
self,
|
|
475
518
|
query: str,
|