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.
Files changed (51) hide show
  1. package/README.md +98 -316
  2. package/docs/CHANGELOG.md +37 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/ONBOARDING.md +1 -1
  6. package/docs/TRUST_MODEL.md +1 -1
  7. package/docs/WHY_LATTICE.md +1 -1
  8. package/docs/kg-schema.md +1 -1
  9. package/lattice_brain/__init__.py +1 -1
  10. package/lattice_brain/graph/retrieval.py +93 -4
  11. package/lattice_brain/graph/retrieval_vector.py +43 -0
  12. package/lattice_brain/ingestion.py +399 -14
  13. package/lattice_brain/runtime/multi_agent.py +1 -1
  14. package/latticeai/__init__.py +1 -1
  15. package/latticeai/api/brain_intelligence.py +12 -0
  16. package/latticeai/api/chat.py +17 -0
  17. package/latticeai/api/chat_helpers.py +48 -0
  18. package/latticeai/api/chat_stream.py +9 -1
  19. package/latticeai/api/local_files.py +120 -2
  20. package/latticeai/core/agent_eval.py +123 -0
  21. package/latticeai/core/legacy_compatibility.py +1 -1
  22. package/latticeai/core/marketplace.py +1 -1
  23. package/latticeai/core/workspace_os.py +1 -1
  24. package/latticeai/services/architecture_readiness.py +1 -1
  25. package/latticeai/services/automation_intelligence.py +151 -14
  26. package/latticeai/services/brain_intelligence.py +72 -0
  27. package/latticeai/services/product_readiness.py +1 -1
  28. package/package.json +1 -1
  29. package/scripts/check_current_release_docs.mjs +1 -1
  30. package/src-tauri/Cargo.lock +1 -1
  31. package/src-tauri/Cargo.toml +1 -1
  32. package/src-tauri/tauri.conf.json +1 -1
  33. package/static/app/asset-manifest.json +11 -11
  34. package/static/app/assets/{Act-B6c39ays.js → Act-Dd3z8AzF.js} +2 -2
  35. package/static/app/assets/Brain-BMkgdWnI.js +321 -0
  36. package/static/app/assets/Capture-D2Aw9gkv.js +1 -0
  37. package/static/app/assets/Library-Yreq-KW5.js +1 -0
  38. package/static/app/assets/System-CXNmmtEo.js +1 -0
  39. package/static/app/assets/{index-85wQvEie.css → index-7gY9t9Sd.css} +1 -1
  40. package/static/app/assets/index-CndfILiF.js +18 -0
  41. package/static/app/assets/primitives-DxsIXb6G.js +1 -0
  42. package/static/app/assets/textarea-DH7ne8VI.js +1 -0
  43. package/static/app/index.html +2 -2
  44. package/static/sw.js +1 -1
  45. package/static/app/assets/Brain-D7Qg4k6M.js +0 -321
  46. package/static/app/assets/Capture-VF_di68r.js +0 -1
  47. package/static/app/assets/Library-D_Gis2PA.js +0 -1
  48. package/static/app/assets/System-C5s5H2ov.js +0 -1
  49. package/static/app/assets/index-DJC_2oub.js +0 -18
  50. package/static/app/assets/primitives-DL4Nip8C.js +0 -1
  51. 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
- ) -> str:
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
- matches = self.hybrid_search(
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
- ).get("matches", [])
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
- return "\n".join(lines)
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,