ltcai 10.9.0 → 11.0.1
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 +46 -64
- package/docs/CHANGELOG.md +72 -237
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/PERFORMANCE.md +78 -7
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/docs/v11.1.0_PRODUCT_INTELLIGENCE_PLAN.md +313 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/graph/_kg_contract.py +11 -0
- package/lattice_brain/graph/curator.py +1 -1
- package/lattice_brain/graph/fusion.py +184 -3
- package/lattice_brain/graph/proactive.py +138 -1
- package/lattice_brain/graph/projection.py +10 -2
- package/lattice_brain/graph/retrieval.py +137 -7
- package/lattice_brain/graph/retrieval_docgen.py +20 -20
- package/lattice_brain/graph/retrieval_policy.py +6 -0
- package/lattice_brain/graph/retrieval_reads.py +188 -1
- package/lattice_brain/graph/retrieval_vector.py +474 -110
- package/lattice_brain/graph/schema.py +125 -2
- package/lattice_brain/graph/vector_index/__init__.py +85 -0
- package/lattice_brain/graph/vector_index/base.py +170 -0
- package/lattice_brain/graph/vector_index/brute_force.py +114 -0
- package/lattice_brain/graph/vector_index/hnsw.py +293 -0
- package/lattice_brain/graph/vector_index/jobs.py +287 -0
- package/lattice_brain/graph/vector_index/quantized.py +151 -0
- package/lattice_brain/graph/vector_index/selector.py +131 -0
- package/lattice_brain/ingestion.py +50 -3
- package/lattice_brain/portability.py +654 -2
- package/lattice_brain/runtime/agent_runtime.py +1 -1
- package/lattice_brain/runtime/contracts.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/lattice_brain/self_model.py +620 -0
- package/lattice_brain/synthesis.py +801 -0
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/brain_intelligence.py +83 -1
- package/latticeai/api/chat_stream.py +4 -1
- package/latticeai/api/local_files.py +62 -0
- package/latticeai/api/models.py +1 -1
- package/latticeai/api/portability.py +130 -1
- package/latticeai/api/security_dashboard.py +48 -13
- package/latticeai/api/voice_capture.py +4 -1
- package/latticeai/api/workspace.py +11 -5
- package/latticeai/core/embedding_providers.py +20 -1
- package/latticeai/core/legacy_compatibility.py +1 -1
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/messages.py +14 -0
- package/latticeai/core/model_compat.py +2 -2
- package/latticeai/core/tool_registry.py +0 -7
- package/latticeai/core/workspace_os_constants.py +1 -1
- package/latticeai/core/workspace_os_utils.py +4 -50
- package/latticeai/core/workspace_review_items.py +12 -1
- package/latticeai/integrations/telegram_bot.py +28 -10
- package/latticeai/models/router.py +1 -1
- package/latticeai/runtime/access_runtime.py +1 -1
- package/latticeai/runtime/network_boundary_wiring.py +9 -5
- package/latticeai/runtime/permission_mode_wiring.py +9 -6
- package/latticeai/runtime/router_registration.py +3 -0
- package/latticeai/services/architecture_readiness.py +1 -1
- package/latticeai/services/brain_intelligence.py +253 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +4 -3
- package/latticeai/services/model_engines.py +28 -14
- package/latticeai/services/obsidian_bridge.py +618 -0
- package/latticeai/services/product_readiness.py +5 -3
- package/latticeai/tools/filesystem.py +4 -1
- package/package.json +1 -1
- package/scripts/bench_vector_index.py +295 -0
- package/scripts/check_current_release_docs.mjs +4 -2
- package/scripts/release_screen_claims.json +31 -0
- 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 +37 -37
- package/static/app/assets/{Act-CS9IeqUX.js → Act-D4zSxFR-.js} +1 -1
- package/static/app/assets/{AdminConsole-3UkIEWGA.js → AdminConsole-w5jBfPt2.js} +1 -1
- package/static/app/assets/{Brain-B22EmNqS.js → Brain-C2EqQg74.js} +2 -2
- package/static/app/assets/BrainHome-CvXS6XiQ.js +2 -0
- package/static/app/assets/BrainSignals-DOE_KhOU.js +1 -0
- package/static/app/assets/Capture-DPqpGK8d.js +1 -0
- package/static/app/assets/{CommandPalette-86m4FCcN.js → CommandPalette-CNf7h5fp.js} +1 -1
- package/static/app/assets/Library-BN0HYOfc.js +1 -0
- package/static/app/assets/LivingBrain-Dfq_wEDI.js +1 -0
- package/static/app/assets/ProductFlow-B-3O0rNV.js +1 -0
- package/static/app/assets/{ReviewCard-BepjSDpN.js → ReviewCard-gZ-tdqFM.js} +1 -1
- package/static/app/assets/System-BElUcSSw.js +1 -0
- package/static/app/assets/arrow-left-CFNIMjhv.js +1 -0
- package/static/app/assets/{bot-DQj0-LkM.js → bot--qYHMtkP.js} +1 -1
- package/static/app/assets/brain-DDCLjRqO.js +1 -0
- package/static/app/assets/{button-CmTknyAP.js → button-51Z3rsuv.js} +1 -1
- package/static/app/assets/{circle-pause-yTCWRziJ.js → circle-pause-CMIiMaQl.js} +1 -1
- package/static/app/assets/{circle-play-Ccrva84R.js → circle-play-DZoO_cfG.js} +1 -1
- package/static/app/assets/{cpu-CbJqWTlS.js → cpu-Bs6uc9W9.js} +1 -1
- package/static/app/assets/{download-CkSzbzU-.js → download-G-2olkWz.js} +1 -1
- package/static/app/assets/{folder-open-CKyjQ4PU.js → folder-open-CTOspnmb.js} +1 -1
- package/static/app/assets/{hard-drive-DAzk9um0.js → hard-drive-CewHWJhn.js} +1 -1
- package/static/app/assets/index-CkzokZAj.css +2 -0
- package/static/app/assets/{index-CxOcwsHV.js → index-D7Rr-J2Y.js} +3 -3
- package/static/app/assets/{input-DcMETmZ7.js → input-D4w_BZWl.js} +1 -1
- package/static/app/assets/{permissionCopy-BVf13_25.js → permissionCopy-CosBEXAZ.js} +1 -1
- package/static/app/assets/primitives-d0g9pvzS.js +1 -0
- package/static/app/assets/search-BLCYt75v.js +1 -0
- package/static/app/assets/{share-2-COWCHNZm.js → share-2-NmD7e_oV.js} +1 -1
- package/static/app/assets/{shield-alert-BDrvilyK.js → shield-alert-CcQeMuju.js} +1 -1
- package/static/app/assets/{textarea-rUmsc8cP.js → textarea-BPAJDc-0.js} +1 -1
- package/static/app/assets/{useFocusTrap-Bi5UY_8v.js → useFocusTrap-C7YLdTBC.js} +1 -1
- package/static/app/assets/{useQuery-C-AicB-3.js → useQuery-DRyD9opW.js} +1 -1
- package/static/app/assets/{utils-BMwWg78e.js → utils-DG1_ExrP.js} +3 -3
- package/static/app/assets/{workspace-Y93tls8P.js → workspace-CWVf3gsI.js} +1 -1
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/BrainHome-CMDqgJF4.js +0 -2
- package/static/app/assets/BrainSignals-BeE8RJo3.js +0 -1
- package/static/app/assets/Capture-ZX9bQh68.js +0 -1
- package/static/app/assets/Library-Bhz5LUca.js +0 -1
- package/static/app/assets/LivingBrain-BSa0wpFG.js +0 -1
- package/static/app/assets/ProductFlow-IP4Q-5aQ.js +0 -1
- package/static/app/assets/System-Bwx1h_jT.js +0 -1
- package/static/app/assets/arrow-left-ig7AZU8B.js +0 -1
- package/static/app/assets/brain-D8OEwmVj.js +0 -1
- package/static/app/assets/index-BfD-jhA9.css +0 -2
- package/static/app/assets/primitives-CQV9Q2YM.js +0 -1
- package/static/app/assets/search-CXGASMMH.js +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# LatticeAI v11.1.0 — Full Feature Sprint Plan
|
|
2
|
+
|
|
3
|
+
**목표 버전**: `11.1.0`
|
|
4
|
+
**포지셔닝**: Foundation Hardening (v9–v11.0) → **Product Intelligence Layer**
|
|
5
|
+
**원칙**: 보안/커버리지 게이트는 회귀만 유지. 새로운 기능 개발에 집중.
|
|
6
|
+
**범위**: 아래 5개 트랙을 **한 릴리즈에서 모두** 완료한다.
|
|
7
|
+
**작성일**: 2026-08-10
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. 한 줄 요약
|
|
12
|
+
|
|
13
|
+
> **벡터 검색을 스케일 가능하게 만들고 → Brain이 스스로 생각하고 모순을 감지하게 만들고 → 멀티모달을 1등 시민으로 올리고 → 개인 온톨로지 + 에이전트 워크스페이스를 붙이고 → 외부 지식 도구와 선택적 공유까지 연결한다.**
|
|
14
|
+
|
|
15
|
+
이 순서는 **의존성**과 **제품 체감 가치**를 동시에 고려한 것이다.
|
|
16
|
+
앞 단계가 없으면 뒤 단계가 의미가 없거나 성능/품질이 붕괴한다.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. 전체 의존성 & 실행 순서 (강제)
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
[1] High-Perf Vector + Incremental/Background Indexing
|
|
24
|
+
↓ (스케일·속도 확보)
|
|
25
|
+
[2] Proactive Synthesis + Contradiction + Temporal Reasoning
|
|
26
|
+
↓ (Brain이 "살아있다"는 지능 레이어)
|
|
27
|
+
[3] Multi-modal as First-Class Graph Citizen
|
|
28
|
+
↓ (이미지/오디오가 진짜 기억이 됨)
|
|
29
|
+
[4] Personal Ontology / Self-Model + Agent-Native Workspace OS
|
|
30
|
+
↓ (개인화 + 장기 자율성)
|
|
31
|
+
[5] Interop Bridges + Selective Encrypted Brain Network
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**왜 이 순서인가**
|
|
35
|
+
|
|
36
|
+
| 순서 | 이유 |
|
|
37
|
+
|------|------|
|
|
38
|
+
| 1 | 현재 brute-force 벡터 검색이 5k 노드에서 ~1.7초. 이 병목을 제거하지 않으면 2~5번 기능을 붙여도 체감이 안 난다. |
|
|
39
|
+
| 2 | `proactive.py`가 이미 읽기 전용으로 존재. 벡터 품질이 올라가야 모순 감지·합성이 신뢰 가능해진다. |
|
|
40
|
+
| 3 | 스키마(IMAGE 등)는 이미 있다. 하지만 retrieval/embedding 파이프라인이 텍스트 중심이라 멀티모달이 붙으면 검색 품질이 떨어질 수 있다 → 1번 이후. |
|
|
41
|
+
| 4 | AgentRuntime + Change Governor가 이미 있다. 개인 온톨로지와 워크스페이스 OS는 2·3번이 만든 “풍부한 기억” 위에 올려야 의미 있다. |
|
|
42
|
+
| 5 | 인제스트 게이트와 포트어빌리티가 안정된 뒤에야 외부 브릿지와 네트워크 공유가 안전하다. |
|
|
43
|
+
|
|
44
|
+
**병렬 가능 구간**
|
|
45
|
+
- 1번 진행 중에도 2번의 **스키마/데이터 모델 설계**와 4번의 **온톨로지 스키마 설계**는 병행 가능.
|
|
46
|
+
- 3번의 비전 임베딩 프로바이더 실험은 1번과 거의 독립적으로 가능.
|
|
47
|
+
- 5번의 Obsidian/Notion 파서 프로토타입은 인제스트 파이프라인만 안정되면 초반부터 가능.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. 공통 가드레일 (모든 트랙에 적용)
|
|
52
|
+
|
|
53
|
+
1. **SQLite는 여전히 Source of Truth**. 벡터 인덱스, 파생 그래프, 캐시는 모두 파생물. 깨져도 재구축 가능해야 한다.
|
|
54
|
+
2. **Change Governor / Proposal-first** 유지. 에이전트가 Brain을 직접 쓰면 안 되고, 제안 → 리뷰 → 적용 경로를 탄다.
|
|
55
|
+
3. **Local-first 기본값**. 클라우드 모델·네트워크·공유는 전부 opt-in.
|
|
56
|
+
4. **정직성(Honest by design)**. 검색 품질, 벡터 freshness, 모순 존재 여부, 멀티모달 추출 품질을 사용자에게 숨기지 않는다.
|
|
57
|
+
5. **문서 동기화**. 기능이 들어가면 `ARCHITECTURE.md`, `FEATURE_STATUS.md`, `RELEASE_NOTES`, `AGENTS.md`의 현재 릴리즈 마커를 같은 커밋에서 업데이트.
|
|
58
|
+
6. **테스트 전략 변경**. 새 기능은 단위 테스트 + 통합 시나리오 테스트만 추가. 커버리지 %를 억지로 올리지 않는다. 기존 100% 게이트는 회귀만 지킨다.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. Track 1 — High-Performance Vector Search + Incremental Indexing
|
|
63
|
+
|
|
64
|
+
### 목표
|
|
65
|
+
- 50k~100k 노드에서도 hybrid 검색 p50 < 40~60ms
|
|
66
|
+
- 배경 인덱싱으로 사용자가 인제스트 후 바로 검색 가능하게
|
|
67
|
+
- 기존 hybrid (FTS5 + vector) + `context_quality` 신호 유지
|
|
68
|
+
|
|
69
|
+
### 현재 상태
|
|
70
|
+
- `lattice_brain/graph/retrieval_vector.py` : brute-force + vector freshness
|
|
71
|
+
- `hybrid_search` 존재
|
|
72
|
+
- PERFORMANCE.md 기준 5k에서 벡터 p50 ~1.7s
|
|
73
|
+
|
|
74
|
+
### 구현 단계
|
|
75
|
+
|
|
76
|
+
#### 1.1 벡터 인덱스 추상화 레이어 도입
|
|
77
|
+
- `lattice_brain/graph/vector_index/` 패키지 신설
|
|
78
|
+
- Protocol 정의:
|
|
79
|
+
```python
|
|
80
|
+
class VectorIndex(Protocol):
|
|
81
|
+
def add(self, id: str, vector: np.ndarray, metadata: dict) -> None: ...
|
|
82
|
+
def search(self, query: np.ndarray, top_k: int, filter: dict | None = None) -> list[ScoredId]: ...
|
|
83
|
+
def remove(self, id: str) -> None: ...
|
|
84
|
+
def rebuild(self) -> None: ...
|
|
85
|
+
def stats(self) -> IndexStats: ...
|
|
86
|
+
```
|
|
87
|
+
- 기본 구현체 2개:
|
|
88
|
+
- `BruteForceIndex` (기존 로직 이전, fallback)
|
|
89
|
+
- `HnswIndex` (신규)
|
|
90
|
+
|
|
91
|
+
#### 1.2 HNSW 엔진 선택 & 통합
|
|
92
|
+
**권장 경로 (우선순위)**:
|
|
93
|
+
1. **단기 (가장 빠름)**: `usearch` 또는 `hnswlib` Python 바인딩으로 프로토타입. Apple Silicon에서 잘 돌아가는지 확인.
|
|
94
|
+
2. **중기 (권장 최종)**: 순수 Rust HNSW + Python binding (또는 Tauri 쪽 native).
|
|
95
|
+
후보: `usearch`(이미 Rust 코어), `minimemory`, `leann-core`, `monavec` 계열.
|
|
96
|
+
SQLite 파일과 별도로 `.hnsw` 또는 메모리맵 인덱스를 Brain 디렉터리에 둔다.
|
|
97
|
+
3. Quantization (int8 / binary) 옵션을 처음부터 넣어서 메모리 절약.
|
|
98
|
+
|
|
99
|
+
#### 1.3 Incremental / Background Indexing
|
|
100
|
+
- 인제스트 파이프라인에 `vector_job` 큐 추가 (이미 `/api/ingestion/jobs` 패턴 존재).
|
|
101
|
+
- 노드 upsert 시 “pending embed” 상태로 표시 → 백그라운드 워커가 임베딩 + 인덱스 추가.
|
|
102
|
+
- `vector_freshness` API를 더 세분화: `embedded / pending / stale / total`.
|
|
103
|
+
- 재인덱싱은 전체 rebuild와 incremental upsert 둘 다 지원.
|
|
104
|
+
|
|
105
|
+
#### 1.4 Hybrid Search 고도화
|
|
106
|
+
- Reciprocal Rank Fusion (RRF) 또는 학습 가능한 fusion weight를 query class별로 적용.
|
|
107
|
+
- graph traversal 결과를 candidate expansion으로 사용 (이미 neighbors/traverse 존재).
|
|
108
|
+
- `context_quality` 점수에 vector freshness와 ANN recall 추정치를 반영.
|
|
109
|
+
|
|
110
|
+
#### 1.5 완료 기준
|
|
111
|
+
- [ ] 10k 합성 노드에서 hybrid p50 < 50ms
|
|
112
|
+
- [ ] 배경 인덱싱이 켜진 상태에서 새 파일 인제스트 후 30초 이내 검색 가능
|
|
113
|
+
- [ ] 기존 hybrid_search API 시그니처 호환 (breaking change 없음)
|
|
114
|
+
- [ ] `docs/PERFORMANCE.md` 업데이트 + 벤치 스크립트 포함
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 4. Track 2 — Proactive Synthesis + Contradiction + Temporal
|
|
119
|
+
|
|
120
|
+
### 목표
|
|
121
|
+
Brain이 수동 검색 도구가 아니라 **스스로 관찰하고 제안하는 존재**가 된다.
|
|
122
|
+
|
|
123
|
+
### 현재 상태
|
|
124
|
+
- `lattice_brain/graph/proactive.py` : 읽기 전용 (duplicate, contradiction, quality report)
|
|
125
|
+
- Brain Brief, MultiAgentOrchestrator, workflow_engine 존재
|
|
126
|
+
|
|
127
|
+
### 구현 단계
|
|
128
|
+
|
|
129
|
+
#### 2.1 Contradiction Detection을 쓰기 가능한 루프로
|
|
130
|
+
- 기존 읽기 전용 감지를 **제안 생성**까지 확장.
|
|
131
|
+
- 새 사실 인제스트 시 기존 관련 노드와 비교 → `ContradictionProposal` 생성.
|
|
132
|
+
- UI: “이 두 기억은 모순됩니다. 어떻게 할까요?” (유지 / 새 사실로 교체 / 둘 다 유지하고 시간 범위 표시)
|
|
133
|
+
|
|
134
|
+
#### 2.2 Temporal Model 도입
|
|
135
|
+
- 노드/엣지에 `valid_from`, `valid_to`, `superseded_by` 필드 추가 (스키마 확장).
|
|
136
|
+
- 쿼리 API: `as_of(timestamp)` → 그 시점의 유효 그래프 슬라이스.
|
|
137
|
+
- “2025년 6월에 내가 알고 있던 프로젝트 상태” 같은 질문 지원.
|
|
138
|
+
|
|
139
|
+
#### 2.3 Background Synthesis Jobs
|
|
140
|
+
- 주기적 또는 이벤트 기반 (새 노드 N개 추가 후) synthesis 실행.
|
|
141
|
+
- 출력물:
|
|
142
|
+
- Brain Brief 강화 (주간/월간 요약)
|
|
143
|
+
- 새로운 상위 개념 노드 제안
|
|
144
|
+
- “자주 같이 등장하지만 연결이 없는” 후보 엣지 제안
|
|
145
|
+
- 모두 Change Governor를 통해 제안으로만 들어온다.
|
|
146
|
+
|
|
147
|
+
#### 2.4 Memory Decay & Consolidation (선택적이지만 강력)
|
|
148
|
+
- 접근 빈도 + 시간 경과로 importance score 계산.
|
|
149
|
+
- 낮은 점수 에피소딕 기억은 상위 semantic 노드로 합쳐지는 “수면” 프로세스 시뮬레이션.
|
|
150
|
+
- 사용자에게 “Brain이 정리 중입니다” 신호를 정직하게 보여줌.
|
|
151
|
+
|
|
152
|
+
#### 2.5 완료 기준
|
|
153
|
+
- [ ] 모순이 발생하면 제안이 자동 생성되고 UI에서 리뷰 가능
|
|
154
|
+
- [ ] `as_of` 쿼리가 동작하고 테스트 커버
|
|
155
|
+
- [ ] Brain Brief가 proactive 제안을 포함
|
|
156
|
+
- [ ] 모든 쓰기는 proposal 경로
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 5. Track 3 — Multi-modal as First-Class Citizen
|
|
161
|
+
|
|
162
|
+
### 목표
|
|
163
|
+
이미지·오디오·(간단)비디오가 텍스트와 동등한 기억 단위가 된다.
|
|
164
|
+
|
|
165
|
+
### 현재 상태
|
|
166
|
+
- 스키마에 IMAGE / IMAGE_TEXT / CONTAINS_IMAGE 존재
|
|
167
|
+
- discovery에 PIL + pytesseract OCR 일부
|
|
168
|
+
- `allow_multimodal` 플래그 기본 false
|
|
169
|
+
- vision embedding / retrieval 경로 미비
|
|
170
|
+
|
|
171
|
+
### 구현 단계
|
|
172
|
+
|
|
173
|
+
#### 3.1 임베딩 파이프라인 확장
|
|
174
|
+
- `embedding_providers.py`에 vision 프로바이더 추가 (로컬: MLX 비전 모델 또는 CLIP 계열, 클라우드: 선택적).
|
|
175
|
+
- 텍스트 임베딩과 같은 차원으로 맞추거나, 멀티모달 공통 공간 사용.
|
|
176
|
+
- 이미지 노드에 `embedding` + `ocr_text` + `caption` (비전 LLM 생성) 저장.
|
|
177
|
+
|
|
178
|
+
#### 3.2 인제스트 라우팅
|
|
179
|
+
- 파일 MIME에 따라 IMAGE / AUDIO 노드 생성.
|
|
180
|
+
- 오디오: 로컬 whisper 계열 또는 시스템 전사 → 텍스트 + 타임스탬프 청크.
|
|
181
|
+
- 비디오: 키프레임 추출 + 자막/전사 (1차로 키프레임만 해도 충분).
|
|
182
|
+
|
|
183
|
+
#### 3.3 Retrieval 통합
|
|
184
|
+
- hybrid_search가 이미지 노드도 반환.
|
|
185
|
+
- “지난달 제주에서 찍은 사진 중 회의 관련” 같은 쿼리가 그래프 + 벡터로 동작.
|
|
186
|
+
- UI Evidence 패널에 이미지 썸네일 + 캡션 + 출처 표시.
|
|
187
|
+
|
|
188
|
+
#### 3.4 완료 기준
|
|
189
|
+
- [ ] 이미지 폴더 인제스트 → 검색 가능
|
|
190
|
+
- [ ] 비전 캡션이 그래프에 들어가고 hybrid에 기여
|
|
191
|
+
- [ ] `allow_multimodal=true`일 때만 동작 (기본값 유지 가능)
|
|
192
|
+
- [ ] 추출 품질 점수가 이미지에도 적용
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 6. Track 4 — Personal Ontology + Agent-Native Workspace OS
|
|
197
|
+
|
|
198
|
+
### 목표
|
|
199
|
+
Brain이 “나”를 알고, 에이전트가 워크스페이스를 실제로 조작·진화시킬 수 있게 한다.
|
|
200
|
+
|
|
201
|
+
### 구현 단계
|
|
202
|
+
|
|
203
|
+
#### 4.1 Personal Ontology / Self-Model
|
|
204
|
+
- 새로운 노드 타입 또는 특수 서브그래프: `Self`, `Preference`, `Decision`, `Habit`, `Relationship`.
|
|
205
|
+
- 대화와 파일에서 자동 추출 → 제안으로만 추가.
|
|
206
|
+
- 에이전트 컨텍스트에 “현재 Self-Model 요약”을 항상 주입할 수 있는 API.
|
|
207
|
+
- 사용자는 Self-Model을 직접 편집·삭제 가능 (ownership).
|
|
208
|
+
|
|
209
|
+
#### 4.2 Agent-Native Workspace
|
|
210
|
+
- AgentRuntime이 파일 생성/수정/이동/삭제 제안을 더 안정적으로 수행.
|
|
211
|
+
- 장기 실행 세션: “이 프로젝트를 정리해줘” → 여러 단계 proposal 체인.
|
|
212
|
+
- Workspace OS (`workspace_os.py`)와 더 깊은 통합: 에이전트가 폴더 구조를 Brain 관점으로 재구성 제안.
|
|
213
|
+
- 약한 로컬 모델(Gemma 등)에서도 “파일 생성까지” 가는 성공률을 높이는 프롬프트/툴 설계 (이미 관심 영역).
|
|
214
|
+
|
|
215
|
+
#### 4.3 완료 기준
|
|
216
|
+
- [ ] Self-Model이 생성되고 채팅 컨텍스트에 반영
|
|
217
|
+
- [ ] 에이전트가 파일 작업을 proposal로 안정적으로 수행
|
|
218
|
+
- [ ] 사용자가 Self-Model을 투명하게 볼 수 있음
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 7. Track 5 — Interop + Selective Brain Network
|
|
223
|
+
|
|
224
|
+
### 목표
|
|
225
|
+
기존 지식 도구와 연결하고, 선택적으로 다른 Brain과 안전하게 공유.
|
|
226
|
+
|
|
227
|
+
### 구현 단계
|
|
228
|
+
|
|
229
|
+
#### 5.1 Interop Bridges (우선순위 높은 것부터)
|
|
230
|
+
1. **Obsidian** — vault 폴더를 감시하거나 수동 동기. 마크다운 → 노드 + 백링크를 엣지로.
|
|
231
|
+
2. **Notion** — export 또는 API (opt-in) 인제스트.
|
|
232
|
+
3. **이메일/캘린더** — 로컬 파일 또는 시스템 권한으로 (macOS 중심).
|
|
233
|
+
4. **Git** — 커밋 메시지 + 변경 파일을 프로젝트 기억으로.
|
|
234
|
+
|
|
235
|
+
모두 기존 IngestionPipeline 단일 게이트를 통과.
|
|
236
|
+
|
|
237
|
+
#### 5.2 Selective Encrypted Brain Network
|
|
238
|
+
- `.latticebrain` 아카이브의 확장: 전체 Brain이 아니라 **선택적 subgraph + provenance** 내보내기.
|
|
239
|
+
- 수신 측은 “제안으로만” 받아들이고, 출처를 명확히 표시.
|
|
240
|
+
- 암호화는 기존 아카이브 메커니즘 재사용 + 수신자 공개키(선택).
|
|
241
|
+
- UI: “이 결정 그래프만 공유” 같은 단위.
|
|
242
|
+
|
|
243
|
+
#### 5.3 완료 기준
|
|
244
|
+
- [ ] Obsidian vault → Brain 인제스트가 동작
|
|
245
|
+
- [ ] subgraph 공유 → 상대방 Brain에 proposal로 들어옴
|
|
246
|
+
- [ ] 네트워크 기능은 기본 꺼짐 (opt-in)
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 8. 권장 실행 일정 (11.1.0 단일 릴리즈 기준)
|
|
251
|
+
|
|
252
|
+
가정: 집중 작업 기준 3~5주 스프린트.
|
|
253
|
+
|
|
254
|
+
| 주차 | 주요 포커스 | 산출물 |
|
|
255
|
+
|------|-------------|--------|
|
|
256
|
+
| Week 1 | Track 1 전체 (벡터 추상화 + HNSW 프로토타입 + 배경 인덱싱) | 벤치 통과, hybrid API 호환 |
|
|
257
|
+
| Week 2 | Track 2 (모순 제안 + temporal 스키마 + synthesis job) + Track 1 안정화 | proactive UI 초안 |
|
|
258
|
+
| Week 3 | Track 3 (비전 임베딩 + 이미지 인제스트 + retrieval) + Track 4 스키마 | 이미지 검색 가능 |
|
|
259
|
+
| Week 4 | Track 4 구현 + Track 5 브릿지 (Obsidian 우선) | Self-Model + Obsidian |
|
|
260
|
+
| Week 5 | 통합, 폴리시, 문서, 릴리즈 노트, 벤치 재측정 | v11.1.0 태그 |
|
|
261
|
+
|
|
262
|
+
병렬로 돌릴 수 있는 부분은 최대한 병렬.
|
|
263
|
+
막히는 지점(특히 Rust 바인딩 성능)은 Python 바인딩으로 먼저 출시하고 다음 마이너에서 네이티브로 교체해도 된다.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 9. 릴리즈 전략 & 문서 게이트
|
|
268
|
+
|
|
269
|
+
- 브랜치: `release/v11.1.0` 또는 `feature/product-intelligence`
|
|
270
|
+
- 모든 트랙이 머지된 후 **한 번의** 통합 테스트 + 성능 벤치 후 태그.
|
|
271
|
+
- `AGENTS.md`, `ARCHITECTURE.md`, `FEATURE_STATUS.md`, `RELEASE_NOTES_v11.1.0.md` 동시 업데이트.
|
|
272
|
+
- CHANGELOG에는 “Product Intelligence Layer”로 포지셔닝.
|
|
273
|
+
- 기존 100% 커버리지 게이트는 유지하되, 신규 코드에 대한 과도한 커버리지 강요는 하지 않는다 (회귀만).
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 10. 위험 요소와 완화
|
|
278
|
+
|
|
279
|
+
| 위험 | 완화 |
|
|
280
|
+
|------|------|
|
|
281
|
+
| HNSW 도입으로 검색 품질(recall) 하락 | 기본 fallback을 brute-force로 두고, ANN은 `approx=true` 옵션. recall 벤치 필수. |
|
|
282
|
+
| 멀티모달 임베딩 차원이 텍스트와 다름 | 공통 공간 모델 사용 또는 별도 인덱스 + late fusion. |
|
|
283
|
+
| Temporal 스키마 변경으로 기존 Brain 깨짐 | 마이그레이션 스크립트 + `valid_from` 기본값을 created_at으로. |
|
|
284
|
+
| 에이전트가 너무 공격적으로 제안 | PermissionMode + Change Governor 강화. 기본은 보수적. |
|
|
285
|
+
| 11.1.0 범위가 너무 큼 | Track 5의 Brain Network는 “프로토타입 + 플래그”로 축소 가능. Interop는 Obsidian만 필수. |
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## 11. 성공의 정의 (v11.1.0 완료 시)
|
|
290
|
+
|
|
291
|
+
사용자가 다음을 체감해야 한다:
|
|
292
|
+
|
|
293
|
+
1. **빠르다** — 큰 Brain에서도 검색이 즉각적이다.
|
|
294
|
+
2. **살아있다** — Brain이 모순을 알려주고, 요약을 해주고, 연결을 제안한다.
|
|
295
|
+
3. **모든 것을 기억한다** — 사진도, 메모도, 대화도 같은 그래프에 있다.
|
|
296
|
+
4. **나를 안다** — 내 선호와 결정 이력이 에이전트 행동에 반영된다.
|
|
297
|
+
5. **연결된다** — Obsidian 등 기존 도구와 자연스럽게 이어지고, 원하면 선택적으로 공유할 수 있다.
|
|
298
|
+
|
|
299
|
+
이것이 “Model is the voice, Brain is the lifelong asset”을 제품 수준에서 완성한 상태다.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## 12. 다음 액션 (바로 시작)
|
|
304
|
+
|
|
305
|
+
1. 이 문서를 `docs/v11.1.0_PRODUCT_INTELLIGENCE_PLAN.md`로 커밋.
|
|
306
|
+
2. Track 1부터 이슈/브랜치 생성.
|
|
307
|
+
3. 벡터 인덱스 Protocol + BruteForce 이전 작업부터 착수.
|
|
308
|
+
4. 매주 금요일에 “체감 데모” 기준으로 진행 상황 점검 (커버리지 %가 아니라).
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
**문서 끝.**
|
|
313
|
+
이 계획대로면 v11.1.0은 “테스트와 보안의 LatticeAI”에서 **“실제로 쓰는 지능형 Digital Brain”**으로의 전환점이 된다.
|
|
@@ -173,6 +173,17 @@ class KnowledgeGraphCore:
|
|
|
173
173
|
"""``{status, pending_items, total_items, detail}`` — never raises."""
|
|
174
174
|
raise NotImplementedError
|
|
175
175
|
|
|
176
|
+
# ── retrieval_reads.py: 1-hop walk (hybrid candidate expansion) ──────────
|
|
177
|
+
def neighbors(
|
|
178
|
+
self,
|
|
179
|
+
node_id: str,
|
|
180
|
+
*,
|
|
181
|
+
allowed_workspaces: Any = None,
|
|
182
|
+
include_legacy_global: bool = False,
|
|
183
|
+
as_of: Any = None,
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
raise NotImplementedError
|
|
186
|
+
|
|
176
187
|
# ── retrieval_reads.py: workspace scoping ────────────────────────────────
|
|
177
188
|
def filter_scoped_nodes(
|
|
178
189
|
self,
|
|
@@ -196,7 +196,7 @@ def extract_topic_candidates(
|
|
|
196
196
|
seen_in_doc: Set[str] = set()
|
|
197
197
|
for term in bag:
|
|
198
198
|
if term in seen_in_doc:
|
|
199
|
-
continue
|
|
199
|
+
continue # pragma: no cover — unreachable: bag is list(set(...)), already deduplicated
|
|
200
200
|
seen_in_doc.add(term)
|
|
201
201
|
counts[term] = counts.get(term, 0.0) + weight
|
|
202
202
|
sources.setdefault(term, []).append(str(doc.get("id") or ""))
|
|
@@ -28,7 +28,7 @@ from __future__ import annotations
|
|
|
28
28
|
import json
|
|
29
29
|
import os
|
|
30
30
|
import re
|
|
31
|
-
from typing import Any, Dict, Mapping, Optional
|
|
31
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
32
32
|
|
|
33
33
|
from ..quiet import quiet
|
|
34
34
|
|
|
@@ -36,6 +36,26 @@ QUERY_CLASSES = ("fact", "code", "person", "recency")
|
|
|
36
36
|
|
|
37
37
|
FUSION_WEIGHTS_ENV = "LATTICEAI_FUSION_WEIGHTS"
|
|
38
38
|
|
|
39
|
+
# ── fusion strategy (v11.1.0) ────────────────────────────────────────────────
|
|
40
|
+
# Linear alpha fusion needs the two channels' scores to be comparable. They
|
|
41
|
+
# are not: the lexical channel scores 1/rank while the vector channel scores a
|
|
42
|
+
# max-normalized cosine, so a query where every cosine lands near 0.9 flattens
|
|
43
|
+
# the vector channel to noise. Reciprocal Rank Fusion ignores score magnitudes
|
|
44
|
+
# entirely and fuses *positions*, which is exactly the property that survives
|
|
45
|
+
# incomparable scales.
|
|
46
|
+
#
|
|
47
|
+
# It is opt-in, per query class, and off everywhere by default: alpha fusion
|
|
48
|
+
# is what every existing ranking assertion in this repo describes, and a
|
|
49
|
+
# ranking change dressed up as a bug fix is how retrieval quality regressions
|
|
50
|
+
# get shipped. Turn it on with LATTICEAI_FUSION_STRATEGY=rrf (all classes) or
|
|
51
|
+
# a JSON object like {"code": "rrf"} (per class).
|
|
52
|
+
FUSION_STRATEGY_ENV = "LATTICEAI_FUSION_STRATEGY"
|
|
53
|
+
FUSION_STRATEGIES = ("alpha", "rrf")
|
|
54
|
+
DEFAULT_FUSION_STRATEGY: Dict[str, str] = dict.fromkeys(QUERY_CLASSES, "alpha")
|
|
55
|
+
#: The smoothing constant from the original RRF paper (Cormack et al., 2009).
|
|
56
|
+
#: Larger k flattens the curve, so rank 1 wins by less.
|
|
57
|
+
DEFAULT_RRF_K = 60
|
|
58
|
+
|
|
39
59
|
# Per-class fusion weights. "fact" mirrors the pre-fusion-gate defaults
|
|
40
60
|
# (SearchService DEFAULT_HYBRID_WEIGHTS + graph-layer alpha=0.6) so the
|
|
41
61
|
# fallback class introduces zero behavioral drift.
|
|
@@ -145,16 +165,163 @@ def fusion_weight_table(
|
|
|
145
165
|
return table
|
|
146
166
|
|
|
147
167
|
|
|
168
|
+
def _env_strategy_overrides() -> Dict[str, str]:
|
|
169
|
+
"""Parse ``LATTICEAI_FUSION_STRATEGY`` → per-class strategy overrides.
|
|
170
|
+
|
|
171
|
+
Two accepted forms: a bare strategy name (``rrf``) applies to every class,
|
|
172
|
+
a JSON object (``{"code": "rrf"}``) applies per class. Anything else —
|
|
173
|
+
a typo, malformed JSON, an unknown strategy — is ignored rather than
|
|
174
|
+
raising, so a bad env var degrades to the default fusion instead of
|
|
175
|
+
taking every search down.
|
|
176
|
+
"""
|
|
177
|
+
raw = os.getenv(FUSION_STRATEGY_ENV, "").strip()
|
|
178
|
+
if not raw:
|
|
179
|
+
return {}
|
|
180
|
+
if raw.lower() in FUSION_STRATEGIES:
|
|
181
|
+
return dict.fromkeys(QUERY_CLASSES, raw.lower())
|
|
182
|
+
try:
|
|
183
|
+
payload = json.loads(raw)
|
|
184
|
+
except (ValueError, TypeError):
|
|
185
|
+
return {}
|
|
186
|
+
if not isinstance(payload, dict):
|
|
187
|
+
return {}
|
|
188
|
+
return {
|
|
189
|
+
str(cls): str(value).lower()
|
|
190
|
+
for cls, value in payload.items()
|
|
191
|
+
if cls in DEFAULT_FUSION_STRATEGY and str(value).lower() in FUSION_STRATEGIES
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def fusion_strategy_table(
|
|
196
|
+
overrides: Optional[Mapping[str, str]] = None,
|
|
197
|
+
) -> Dict[str, str]:
|
|
198
|
+
"""Full per-class strategy table: defaults ← env override ← caller."""
|
|
199
|
+
table = dict(DEFAULT_FUSION_STRATEGY)
|
|
200
|
+
for source in (_env_strategy_overrides(), overrides or {}):
|
|
201
|
+
for cls, value in source.items():
|
|
202
|
+
if cls in table and str(value).lower() in FUSION_STRATEGIES:
|
|
203
|
+
table[cls] = str(value).lower()
|
|
204
|
+
return table
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def rrf_score(rank: int, *, k: int = DEFAULT_RRF_K) -> float:
|
|
208
|
+
"""One channel's contribution for an item it ranked at ``rank`` (1-based)."""
|
|
209
|
+
return 1.0 / (int(k) + max(1, int(rank)))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def rrf_fuse(
|
|
213
|
+
rankings: Mapping[str, Sequence[str]],
|
|
214
|
+
*,
|
|
215
|
+
k: int = DEFAULT_RRF_K,
|
|
216
|
+
weights: Optional[Mapping[str, float]] = None,
|
|
217
|
+
) -> Dict[str, float]:
|
|
218
|
+
"""Reciprocal Rank Fusion over per-channel id rankings.
|
|
219
|
+
|
|
220
|
+
``rankings`` maps a channel name to its ids in rank order (best first).
|
|
221
|
+
An id absent from a channel simply contributes nothing from it — RRF has
|
|
222
|
+
no notion of a "missing score" to guess at, which is the other reason it
|
|
223
|
+
survives channels that fail independently.
|
|
224
|
+
|
|
225
|
+
``weights`` optionally scales a channel's contribution (a channel missing
|
|
226
|
+
from the mapping weighs 1.0). Deterministic and pure.
|
|
227
|
+
"""
|
|
228
|
+
fused: Dict[str, float] = {}
|
|
229
|
+
for channel, ordered in rankings.items():
|
|
230
|
+
weight = 1.0 if weights is None else float(weights.get(channel, 1.0))
|
|
231
|
+
for rank, item_id in enumerate(ordered, start=1):
|
|
232
|
+
fused[item_id] = fused.get(item_id, 0.0) + weight * rrf_score(rank, k=k)
|
|
233
|
+
return fused
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ── graph traversal candidate expansion (v11.1.0) ────────────────────────────
|
|
237
|
+
# Both retrieval channels can only return what they matched. A note that
|
|
238
|
+
# never says "postgres" but is one CONTAINS edge away from the one that does
|
|
239
|
+
# is unreachable by either — and it is often the answer. Expansion walks one
|
|
240
|
+
# hop out from the top hits and offers those neighbours as candidates.
|
|
241
|
+
#
|
|
242
|
+
# It is capped, off by default, and reports its own counts, because the
|
|
243
|
+
# failure mode is dilution: an unbounded expansion turns a precise answer into
|
|
244
|
+
# a tour of the graph.
|
|
245
|
+
GRAPH_EXPANSION_ENV = "LATTICEAI_GRAPH_EXPANSION"
|
|
246
|
+
DEFAULT_EXPANSION_SEEDS = 3
|
|
247
|
+
DEFAULT_EXPANSION_CAP = 5
|
|
248
|
+
#: Expanded candidates inherit a damped share of their seed's score: they are
|
|
249
|
+
#: related to a hit, they are not themselves a hit.
|
|
250
|
+
EXPANSION_DECAY = 0.5
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def graph_expansion_enabled() -> bool:
|
|
254
|
+
"""True when ``LATTICEAI_GRAPH_EXPANSION`` opts in (default: off)."""
|
|
255
|
+
raw = os.getenv(GRAPH_EXPANSION_ENV, "").strip().lower()
|
|
256
|
+
return raw in {"1", "true", "yes", "on"}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def expand_with_neighbors(
|
|
260
|
+
seeds: Sequence[Tuple[str, float]],
|
|
261
|
+
neighbors_fn: Callable[[str], Any],
|
|
262
|
+
*,
|
|
263
|
+
exclude: Optional[Sequence[str]] = None,
|
|
264
|
+
cap: int = DEFAULT_EXPANSION_CAP,
|
|
265
|
+
) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
|
|
266
|
+
"""One-hop neighbours of ``seeds`` as extra candidates + an honest report.
|
|
267
|
+
|
|
268
|
+
``seeds`` is ``[(node_id, score)]`` best first; ``neighbors_fn`` is the
|
|
269
|
+
store's ``neighbors``. Returns ``(candidates, report)`` where each
|
|
270
|
+
candidate carries its originating seed and a decayed score, and the report
|
|
271
|
+
states how many seeds were walked, how many candidates were kept, and
|
|
272
|
+
whether the cap bit. A neighbour lookup that fails is skipped and counted,
|
|
273
|
+
never raised: expansion is an optimization, not a dependency.
|
|
274
|
+
"""
|
|
275
|
+
known = {str(item) for item in (exclude or ())}
|
|
276
|
+
cap = max(0, int(cap))
|
|
277
|
+
candidates: List[Dict[str, Any]] = []
|
|
278
|
+
failures = 0
|
|
279
|
+
for seed_id, seed_score in seeds:
|
|
280
|
+
if len(candidates) >= cap:
|
|
281
|
+
break
|
|
282
|
+
try:
|
|
283
|
+
payload = neighbors_fn(seed_id) or {}
|
|
284
|
+
except Exception: # noqa: BLE001 — a bad seed must not fail the search
|
|
285
|
+
failures += 1
|
|
286
|
+
continue
|
|
287
|
+
for node in payload.get("neighbors") or []:
|
|
288
|
+
node_id = str(node.get("id") or "")
|
|
289
|
+
if not node_id or node_id in known:
|
|
290
|
+
continue
|
|
291
|
+
known.add(node_id)
|
|
292
|
+
candidates.append(
|
|
293
|
+
{
|
|
294
|
+
"node": node,
|
|
295
|
+
"seed": str(seed_id),
|
|
296
|
+
"score": round(float(seed_score) * EXPANSION_DECAY, 6),
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
if len(candidates) >= cap:
|
|
300
|
+
break
|
|
301
|
+
report = {
|
|
302
|
+
"enabled": True,
|
|
303
|
+
"seeds": len(seeds),
|
|
304
|
+
"added": len(candidates),
|
|
305
|
+
"cap": cap,
|
|
306
|
+
"truncated": len(candidates) >= cap,
|
|
307
|
+
"failed_seeds": failures,
|
|
308
|
+
}
|
|
309
|
+
return candidates, report
|
|
310
|
+
|
|
311
|
+
|
|
148
312
|
def fusion_profile(
|
|
149
313
|
query: Any,
|
|
150
314
|
*,
|
|
151
315
|
overrides: Optional[Mapping[str, Mapping[str, float]]] = None,
|
|
316
|
+
strategies: Optional[Mapping[str, str]] = None,
|
|
152
317
|
) -> Dict[str, Any]:
|
|
153
318
|
"""Classify ``query`` and return its resolved fusion weights.
|
|
154
319
|
|
|
155
320
|
Returns ``{"query_class": str, "weights": {keyword, vector, graph},
|
|
156
|
-
"alpha": float}``. ``weights`` feeds the
|
|
157
|
-
``alpha`` feeds the two-channel graph-layer
|
|
321
|
+
"alpha": float, "strategy": "alpha"|"rrf"}``. ``weights`` feeds the
|
|
322
|
+
three-channel service fusion; ``alpha`` feeds the two-channel graph-layer
|
|
323
|
+
fusion; ``strategy`` selects *how* those channels combine and is
|
|
324
|
+
``"alpha"`` for every class unless configured otherwise.
|
|
158
325
|
"""
|
|
159
326
|
query_class = classify_query(query)
|
|
160
327
|
table = fusion_weight_table(overrides)
|
|
@@ -170,14 +337,28 @@ def fusion_profile(
|
|
|
170
337
|
"graph": resolved["graph"],
|
|
171
338
|
},
|
|
172
339
|
"alpha": resolved["alpha"],
|
|
340
|
+
"strategy": fusion_strategy_table(strategies)[query_class],
|
|
173
341
|
}
|
|
174
342
|
|
|
175
343
|
|
|
176
344
|
__all__ = [
|
|
345
|
+
"DEFAULT_EXPANSION_CAP",
|
|
346
|
+
"DEFAULT_EXPANSION_SEEDS",
|
|
347
|
+
"DEFAULT_FUSION_STRATEGY",
|
|
177
348
|
"DEFAULT_FUSION_WEIGHTS",
|
|
349
|
+
"DEFAULT_RRF_K",
|
|
350
|
+
"EXPANSION_DECAY",
|
|
351
|
+
"FUSION_STRATEGIES",
|
|
352
|
+
"FUSION_STRATEGY_ENV",
|
|
178
353
|
"FUSION_WEIGHTS_ENV",
|
|
354
|
+
"GRAPH_EXPANSION_ENV",
|
|
179
355
|
"QUERY_CLASSES",
|
|
180
356
|
"classify_query",
|
|
357
|
+
"expand_with_neighbors",
|
|
181
358
|
"fusion_profile",
|
|
359
|
+
"fusion_strategy_table",
|
|
182
360
|
"fusion_weight_table",
|
|
361
|
+
"graph_expansion_enabled",
|
|
362
|
+
"rrf_fuse",
|
|
363
|
+
"rrf_score",
|
|
183
364
|
]
|