ltcai 8.9.0 → 9.1.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 +81 -58
- package/auto_setup.py +7 -904
- package/desktop/electron/README.md +9 -0
- package/desktop/electron/main.cjs +5 -3
- package/docs/CHANGELOG.md +221 -238
- package/docs/COMMUNITY_AND_PLUGINS.md +3 -2
- package/docs/DEVELOPMENT.md +53 -14
- package/docs/LEGACY_COMPATIBILITY.md +4 -4
- package/docs/ONBOARDING.md +10 -2
- package/docs/OPERATIONS.md +8 -4
- package/docs/PRODUCT_DIRECTION_REVIEW.md +4 -0
- package/docs/ROADMAP_RECOMMENDATIONS.md +61 -36
- package/docs/TRUST_MODEL.md +5 -2
- package/docs/WHY_LATTICE.md +15 -10
- package/docs/WORKFLOW_DESIGNER.md +22 -0
- package/docs/kg-schema.md +13 -2
- package/docs/mcp-tools.md +17 -6
- package/docs/privacy.md +19 -3
- package/docs/public-deploy.md +32 -3
- package/docs/security-model.md +46 -11
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +4 -14
- package/lattice_brain/context.py +66 -9
- package/lattice_brain/embeddings.py +38 -2
- package/lattice_brain/graph/_kg_common.py +27 -462
- package/lattice_brain/graph/_kg_constants.py +243 -0
- package/lattice_brain/graph/_kg_fsutil.py +293 -0
- package/lattice_brain/graph/discovery.py +0 -948
- package/lattice_brain/graph/discovery_index.py +1126 -0
- package/lattice_brain/graph/documents.py +44 -9
- package/lattice_brain/graph/ingest.py +47 -20
- package/lattice_brain/graph/provenance.py +13 -6
- package/lattice_brain/graph/retrieval.py +141 -610
- package/lattice_brain/graph/retrieval_docgen.py +243 -0
- package/lattice_brain/graph/retrieval_vector.py +460 -0
- package/lattice_brain/graph/store.py +6 -0
- package/lattice_brain/ingestion.py +69 -4
- package/lattice_brain/portability.py +29 -23
- package/lattice_brain/quality.py +98 -4
- package/lattice_brain/runtime/agent_runtime.py +169 -7
- package/lattice_brain/runtime/hooks.py +30 -13
- package/lattice_brain/runtime/multi_agent.py +27 -8
- package/lattice_brain/runtime/statuses.py +10 -0
- package/lattice_brain/utils.py +46 -0
- package/lattice_brain/workflow.py +1 -5
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agent_registry.py +10 -8
- package/latticeai/api/agents.py +22 -3
- package/latticeai/api/auth.py +78 -16
- package/latticeai/api/browser.py +303 -34
- package/latticeai/api/chat.py +355 -952
- package/latticeai/api/chat_agent_http.py +356 -0
- package/latticeai/api/chat_contracts.py +54 -0
- package/latticeai/api/chat_documents.py +256 -0
- package/latticeai/api/chat_helpers.py +250 -0
- package/latticeai/api/chat_history.py +99 -0
- package/latticeai/api/chat_intents.py +302 -0
- package/latticeai/api/chat_stream.py +115 -0
- package/latticeai/api/computer_use.py +211 -40
- package/latticeai/api/health.py +9 -3
- package/latticeai/api/hooks.py +17 -6
- package/latticeai/api/knowledge_graph.py +78 -14
- package/latticeai/api/local_files.py +7 -2
- package/latticeai/api/marketplace.py +11 -0
- package/latticeai/api/mcp.py +102 -23
- package/latticeai/api/models.py +72 -33
- package/latticeai/api/network.py +5 -5
- package/latticeai/api/permissions.py +70 -29
- package/latticeai/api/plugins.py +21 -7
- package/latticeai/api/portability.py +5 -5
- package/latticeai/api/realtime.py +33 -5
- package/latticeai/api/setup.py +2 -2
- package/latticeai/api/static_routes.py +123 -24
- package/latticeai/api/tools.py +97 -14
- package/latticeai/api/workflow_designer.py +37 -0
- package/latticeai/api/workspace.py +2 -1
- package/latticeai/app_factory.py +185 -405
- package/latticeai/core/agent.py +19 -9
- package/latticeai/core/agent_registry.py +1 -5
- package/latticeai/core/config.py +23 -3
- package/latticeai/core/context_builder.py +21 -3
- package/latticeai/core/invitations.py +1 -4
- package/latticeai/core/io_utils.py +45 -0
- package/latticeai/core/legacy_compatibility.py +24 -3
- package/latticeai/core/local_embeddings.py +2 -4
- package/latticeai/core/marketplace.py +33 -2
- package/latticeai/core/mcp_catalog.py +450 -0
- package/latticeai/core/mcp_registry.py +2 -441
- package/latticeai/core/plugins.py +15 -0
- package/latticeai/core/policy.py +1 -1
- package/latticeai/core/realtime.py +38 -15
- package/latticeai/core/sessions.py +7 -2
- package/latticeai/core/timeutil.py +10 -0
- package/latticeai/core/tool_registry.py +90 -18
- package/latticeai/core/users.py +5 -14
- package/latticeai/core/workspace_graph_trace.py +31 -5
- package/latticeai/core/workspace_memory.py +3 -1
- package/latticeai/core/workspace_os.py +18 -7
- package/latticeai/core/workspace_os_utils.py +2 -21
- package/latticeai/core/workspace_permissions.py +2 -1
- package/latticeai/core/workspace_plugins.py +1 -1
- package/latticeai/core/workspace_runs.py +14 -5
- package/latticeai/core/workspace_skills.py +1 -1
- package/latticeai/core/workspace_snapshots.py +2 -1
- package/latticeai/core/workspace_timeline.py +2 -1
- package/latticeai/integrations/telegram_bot.py +96 -40
- package/latticeai/models/model_providers.py +111 -0
- package/latticeai/models/router.py +189 -173
- package/latticeai/runtime/access_runtime.py +62 -4
- package/latticeai/runtime/audit_runtime.py +27 -16
- package/latticeai/runtime/automation_runtime.py +22 -7
- package/latticeai/runtime/brain_runtime.py +19 -7
- package/latticeai/runtime/chat_wiring.py +2 -0
- package/latticeai/runtime/config_runtime.py +58 -26
- package/latticeai/runtime/context_runtime.py +16 -4
- package/latticeai/runtime/history_runtime.py +163 -0
- package/latticeai/runtime/hooks_runtime.py +1 -1
- package/latticeai/runtime/model_wiring.py +33 -5
- package/latticeai/runtime/namespace_runtime.py +163 -0
- package/latticeai/runtime/network_config_runtime.py +56 -0
- package/latticeai/runtime/platform_runtime_wiring.py +4 -3
- package/latticeai/runtime/review_wiring.py +1 -1
- package/latticeai/runtime/router_registration.py +34 -1
- package/latticeai/runtime/security_runtime.py +110 -13
- package/latticeai/runtime/sso_config_runtime.py +128 -0
- package/latticeai/runtime/stages.py +27 -0
- package/latticeai/runtime/user_key_runtime.py +106 -0
- package/latticeai/server_app.py +5 -1
- package/latticeai/services/architecture_readiness.py +74 -5
- package/latticeai/services/brain_automation.py +3 -26
- package/latticeai/services/chat_service.py +205 -15
- package/latticeai/services/local_knowledge.py +423 -0
- package/latticeai/services/memory_service.py +268 -21
- package/latticeai/services/model_engines.py +48 -33
- package/latticeai/services/model_errors.py +17 -0
- package/latticeai/services/model_loading.py +28 -25
- package/latticeai/services/model_runtime.py +228 -162
- package/latticeai/services/p_reinforce.py +22 -3
- package/latticeai/services/platform_runtime.py +92 -24
- package/latticeai/services/product_readiness.py +19 -17
- package/latticeai/services/review_queue.py +76 -11
- package/latticeai/services/router_context.py +1 -0
- package/latticeai/services/run_executor.py +25 -9
- package/latticeai/services/search_service.py +203 -28
- package/latticeai/services/setup_detection.py +80 -0
- package/latticeai/services/tool_dispatch.py +28 -5
- package/latticeai/services/triggers.py +53 -4
- package/latticeai/services/upload_service.py +13 -2
- package/latticeai/setup/__init__.py +25 -0
- package/latticeai/setup/auto_setup.py +857 -0
- package/latticeai/setup/wizard.py +1264 -0
- package/latticeai/tools/__init__.py +278 -0
- package/{tools → latticeai/tools}/commands.py +67 -7
- package/{tools → latticeai/tools}/computer.py +1 -1
- package/{tools → latticeai/tools}/documents.py +1 -1
- package/{tools → latticeai/tools}/filesystem.py +3 -3
- package/latticeai/tools/knowledge.py +178 -0
- package/{tools → latticeai/tools}/local_files.py +7 -1
- package/{tools → latticeai/tools}/network.py +0 -1
- package/local_knowledge_api.py +4 -342
- package/package.json +22 -2
- package/scripts/brain_quality_eval.py +3 -1
- package/scripts/bump_version.py +3 -0
- package/scripts/capture_release_evidence.mjs +180 -0
- package/scripts/check_current_release_docs.mjs +142 -0
- package/scripts/check_i18n_literals.mjs +107 -31
- package/scripts/check_openapi_drift.mjs +56 -0
- package/scripts/export_openapi.py +67 -7
- package/scripts/i18n_literal_allowlist.json +22 -24
- package/scripts/run_integration_tests.mjs +72 -10
- package/scripts/validate_release_artifacts.py +49 -7
- package/scripts/wheel_smoke.py +5 -0
- package/setup_wizard.py +3 -1304
- 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-Bzz0bUyW.js +1 -0
- package/static/app/assets/Brain-Dj2J20YA.js +321 -0
- package/static/app/assets/Capture-CqlEl1Ga.js +1 -0
- package/static/app/assets/Library-B03FP1Yx.js +1 -0
- package/static/app/assets/System-80lHW0Ux.js +1 -0
- package/static/app/assets/index-BiMofBTM.js +17 -0
- package/static/app/assets/index-Bmx9rzTc.css +2 -0
- package/static/app/assets/primitives-Q1A96_7v.js +1 -0
- package/static/app/assets/textarea-D13RtnTo.js +1 -0
- package/static/app/index.html +2 -2
- package/static/css/tokens.css +4 -2
- package/static/sw.js +1 -1
- package/tools/__init__.py +21 -269
- package/docs/CODE_REVIEW_2026-07-06.md +0 -764
- package/latticeai/runtime/app_context_runtime.py +0 -13
- package/latticeai/runtime/sso_runtime.py +0 -52
- package/latticeai/runtime/tail_wiring.py +0 -21
- package/scripts/com.pts.claudecode.discord.plist +0 -31
- package/scripts/pts-claudecode-discord-bridge.mjs +0 -207
- package/scripts/start-pts-claudecode-discord.sh +0 -51
- package/static/app/assets/Act-fZokUnC0.js +0 -1
- package/static/app/assets/Brain-DtyuWubr.js +0 -321
- package/static/app/assets/Capture-D5KV3Cu7.js +0 -1
- package/static/app/assets/Library-C9kyFkSt.js +0 -1
- package/static/app/assets/System-VbChmX7r.js +0 -1
- package/static/app/assets/index-DCh5AoXt.css +0 -2
- package/static/app/assets/index-DPdcPoF0.js +0 -17
- package/static/app/assets/primitives-DFeanEV6.js +0 -1
- package/static/app/assets/textarea-CD8UNKIy.js +0 -1
- package/tools/knowledge.py +0 -95
package/docs/security-model.md
CHANGED
|
@@ -22,15 +22,23 @@ Lattice AI는 **개인 AI 워크스페이스**로 설계되었습니다. 기본
|
|
|
22
22
|
|
|
23
23
|
### 세션
|
|
24
24
|
|
|
25
|
-
-
|
|
25
|
+
- `secrets.token_urlsafe`로 생성한 토큰은 SHA-256 해시만
|
|
26
|
+
`~/.ltcai/sessions.json`에 저장(레거시 평문 토큰은 로드 시 자동 마이그레이션)
|
|
26
27
|
- TTL: 24시간 + sliding refresh (활동 시 자동 연장, 15분 단위 디스크 쓰기)
|
|
27
28
|
- 쿠키: `HttpOnly; SameSite=Lax; Path=/`
|
|
29
|
+
- public/non-loopback에서는 `Secure`도 강제(HTTPS 필요)
|
|
28
30
|
- 서버 재시작 후에도 유지 (파일 기반)
|
|
31
|
+
- 삭제되거나 비활성화된 계정의 기존 세션은 다음 요청에서 즉시 거부
|
|
32
|
+
- POSIX에서 데이터 디렉터리 `0700`, atomic JSON/세션 파일 `0600`
|
|
33
|
+
- 초대 게이트 쿠키는 설치별 secret으로 HMAC 서명되고 만료 시각을 포함함.
|
|
34
|
+
`authorized=true` 리터럴은 신뢰하지 않으며, 공용 기본 invite code도 없음
|
|
29
35
|
|
|
30
36
|
### SSO (선택적)
|
|
31
37
|
|
|
32
38
|
- Entra ID / Okta OIDC (`OIDC_DISCOVERY_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`)
|
|
33
39
|
- 콜백 후 내부 세션 토큰으로 변환
|
|
40
|
+
- 초대 게이트 활성화 시 신규 SSO JIT 계정은 서명 초대 권한이 필요하며,
|
|
41
|
+
해당 권한은 서버 측 일회용 state/nonce/PKCE 트랜잭션에 바인딩됨
|
|
34
42
|
- 어드민 핸드오프: `sessionStorage` 1회 읽기 (URL 파라미터 노출 방지)
|
|
35
43
|
|
|
36
44
|
## API 키 보안
|
|
@@ -83,15 +91,27 @@ MAGIC_NUMBERS = {
|
|
|
83
91
|
- **수집 라이프사이클**: 모든 수집은 `IngestionPipeline.ingest` → `dispatch_tool`
|
|
84
92
|
를 거쳐 `pre_tool`/`post_tool` 훅이 발화됩니다. `pre_tool`이 차단하면 수집은
|
|
85
93
|
정직하게 `status="blocked"`로 거부됩니다(권한 게이트·민감정보 가드 적용).
|
|
86
|
-
- **웹 URL 읽기**(`/api/browser/read-url`): `http(s)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
- **웹 URL 읽기**(`/api/browser/read-url`): `http(s)`만 허용하고 DNS의 모든 결과에서
|
|
95
|
+
loopback/private/link-local/multicast/reserved 주소를 거부합니다. 검증한 IP에 연결을
|
|
96
|
+
고정해 DNS rebinding을 막고, redirect마다 재검증하며 환경 proxy를 사용하지
|
|
97
|
+
않습니다. 12초 타임아웃, 스트리밍된 4MB 응답 상한, textual content type만
|
|
98
|
+
처리합니다. 차단/로그인 필요 페이지는 5xx가 아닌 **422로 실패**합니다.
|
|
90
99
|
- **브라우저 탭 수집**(`/api/browser/ingest-current-tab`): payload 정화(스크립트/
|
|
91
100
|
스타일 제거) + 페이로드 크기 상한(413). Manifest V3 확장은 **`127.0.0.1`로만**
|
|
92
101
|
전송하며 클라우드 엔드포인트가 없습니다.
|
|
93
|
-
- **포터빌리티 권한**:
|
|
94
|
-
backup / restore
|
|
102
|
+
- **포터빌리티 권한**: 상태 읽기는 로그인 사용자, 전체 그래프 export/provenance와
|
|
103
|
+
**import / backup / restore는 admin 전용**(`require_admin`)입니다. 그래프는
|
|
104
|
+
머신-전역 자원입니다.
|
|
105
|
+
- **워크스페이스 컨텍스트**: MCP 그래프 호출, 메모리 recall, hybrid search,
|
|
106
|
+
garden-note 컨텍스트, realtime presence는 인증된 사용자와 활성/허용 workspace에
|
|
107
|
+
바인딩됩니다. 에이전트·플러그인 registry 및 graph curation 변경은 admin
|
|
108
|
+
전용이며, MCP 환경 변수 값은 API 응답에 포함되지 않습니다. MCP/플러그인
|
|
109
|
+
실행은 local file/document 도구를 호출할 수 없고 전용 승인 토큰 경로를
|
|
110
|
+
사용해야 합니다. Document RAG와 answer trace도 동일한 workspace 범위로
|
|
111
|
+
검색·저장됩니다.
|
|
112
|
+
- **fail-closed KG 범위**: workspace projection 조회 실패 또는 scope를 알 수 없는
|
|
113
|
+
v2 node는 반환하지 않습니다. legacy-global 데이터는
|
|
114
|
+
`include_legacy_global=True`를 명시한 호환 경로에서만 읽습니다.
|
|
95
115
|
- **복원 무결성**: 백업 아카이브는 `manifest.json`의 sha256과 대조 검증 후에만
|
|
96
116
|
복원되며, 불일치 시 거부됩니다.
|
|
97
117
|
|
|
@@ -99,9 +119,10 @@ MAGIC_NUMBERS = {
|
|
|
99
119
|
|
|
100
120
|
### `run_command()` 위험 플래그 차단
|
|
101
121
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
고정 allowlist(`pwd`, `ls`, `find`, `cat`, `head`, `tail`, `wc`, `rg`)와 정화된
|
|
123
|
+
환경만 사용합니다. Python/Node/npm/npx/sed 같은 일반 인터프리터, 실행 파일 경로,
|
|
124
|
+
절대 경로·`..` traversal·workspace 밖 symlink, `rg --pre`, `find -exec/-delete`
|
|
125
|
+
등은 실행 전에 거부합니다.
|
|
105
126
|
|
|
106
127
|
### `edit_file()` 유일성 검증
|
|
107
128
|
|
|
@@ -119,6 +140,17 @@ MAGIC_NUMBERS = {
|
|
|
119
140
|
- 평문 비밀번호 마이그레이션 이벤트: `password_migrated_from_plaintext`
|
|
120
141
|
- `server.log` 파일에 모든 요청 기록
|
|
121
142
|
|
|
143
|
+
## Telegram 및 권한 알림
|
|
144
|
+
|
|
145
|
+
- Telegram은 `LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS`에 지정된 chat의 메시지와
|
|
146
|
+
callback query만 처리하며, 허용 전에 chat을 등록하지 않습니다.
|
|
147
|
+
- 봇의 로컬 API 호출에는 `LATTICEAI_SERVER_SESSION_TOKEN` 전용 bearer가
|
|
148
|
+
필요합니다. 봇은 `sessions.json`을 스캔하지 않습니다.
|
|
149
|
+
- 권한 요청 알림은 전체 승인 토큰 대신 8자 hint만 포함합니다. 선택적인
|
|
150
|
+
`LATTICEAI_PERMISSION_UI_URL`은 사람의 검토 페이지로 연결하지만 토큰을 URL에
|
|
151
|
+
넣지 않습니다.
|
|
152
|
+
- permission queue는 atomic write와 POSIX `0600`을 사용합니다.
|
|
153
|
+
|
|
122
154
|
## 텔레메트리
|
|
123
155
|
|
|
124
156
|
**없음.** 모든 데이터는 로컬에만 저장됩니다. 외부 서버로 어떠한 사용 데이터도 전송되지 않습니다.
|
|
@@ -128,11 +160,14 @@ MAGIC_NUMBERS = {
|
|
|
128
160
|
## 퍼블릭 배포 체크리스트
|
|
129
161
|
|
|
130
162
|
- [ ] `LATTICEAI_MODE=public`
|
|
131
|
-
- [ ] `
|
|
163
|
+
- [ ] 초대 온보딩을 사용할 때만 `LATTICEAI_INVITE_GATE_ENABLED=true`와 비공개
|
|
164
|
+
`LATTICEAI_INVITE_CODE` 설정(미설정 시 생성되는 설치별 secret 영구 보관)
|
|
132
165
|
- [ ] HTTPS 리버스 프록시 (nginx/Caddy)
|
|
133
166
|
- [ ] `LATTICEAI_ENABLE_GRAPH=false` (필요 시)
|
|
134
167
|
- [ ] `/data` 영구 볼륨 마운트
|
|
135
168
|
- [ ] `LATTICEAI_ALLOW_LOCAL_MODELS=false`
|
|
136
169
|
- [ ] 방화벽에서 4825 포트 직접 노출 차단 (리버스 프록시 통해서만)
|
|
170
|
+
- [ ] Telegram 활성화 시 `LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS`와
|
|
171
|
+
`LATTICEAI_SERVER_SESSION_TOKEN` 설정
|
|
137
172
|
|
|
138
173
|
자세한 내용: [public-deploy.md](public-deploy.md)
|
package/lattice_brain/archive.py
CHANGED
|
@@ -9,7 +9,6 @@ restore on another machine without contacting a service.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import base64
|
|
12
|
-
import hashlib
|
|
13
12
|
import io
|
|
14
13
|
import json
|
|
15
14
|
import os
|
|
@@ -18,7 +17,6 @@ import sqlite3
|
|
|
18
17
|
import tempfile
|
|
19
18
|
import zipfile
|
|
20
19
|
from dataclasses import dataclass
|
|
21
|
-
from datetime import datetime, timezone
|
|
22
20
|
from pathlib import Path, PurePosixPath
|
|
23
21
|
from typing import Any, Dict, Iterable, List, Optional
|
|
24
22
|
|
|
@@ -27,6 +25,8 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
|
27
25
|
from cryptography.exceptions import InvalidTag
|
|
28
26
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
29
27
|
|
|
28
|
+
from .utils import sha256_file as _sha256_file, utc_now_iso as _now
|
|
29
|
+
|
|
30
30
|
|
|
31
31
|
ARCHIVE_FORMAT = "latticebrain.encrypted"
|
|
32
32
|
ARCHIVE_VERSION = 2
|
|
@@ -47,10 +47,6 @@ PORTABLE_DATA_FILES = (
|
|
|
47
47
|
PORTABLE_EXPORT_SUFFIXES = (".json", ".zip")
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def _now() -> str:
|
|
51
|
-
return datetime.now(timezone.utc).isoformat()
|
|
52
|
-
|
|
53
|
-
|
|
54
50
|
def _stamp() -> str:
|
|
55
51
|
return _now().replace(":", "").replace("-", "").replace(".", "")[:15]
|
|
56
52
|
|
|
@@ -68,15 +64,9 @@ def _derive_key(passphrase: str, salt: bytes) -> bytes:
|
|
|
68
64
|
|
|
69
65
|
|
|
70
66
|
def _sha256_bytes(data: bytes) -> str:
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
import hashlib
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
h = hashlib.sha256()
|
|
76
|
-
with open(path, "rb") as fh:
|
|
77
|
-
for block in iter(lambda: fh.read(65536), b""):
|
|
78
|
-
h.update(block)
|
|
79
|
-
return h.hexdigest()
|
|
69
|
+
return hashlib.sha256(data).hexdigest()
|
|
80
70
|
|
|
81
71
|
|
|
82
72
|
def _safe_json(value: Any) -> Any:
|
package/lattice_brain/context.py
CHANGED
|
@@ -13,6 +13,7 @@ design-review amendment T5).
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import inspect
|
|
16
17
|
import logging
|
|
17
18
|
from dataclasses import dataclass, field
|
|
18
19
|
from typing import Any, Callable, Dict, List, Optional
|
|
@@ -23,6 +24,46 @@ def approx_tokens(text: str) -> int:
|
|
|
23
24
|
return max(0, (len(text or "") + 3) // 4)
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _call_context_seam(callback: Callable[..., Any], query: str, **context: Any) -> Any:
|
|
28
|
+
"""Pass identity/scope only when a legacy seam declares support for it.
|
|
29
|
+
|
|
30
|
+
Signature inspection preserves old one-argument adapters without catching
|
|
31
|
+
a callback's own ``TypeError`` and retrying it with less restrictive scope.
|
|
32
|
+
If a callable cannot be inspected, the secure behavior is to pass every
|
|
33
|
+
context field and let the caller's failure isolation omit that section.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
parameters = inspect.signature(callback).parameters
|
|
37
|
+
except (TypeError, ValueError):
|
|
38
|
+
return callback(query, **context)
|
|
39
|
+
accepts_kwargs = any(
|
|
40
|
+
item.kind is inspect.Parameter.VAR_KEYWORD
|
|
41
|
+
for item in parameters.values()
|
|
42
|
+
)
|
|
43
|
+
supported = context if accepts_kwargs else {
|
|
44
|
+
key: value for key, value in context.items()
|
|
45
|
+
if key in parameters
|
|
46
|
+
}
|
|
47
|
+
return callback(query, **supported)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _call_keyword_seam(callback: Callable[..., Any], **context: Any) -> Any:
|
|
51
|
+
"""Invoke a keyword-only seam while preserving legacy narrow signatures."""
|
|
52
|
+
try:
|
|
53
|
+
parameters = inspect.signature(callback).parameters
|
|
54
|
+
except (TypeError, ValueError):
|
|
55
|
+
return callback(**context)
|
|
56
|
+
accepts_kwargs = any(
|
|
57
|
+
item.kind is inspect.Parameter.VAR_KEYWORD
|
|
58
|
+
for item in parameters.values()
|
|
59
|
+
)
|
|
60
|
+
supported = context if accepts_kwargs else {
|
|
61
|
+
key: value for key, value in context.items()
|
|
62
|
+
if key in parameters
|
|
63
|
+
}
|
|
64
|
+
return callback(**supported)
|
|
65
|
+
|
|
66
|
+
|
|
26
67
|
@dataclass
|
|
27
68
|
class ContextSection:
|
|
28
69
|
name: str
|
|
@@ -112,11 +153,11 @@ class ContextAssembler:
|
|
|
112
153
|
if self._memory_recall is not None:
|
|
113
154
|
sections.append(self._memories_section(query, user_email, workspace_id, memory_limit))
|
|
114
155
|
if self._hybrid_search is not None:
|
|
115
|
-
sections.append(self._knowledge_section(query, knowledge_limit, user_email))
|
|
156
|
+
sections.append(self._knowledge_section(query, knowledge_limit, user_email, workspace_id))
|
|
116
157
|
if self._notes_context is not None:
|
|
117
|
-
sections.append(self._notes_section(query))
|
|
158
|
+
sections.append(self._notes_section(query, user_email, workspace_id))
|
|
118
159
|
if self._recent_chat is not None:
|
|
119
|
-
sections.append(self._recent_section(user_email, conversation_id))
|
|
160
|
+
sections.append(self._recent_section(user_email, conversation_id, workspace_id))
|
|
120
161
|
|
|
121
162
|
sections = [s for s in sections if s.content.strip()]
|
|
122
163
|
self._apply_budget(sections, budget)
|
|
@@ -141,9 +182,15 @@ class ContextAssembler:
|
|
|
141
182
|
],
|
|
142
183
|
)
|
|
143
184
|
|
|
144
|
-
def _knowledge_section(self, query, limit, user_email=None) -> ContextSection:
|
|
185
|
+
def _knowledge_section(self, query, limit, user_email=None, workspace_id=None) -> ContextSection:
|
|
145
186
|
try:
|
|
146
|
-
hybrid =
|
|
187
|
+
hybrid = _call_context_seam(
|
|
188
|
+
self._hybrid_search,
|
|
189
|
+
query,
|
|
190
|
+
limit=limit,
|
|
191
|
+
user_email=user_email,
|
|
192
|
+
workspace_id=workspace_id,
|
|
193
|
+
)
|
|
147
194
|
matches = hybrid.get("matches", [])[:limit]
|
|
148
195
|
except Exception as exc:
|
|
149
196
|
logging.debug("context: hybrid search failed: %s", exc)
|
|
@@ -166,9 +213,14 @@ class ContextAssembler:
|
|
|
166
213
|
provenance=provenance,
|
|
167
214
|
)
|
|
168
215
|
|
|
169
|
-
def _notes_section(self, query) -> ContextSection:
|
|
216
|
+
def _notes_section(self, query, user_email=None, workspace_id=None) -> ContextSection:
|
|
170
217
|
try:
|
|
171
|
-
content =
|
|
218
|
+
content = _call_context_seam(
|
|
219
|
+
self._notes_context,
|
|
220
|
+
query,
|
|
221
|
+
user_email=user_email,
|
|
222
|
+
workspace_id=workspace_id,
|
|
223
|
+
) or ""
|
|
172
224
|
except Exception as exc:
|
|
173
225
|
logging.debug("context: notes context failed: %s", exc)
|
|
174
226
|
content = ""
|
|
@@ -179,9 +231,14 @@ class ContextAssembler:
|
|
|
179
231
|
provenance=[{"source": "garden", "included": bool(content)}],
|
|
180
232
|
)
|
|
181
233
|
|
|
182
|
-
def _recent_section(self, user_email, conversation_id) -> ContextSection:
|
|
234
|
+
def _recent_section(self, user_email, conversation_id, workspace_id=None) -> ContextSection:
|
|
183
235
|
try:
|
|
184
|
-
content =
|
|
236
|
+
content = _call_keyword_seam(
|
|
237
|
+
self._recent_chat,
|
|
238
|
+
user_email=user_email,
|
|
239
|
+
conversation_id=conversation_id,
|
|
240
|
+
workspace_id=workspace_id,
|
|
241
|
+
) or ""
|
|
185
242
|
except Exception as exc:
|
|
186
243
|
logging.debug("context: recent chat failed: %s", exc)
|
|
187
244
|
content = ""
|
|
@@ -8,7 +8,7 @@ import os
|
|
|
8
8
|
import re
|
|
9
9
|
import struct
|
|
10
10
|
from dataclasses import dataclass
|
|
11
|
-
from typing import Iterable, List
|
|
11
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
DEFAULT_EMBEDDING_DIM = int(os.getenv("LATTICEAI_VECTOR_DIM", "384"))
|
|
@@ -87,4 +87,40 @@ class LocalEmbeddingModel:
|
|
|
87
87
|
return list(struct.unpack(f"<{count}f", payload[: count * 4]))
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
# --- Large candidate #2 slice: multimodal (vision) stubs ---
|
|
91
|
+
# Schema already defines IMAGE / IMAGE_TEXT / CONTAINS_IMAGE.
|
|
92
|
+
# These stubs allow ingestion + retrieval paths to carry image signals without
|
|
93
|
+
# requiring heavy deps at core. Real impl can swap in local vision (e.g. via
|
|
94
|
+
# ollama vision or onnx CLIP) behind the same interface.
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class VisionStub:
|
|
97
|
+
"""Offline vision describe + embed stubs for multimodal Brain.
|
|
98
|
+
|
|
99
|
+
describe: returns a short textual caption derived from metadata/filename.
|
|
100
|
+
embed: produces a vector from image meta (size+format) + optional caption hash.
|
|
101
|
+
Later: replace with real embedding model that accepts image bytes/path.
|
|
102
|
+
"""
|
|
103
|
+
dim: int = DEFAULT_EMBEDDING_DIM
|
|
104
|
+
|
|
105
|
+
def describe(self, path: str | None = None, meta: Optional[Dict[str, Any]] = None) -> str:
|
|
106
|
+
meta = meta or {}
|
|
107
|
+
w = meta.get("width") or meta.get("w") or "?"
|
|
108
|
+
h = meta.get("height") or meta.get("h") or "?"
|
|
109
|
+
fmt = meta.get("format") or meta.get("ext") or "img"
|
|
110
|
+
name = (path or "").split("/")[-1] or "image"
|
|
111
|
+
# deterministic caption stub (no external call)
|
|
112
|
+
return f"Image {name} ({fmt} {w}x{h})"
|
|
113
|
+
|
|
114
|
+
def embed_image(self, path: str | None = None, meta: Optional[Dict[str, Any]] = None, caption: str = "") -> List[float]:
|
|
115
|
+
meta = meta or {}
|
|
116
|
+
basis = f"{path or ''}|{meta.get('width',0)}x{meta.get('height',0)}|{meta.get('format','')}|{caption[:120]}"
|
|
117
|
+
# reuse text embedder for determinism (image content hash would be better with real vision)
|
|
118
|
+
model = LocalEmbeddingModel(dim=self.dim)
|
|
119
|
+
return model.embed(basis)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_vision_embedder(dim: int | None = None) -> VisionStub:
|
|
123
|
+
return VisionStub(dim=dim or DEFAULT_EMBEDDING_DIM)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ["DEFAULT_EMBEDDING_DIM", "EMBEDDING_MODEL_ID", "LocalEmbeddingModel", "embedding_model_id", "VisionStub", "get_vision_embedder"]
|