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
|
@@ -16,11 +16,11 @@ import shutil
|
|
|
16
16
|
import time
|
|
17
17
|
import urllib.error
|
|
18
18
|
import urllib.request
|
|
19
|
-
import
|
|
19
|
+
from dataclasses import dataclass, field, fields
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import AsyncIterator, Dict, List, Optional
|
|
21
|
+
from typing import Any, AsyncIterator, Callable, Dict, List, Optional
|
|
22
22
|
|
|
23
|
-
from
|
|
23
|
+
from .model_errors import ModelRuntimeError
|
|
24
24
|
|
|
25
25
|
from latticeai.models.router import (
|
|
26
26
|
AsyncOpenAI,
|
|
@@ -66,111 +66,68 @@ _MODEL_LOADING_COMPAT_EXPORTS = (
|
|
|
66
66
|
)
|
|
67
67
|
|
|
68
68
|
|
|
69
|
-
def _missing_current_user(_request:
|
|
69
|
+
def _missing_current_user(_request: Any) -> Optional[str]:
|
|
70
70
|
return None
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def _missing_user_api_key(_email: Optional[str], _provider: str) -> Optional[str]:
|
|
74
74
|
return None
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
77
78
|
class ModelRuntimeState:
|
|
78
|
-
"""
|
|
79
|
-
|
|
79
|
+
"""Immutable application-owned dependencies for one model runtime.
|
|
80
|
+
|
|
81
|
+
Upper-case configuration field names intentionally match the long-standing
|
|
82
|
+
composition-root vocabulary. Unlike the former module ``STATE`` object,
|
|
83
|
+
instances are explicit, immutable, and safe to create more than once in a
|
|
84
|
+
process (for example in isolated tests or multiple ASGI applications).
|
|
80
85
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"""Internal no-warning sync used at init."""
|
|
122
|
-
global router, APP_MODE, DEFAULT_HOST, DEFAULT_PORT, DATA_DIR, BASE_DIR
|
|
123
|
-
global ENABLE_TELEGRAM, ENABLE_GRAPH, AUTOLOAD_MODELS, MODEL_IDLE_UNLOAD_SECONDS
|
|
124
|
-
global ALLOW_MODEL_DOWNLOADS, MODEL_DOWNLOAD_TIMEOUT, ALLOW_LOCAL_MODELS, REQUIRE_AUTH
|
|
125
|
-
global INVITE_GATE_ENABLED, ALLOW_PLAINTEXT_API_KEYS
|
|
126
|
-
global CORS_ALLOW_NETWORK, PUBLIC_MODEL, LOCAL_MODEL, IS_PUBLIC_MODE
|
|
127
|
-
global keyring, get_current_user, get_user_api_key
|
|
128
|
-
router = self.router
|
|
129
|
-
APP_MODE = self.APP_MODE
|
|
130
|
-
DEFAULT_HOST = self.DEFAULT_HOST
|
|
131
|
-
DEFAULT_PORT = self.DEFAULT_PORT
|
|
132
|
-
DATA_DIR = self.DATA_DIR
|
|
133
|
-
BASE_DIR = self.BASE_DIR
|
|
134
|
-
ENABLE_TELEGRAM = self.ENABLE_TELEGRAM
|
|
135
|
-
ENABLE_GRAPH = self.ENABLE_GRAPH
|
|
136
|
-
AUTOLOAD_MODELS = self.AUTOLOAD_MODELS
|
|
137
|
-
MODEL_IDLE_UNLOAD_SECONDS = self.MODEL_IDLE_UNLOAD_SECONDS
|
|
138
|
-
ALLOW_MODEL_DOWNLOADS = self.ALLOW_MODEL_DOWNLOADS
|
|
139
|
-
MODEL_DOWNLOAD_TIMEOUT = self.MODEL_DOWNLOAD_TIMEOUT
|
|
140
|
-
ALLOW_LOCAL_MODELS = self.ALLOW_LOCAL_MODELS
|
|
141
|
-
REQUIRE_AUTH = self.REQUIRE_AUTH
|
|
142
|
-
INVITE_GATE_ENABLED = self.INVITE_GATE_ENABLED
|
|
143
|
-
ALLOW_PLAINTEXT_API_KEYS = self.ALLOW_PLAINTEXT_API_KEYS
|
|
144
|
-
CORS_ALLOW_NETWORK = self.CORS_ALLOW_NETWORK
|
|
145
|
-
PUBLIC_MODEL = self.PUBLIC_MODEL
|
|
146
|
-
LOCAL_MODEL = self.LOCAL_MODEL
|
|
147
|
-
IS_PUBLIC_MODE = self.IS_PUBLIC_MODE
|
|
148
|
-
keyring = self.keyring
|
|
149
|
-
get_current_user = self.get_current_user
|
|
150
|
-
get_user_api_key = self.get_user_api_key
|
|
151
|
-
|
|
152
|
-
STATE = ModelRuntimeState()
|
|
153
|
-
STATE._sync_globals() # initial no-warning; public API warns on explicit legacy syncs
|
|
154
|
-
|
|
155
|
-
# Configured by server_app.configure_model_runtime during app assembly.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _env_bool(key: str, default: bool = False) -> bool:
|
|
159
|
-
raw = os.getenv(key)
|
|
160
|
-
if raw is None:
|
|
161
|
-
return default
|
|
162
|
-
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _download_allowed(allow_download: bool = False) -> bool:
|
|
166
|
-
# Prefer STATE (the source of truth) over bare module global for internal logic.
|
|
167
|
-
autoload = getattr(STATE, "AUTOLOAD_MODELS", AUTOLOAD_MODELS)
|
|
168
|
-
configured = getattr(STATE, "ALLOW_MODEL_DOWNLOADS", ALLOW_MODEL_DOWNLOADS)
|
|
86
|
+
|
|
87
|
+
router: Any = None
|
|
88
|
+
APP_MODE: str = "local"
|
|
89
|
+
DEFAULT_HOST: str = "127.0.0.1"
|
|
90
|
+
DEFAULT_PORT: int = 4825
|
|
91
|
+
DATA_DIR: Path = field(default_factory=lambda: Path.home() / ".latticeai")
|
|
92
|
+
BASE_DIR: Path = field(default_factory=Path.cwd)
|
|
93
|
+
ENABLE_TELEGRAM: bool = False
|
|
94
|
+
ENABLE_GRAPH: bool = True
|
|
95
|
+
AUTOLOAD_MODELS: bool = False
|
|
96
|
+
MODEL_IDLE_UNLOAD_SECONDS: int = 0
|
|
97
|
+
ALLOW_MODEL_DOWNLOADS: bool = False
|
|
98
|
+
MODEL_DOWNLOAD_TIMEOUT: int = 300
|
|
99
|
+
ALLOW_LOCAL_MODELS: bool = True
|
|
100
|
+
REQUIRE_AUTH: bool = False
|
|
101
|
+
INVITE_GATE_ENABLED: bool = False
|
|
102
|
+
ALLOW_PLAINTEXT_API_KEYS: bool = False
|
|
103
|
+
CORS_ALLOW_NETWORK: bool = False
|
|
104
|
+
PUBLIC_MODEL: str = "openai:gpt-4o-mini"
|
|
105
|
+
LOCAL_MODEL: str = "mlx-community/gemma-4-12b-it-4bit"
|
|
106
|
+
IS_PUBLIC_MODE: bool = False
|
|
107
|
+
keyring: Any = None
|
|
108
|
+
get_current_user: Callable[[Any], Optional[str]] = _missing_current_user
|
|
109
|
+
get_user_api_key: Callable[[Optional[str], str], Optional[str]] = _missing_user_api_key
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def create_model_runtime_state(**deps: Any) -> ModelRuntimeState:
|
|
113
|
+
"""Create an immutable runtime dependency set with strict key validation."""
|
|
114
|
+
|
|
115
|
+
known = {item.name for item in fields(ModelRuntimeState)}
|
|
116
|
+
unknown = sorted(set(deps) - known)
|
|
117
|
+
if unknown:
|
|
118
|
+
raise TypeError(f"unknown model runtime dependencies: {', '.join(unknown)}")
|
|
119
|
+
return ModelRuntimeState(**deps)
|
|
120
|
+
|
|
121
|
+
def _download_allowed(
|
|
122
|
+
allow_download: bool = False, *, state: ModelRuntimeState
|
|
123
|
+
) -> bool:
|
|
124
|
+
autoload = state.AUTOLOAD_MODELS
|
|
125
|
+
configured = state.ALLOW_MODEL_DOWNLOADS
|
|
169
126
|
return bool(allow_download) or bool(configured) or bool(autoload)
|
|
170
127
|
|
|
171
128
|
|
|
172
129
|
def _download_block(provider: str, model_name: str) -> None:
|
|
173
|
-
raise
|
|
130
|
+
raise ModelRuntimeError(
|
|
174
131
|
status_code=409,
|
|
175
132
|
detail={
|
|
176
133
|
"status": "unavailable",
|
|
@@ -188,7 +145,7 @@ def _download_block(provider: str, model_name: str) -> None:
|
|
|
188
145
|
|
|
189
146
|
|
|
190
147
|
def _engine_install_block(engine: str) -> None:
|
|
191
|
-
raise
|
|
148
|
+
raise ModelRuntimeError(
|
|
192
149
|
status_code=409,
|
|
193
150
|
detail={
|
|
194
151
|
"status": "unavailable",
|
|
@@ -203,33 +160,15 @@ def _engine_install_block(engine: str) -> None:
|
|
|
203
160
|
)
|
|
204
161
|
|
|
205
162
|
|
|
206
|
-
def
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _missing_user_api_key(_email: Optional[str], _provider: str) -> Optional[str]:
|
|
211
|
-
return None
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def configure_model_runtime(**deps) -> None:
|
|
215
|
-
"""Wire app-owned runtime dependencies without importing server_app.
|
|
163
|
+
def configure_model_runtime(**deps: Any) -> "ModelRuntimeService":
|
|
164
|
+
"""Compatibility factory returning an isolated, bound runtime service.
|
|
216
165
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Now uses STATE class for clean decomp.
|
|
166
|
+
The historical function mutated process-wide module globals. Keeping the
|
|
167
|
+
import path while returning a service preserves practical construction
|
|
168
|
+
compatibility without ambient state or cross-application leakage.
|
|
221
169
|
"""
|
|
222
|
-
for key, value in deps.items():
|
|
223
|
-
if hasattr(STATE, key):
|
|
224
|
-
setattr(STATE, key, value)
|
|
225
|
-
elif key == "keyring":
|
|
226
|
-
STATE.keyring = value
|
|
227
|
-
elif key == "get_current_user":
|
|
228
|
-
STATE.get_current_user = value
|
|
229
|
-
elif key == "get_user_api_key":
|
|
230
|
-
STATE.get_user_api_key = value
|
|
231
170
|
|
|
232
|
-
|
|
171
|
+
return ModelRuntimeService(create_model_runtime_state(**deps))
|
|
233
172
|
|
|
234
173
|
|
|
235
174
|
# Catalog data + version-dedup helpers live in ``model_catalog``; re-exported
|
|
@@ -394,9 +333,9 @@ def ensure_lmstudio_model(model_name: str) -> Dict[str, object]:
|
|
|
394
333
|
)
|
|
395
334
|
except urllib.error.HTTPError as e:
|
|
396
335
|
detail = e.read().decode("utf-8", errors="replace")[-2000:]
|
|
397
|
-
raise
|
|
336
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {detail or e.reason}")
|
|
398
337
|
except Exception as e:
|
|
399
|
-
raise
|
|
338
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {e}")
|
|
400
339
|
|
|
401
340
|
status = str(job.get("status") or "")
|
|
402
341
|
job_id = str(job.get("job_id") or "")
|
|
@@ -412,10 +351,10 @@ def ensure_lmstudio_model(model_name: str) -> Dict[str, object]:
|
|
|
412
351
|
if polled_status == "completed":
|
|
413
352
|
break
|
|
414
353
|
if polled_status == "failed":
|
|
415
|
-
raise
|
|
354
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 다운로드 실패: {polled}")
|
|
416
355
|
time.sleep(2)
|
|
417
356
|
else:
|
|
418
|
-
raise
|
|
357
|
+
raise ModelRuntimeError(status_code=408, detail="LM Studio 모델 다운로드 시간이 초과되었습니다.")
|
|
419
358
|
|
|
420
359
|
models = get_lmstudio_models(force=True)
|
|
421
360
|
model_key = _find_lmstudio_model_key(model_name, models) or model_name
|
|
@@ -435,12 +374,12 @@ def ensure_lmstudio_model(model_name: str) -> Dict[str, object]:
|
|
|
435
374
|
)
|
|
436
375
|
except urllib.error.HTTPError as e:
|
|
437
376
|
detail = e.read().decode("utf-8", errors="replace")[-2000:]
|
|
438
|
-
raise
|
|
377
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {detail or e.reason}")
|
|
439
378
|
except Exception as e:
|
|
440
|
-
raise
|
|
379
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {e}")
|
|
441
380
|
|
|
442
381
|
if str(loaded.get("status") or "") != "loaded":
|
|
443
|
-
raise
|
|
382
|
+
raise ModelRuntimeError(status_code=500, detail=f"LM Studio 모델 로드 실패: {loaded}")
|
|
444
383
|
|
|
445
384
|
return {
|
|
446
385
|
"provider": "lmstudio",
|
|
@@ -545,7 +484,7 @@ def download_hf_model(
|
|
|
545
484
|
progress_emit=None,
|
|
546
485
|
) -> Dict[str, object]:
|
|
547
486
|
if importlib.util.find_spec("huggingface_hub") is None:
|
|
548
|
-
raise
|
|
487
|
+
raise ModelRuntimeError(status_code=400, detail="huggingface_hub가 없습니다. 먼저 MLX runtime 설치를 진행해 주세요.")
|
|
549
488
|
|
|
550
489
|
target_dir = hf_model_dir(repo_id)
|
|
551
490
|
if hf_model_ready(repo_id, provider):
|
|
@@ -697,10 +636,10 @@ def download_hf_model(
|
|
|
697
636
|
eta_seconds=0,
|
|
698
637
|
))
|
|
699
638
|
except Exception as e:
|
|
700
|
-
raise
|
|
639
|
+
raise ModelRuntimeError(status_code=500, detail=f"{repo_id} 다운로드 실패: {str(e)[-2000:]}")
|
|
701
640
|
|
|
702
641
|
if not hf_model_ready(repo_id, provider):
|
|
703
|
-
raise
|
|
642
|
+
raise ModelRuntimeError(status_code=500, detail=f"{repo_id} 다운로드가 완료되지 않았습니다. 모델 파일을 찾지 못했습니다.")
|
|
704
643
|
|
|
705
644
|
return {"model": repo_id, "path": str(target_dir), "cached": False}
|
|
706
645
|
|
|
@@ -733,9 +672,13 @@ def ensure_llamacpp_server(model_name: str) -> None:
|
|
|
733
672
|
return _ensure_llamacpp_server(model_name)
|
|
734
673
|
|
|
735
674
|
|
|
736
|
-
def _safe_engine_install_plan(
|
|
675
|
+
def _safe_engine_install_plan(
|
|
676
|
+
engine: str,
|
|
677
|
+
*,
|
|
678
|
+
base_dir: Path,
|
|
679
|
+
) -> Optional[Dict[str, object]]:
|
|
737
680
|
try:
|
|
738
|
-
return _engine_install_plan(engine)
|
|
681
|
+
return _engine_install_plan(engine, base_dir=base_dir)
|
|
739
682
|
except Exception:
|
|
740
683
|
return None
|
|
741
684
|
|
|
@@ -758,8 +701,13 @@ def engine_installed(engine: str) -> bool:
|
|
|
758
701
|
return AsyncOpenAI is not None
|
|
759
702
|
return False
|
|
760
703
|
|
|
761
|
-
def engine_status(
|
|
762
|
-
|
|
704
|
+
def engine_status(
|
|
705
|
+
*,
|
|
706
|
+
state: ModelRuntimeState,
|
|
707
|
+
cloud_verify_cache: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
708
|
+
) -> List[Dict]:
|
|
709
|
+
r = state.router
|
|
710
|
+
verify_cache = cloud_verify_cache or {}
|
|
763
711
|
cloud_models = r.detected_cloud_models() if r else []
|
|
764
712
|
cloud_by_provider = {}
|
|
765
713
|
for model in cloud_models:
|
|
@@ -861,7 +809,7 @@ def engine_status() -> List[Dict]:
|
|
|
861
809
|
"installed": engine_installed("local_mlx"),
|
|
862
810
|
"installable": True,
|
|
863
811
|
"install_label": ENGINE_INSTALLERS["local_mlx"]["label"],
|
|
864
|
-
"install_plan": _safe_engine_install_plan("local_mlx"),
|
|
812
|
+
"install_plan": _safe_engine_install_plan("local_mlx", base_dir=state.BASE_DIR),
|
|
865
813
|
"models": mlx_models,
|
|
866
814
|
},
|
|
867
815
|
{
|
|
@@ -872,7 +820,7 @@ def engine_status() -> List[Dict]:
|
|
|
872
820
|
"installed": ollama_installed,
|
|
873
821
|
"installable": True,
|
|
874
822
|
"install_label": ENGINE_INSTALLERS["ollama"]["label"],
|
|
875
|
-
"install_plan": _safe_engine_install_plan("ollama"),
|
|
823
|
+
"install_plan": _safe_engine_install_plan("ollama", base_dir=state.BASE_DIR),
|
|
876
824
|
"models": ollama_models,
|
|
877
825
|
},
|
|
878
826
|
]
|
|
@@ -888,7 +836,11 @@ def engine_status() -> List[Dict]:
|
|
|
888
836
|
"support_reason": support["reason"],
|
|
889
837
|
"installable": support["supported"] and spec["id"] in ENGINE_INSTALLERS,
|
|
890
838
|
"install_label": ENGINE_INSTALLERS.get(spec["id"], {}).get("label"),
|
|
891
|
-
"install_plan":
|
|
839
|
+
"install_plan": (
|
|
840
|
+
_safe_engine_install_plan(spec["id"], base_dir=state.BASE_DIR)
|
|
841
|
+
if spec["id"] in ENGINE_INSTALLERS
|
|
842
|
+
else None
|
|
843
|
+
),
|
|
892
844
|
"requires": spec["requires"],
|
|
893
845
|
"models": (
|
|
894
846
|
vllm_models if spec["id"] == "vllm"
|
|
@@ -903,7 +855,7 @@ def engine_status() -> List[Dict]:
|
|
|
903
855
|
env_key = next((item.get("requires") for item in cloud_by_provider.get(provider, []) if item.get("requires")), None)
|
|
904
856
|
provider_models = []
|
|
905
857
|
for model in cloud_by_provider.get(provider, []):
|
|
906
|
-
cache =
|
|
858
|
+
cache = verify_cache.get(model.get("id"))
|
|
907
859
|
provider_models.append({
|
|
908
860
|
**model,
|
|
909
861
|
"verified": cache.get("ok") if cache else None,
|
|
@@ -917,17 +869,15 @@ def engine_status() -> List[Dict]:
|
|
|
917
869
|
"installed": engine_installed(provider),
|
|
918
870
|
"installable": True,
|
|
919
871
|
"install_label": ENGINE_INSTALLERS[provider]["label"],
|
|
920
|
-
"install_plan": _safe_engine_install_plan(provider),
|
|
872
|
+
"install_plan": _safe_engine_install_plan(provider, base_dir=state.BASE_DIR),
|
|
921
873
|
"requires": env_key,
|
|
922
874
|
"models": provider_models,
|
|
923
875
|
})
|
|
924
876
|
return engines
|
|
925
877
|
|
|
926
|
-
def runtime_features() -> Dict:
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
s = STATE
|
|
930
|
-
r = getattr(s, "router", None) or router
|
|
878
|
+
def runtime_features(*, state: ModelRuntimeState) -> Dict:
|
|
879
|
+
s = state
|
|
880
|
+
r = s.router
|
|
931
881
|
return {
|
|
932
882
|
"mode": s.APP_MODE,
|
|
933
883
|
"public": s.IS_PUBLIC_MODE,
|
|
@@ -964,8 +914,17 @@ def runtime_features() -> Dict:
|
|
|
964
914
|
},
|
|
965
915
|
}
|
|
966
916
|
|
|
967
|
-
def install_engine(
|
|
968
|
-
|
|
917
|
+
def install_engine(
|
|
918
|
+
engine: str,
|
|
919
|
+
confirmation_token: Optional[str] = None,
|
|
920
|
+
*,
|
|
921
|
+
state: ModelRuntimeState,
|
|
922
|
+
) -> Dict:
|
|
923
|
+
return _install_engine(
|
|
924
|
+
engine,
|
|
925
|
+
confirmation_token=confirmation_token,
|
|
926
|
+
base_dir=state.BASE_DIR,
|
|
927
|
+
)
|
|
969
928
|
|
|
970
929
|
|
|
971
930
|
def _resolve_model_alias(model_id: str, engine: Optional[str] = None) -> str:
|
|
@@ -999,13 +958,13 @@ def normalize_local_model_request(model_id: str, engine: Optional[str] = None) -
|
|
|
999
958
|
return model_id
|
|
1000
959
|
|
|
1001
960
|
|
|
1002
|
-
def ensure_engine_ready(engine: str) -> Dict[str, object]:
|
|
961
|
+
def ensure_engine_ready(engine: str, *, state: ModelRuntimeState) -> Dict[str, object]:
|
|
1003
962
|
engine = "local_mlx" if engine == "mlx" else engine
|
|
1004
963
|
if engine not in ENGINE_INSTALLERS and engine not in OPENAI_COMPATIBLE_PROVIDERS:
|
|
1005
|
-
raise
|
|
964
|
+
raise ModelRuntimeError(status_code=400, detail=f"지원하지 않는 엔진입니다: {engine}")
|
|
1006
965
|
support = engine_support_status(engine)
|
|
1007
966
|
if not support["supported"]:
|
|
1008
|
-
raise
|
|
967
|
+
raise ModelRuntimeError(status_code=400, detail=str(support["reason"]))
|
|
1009
968
|
|
|
1010
969
|
if engine_installed(engine):
|
|
1011
970
|
if engine == "local_mlx":
|
|
@@ -1013,12 +972,12 @@ def ensure_engine_ready(engine: str) -> Dict[str, object]:
|
|
|
1013
972
|
return {"engine": engine, "installed": True, "installed_now": False}
|
|
1014
973
|
|
|
1015
974
|
if engine not in ENGINE_INSTALLERS:
|
|
1016
|
-
raise
|
|
975
|
+
raise ModelRuntimeError(status_code=400, detail=f"{engine} 엔진 설치 방법이 등록되어 있지 않습니다.")
|
|
1017
976
|
|
|
1018
|
-
result = install_engine(engine)
|
|
977
|
+
result = install_engine(engine, state=state)
|
|
1019
978
|
if result.get("returncode") not in (0, None) or not engine_installed(engine):
|
|
1020
979
|
detail = result.get("stderr") or result.get("stdout") or f"{engine} 설치에 실패했습니다."
|
|
1021
|
-
raise
|
|
980
|
+
raise ModelRuntimeError(status_code=500, detail=str(detail)[-2000:])
|
|
1022
981
|
|
|
1023
982
|
if engine == "local_mlx":
|
|
1024
983
|
ensure_mlx_runtime()
|
|
@@ -1054,20 +1013,27 @@ async def _smoke_test_loaded_model(
|
|
|
1054
1013
|
resolution: _ModelResolution,
|
|
1055
1014
|
*,
|
|
1056
1015
|
api_key_override: Optional[str] = None,
|
|
1016
|
+
state: ModelRuntimeState,
|
|
1057
1017
|
) -> Dict[str, object]:
|
|
1058
1018
|
# Delegated to model_engines for server decomp
|
|
1059
1019
|
from .model_engines import _smoke_test_loaded_model as _impl_smoke
|
|
1060
|
-
return await _impl_smoke(
|
|
1020
|
+
return await _impl_smoke(
|
|
1021
|
+
resolution,
|
|
1022
|
+
api_key_override=api_key_override,
|
|
1023
|
+
model_router=state.router,
|
|
1024
|
+
)
|
|
1061
1025
|
|
|
1062
1026
|
|
|
1063
1027
|
async def prepare_and_load_model(
|
|
1064
1028
|
model_id: str,
|
|
1065
|
-
request:
|
|
1029
|
+
request: Any,
|
|
1066
1030
|
engine: Optional[str] = None,
|
|
1067
1031
|
user_email: Optional[str] = None,
|
|
1068
1032
|
adapter_path: Optional[str] = None,
|
|
1069
1033
|
draft_model_id: Optional[str] = None,
|
|
1070
1034
|
allow_download: bool = False,
|
|
1035
|
+
*,
|
|
1036
|
+
state: ModelRuntimeState,
|
|
1071
1037
|
) -> Dict[str, object]:
|
|
1072
1038
|
from .model_loading import prepare_and_load_model as _impl
|
|
1073
1039
|
|
|
@@ -1079,6 +1045,7 @@ async def prepare_and_load_model(
|
|
|
1079
1045
|
adapter_path=adapter_path,
|
|
1080
1046
|
draft_model_id=draft_model_id,
|
|
1081
1047
|
allow_download=allow_download,
|
|
1048
|
+
runtime_state=state,
|
|
1082
1049
|
)
|
|
1083
1050
|
|
|
1084
1051
|
|
|
@@ -1088,10 +1055,12 @@ def sse_event(event: str, data: Dict[str, object]) -> str:
|
|
|
1088
1055
|
|
|
1089
1056
|
async def prepare_and_load_model_stream(
|
|
1090
1057
|
model_id: str,
|
|
1091
|
-
request:
|
|
1058
|
+
request: Any,
|
|
1092
1059
|
engine: Optional[str] = None,
|
|
1093
1060
|
user_email: Optional[str] = None,
|
|
1094
1061
|
allow_download: bool = False,
|
|
1062
|
+
*,
|
|
1063
|
+
state: ModelRuntimeState,
|
|
1095
1064
|
) -> AsyncIterator[str]:
|
|
1096
1065
|
from .model_loading import prepare_and_load_model_stream as _impl
|
|
1097
1066
|
|
|
@@ -1101,11 +1070,11 @@ async def prepare_and_load_model_stream(
|
|
|
1101
1070
|
engine=engine,
|
|
1102
1071
|
user_email=user_email,
|
|
1103
1072
|
allow_download=allow_download,
|
|
1073
|
+
runtime_state=state,
|
|
1104
1074
|
):
|
|
1105
1075
|
yield event
|
|
1106
1076
|
|
|
1107
1077
|
|
|
1108
|
-
CLOUD_VERIFY_CACHE: Dict[str, Dict] = {}
|
|
1109
1078
|
CLOUD_VERIFY_TTL_SECONDS = 600
|
|
1110
1079
|
|
|
1111
1080
|
async def _probe_cloud_model(model_ref: str) -> Dict[str, object]:
|
|
@@ -1140,9 +1109,15 @@ async def _probe_cloud_model(model_ref: str) -> Dict[str, object]:
|
|
|
1140
1109
|
return {"ok": False, "reason": str(e)[:220]}
|
|
1141
1110
|
|
|
1142
1111
|
|
|
1143
|
-
async def verify_cloud_models(
|
|
1112
|
+
async def verify_cloud_models(
|
|
1113
|
+
force: bool = False,
|
|
1114
|
+
provider_filter: Optional[str] = None,
|
|
1115
|
+
*,
|
|
1116
|
+
state: ModelRuntimeState,
|
|
1117
|
+
cache: Dict[str, Dict[str, Any]],
|
|
1118
|
+
) -> Dict[str, Dict]:
|
|
1144
1119
|
now = time.time()
|
|
1145
|
-
r =
|
|
1120
|
+
r = state.router
|
|
1146
1121
|
cloud_items = [item for item in (r.detected_cloud_models() if r else []) if item.get("tag") == "cloud"]
|
|
1147
1122
|
if provider_filter:
|
|
1148
1123
|
cloud_items = [item for item in cloud_items if item.get("provider") == provider_filter]
|
|
@@ -1150,17 +1125,108 @@ async def verify_cloud_models(force: bool = False, provider_filter: Optional[str
|
|
|
1150
1125
|
results: Dict[str, Dict] = {}
|
|
1151
1126
|
for item in cloud_items:
|
|
1152
1127
|
model_ref = item["id"]
|
|
1153
|
-
cached =
|
|
1128
|
+
cached = cache.get(model_ref)
|
|
1154
1129
|
if not force and cached and (now - cached.get("ts", 0) <= CLOUD_VERIFY_TTL_SECONDS):
|
|
1155
1130
|
results[model_ref] = cached
|
|
1156
1131
|
continue
|
|
1157
1132
|
if item.get("available") is False:
|
|
1158
1133
|
record = {"ok": False, "reason": item.get("requires") or "API key missing", "ts": now}
|
|
1159
|
-
|
|
1134
|
+
cache[model_ref] = record
|
|
1160
1135
|
results[model_ref] = record
|
|
1161
1136
|
continue
|
|
1162
1137
|
probe = await _probe_cloud_model(model_ref)
|
|
1163
1138
|
record = {"ok": bool(probe.get("ok")), "reason": probe.get("reason", ""), "ts": now}
|
|
1164
|
-
|
|
1139
|
+
cache[model_ref] = record
|
|
1165
1140
|
results[model_ref] = record
|
|
1166
1141
|
return results
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
@dataclass(slots=True)
|
|
1145
|
+
class ModelRuntimeService:
|
|
1146
|
+
"""Bound model operations for one explicitly configured application.
|
|
1147
|
+
|
|
1148
|
+
All configuration and app-owned callables live on ``state``. Operational
|
|
1149
|
+
verification cache data belongs to this service instance, so creating a
|
|
1150
|
+
second ASGI app cannot inherit credentials, routers, or probe results from
|
|
1151
|
+
the first one.
|
|
1152
|
+
"""
|
|
1153
|
+
|
|
1154
|
+
state: ModelRuntimeState
|
|
1155
|
+
_cloud_verify_cache: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
1156
|
+
|
|
1157
|
+
def runtime_features(self) -> Dict[str, Any]:
|
|
1158
|
+
return runtime_features(state=self.state)
|
|
1159
|
+
|
|
1160
|
+
def engine_status(self) -> List[Dict[str, Any]]:
|
|
1161
|
+
return engine_status(
|
|
1162
|
+
state=self.state,
|
|
1163
|
+
cloud_verify_cache=self._cloud_verify_cache,
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
def install_engine(
|
|
1167
|
+
self,
|
|
1168
|
+
engine: str,
|
|
1169
|
+
confirmation_token: Optional[str] = None,
|
|
1170
|
+
) -> Dict[str, Any]:
|
|
1171
|
+
return install_engine(
|
|
1172
|
+
engine,
|
|
1173
|
+
confirmation_token=confirmation_token,
|
|
1174
|
+
state=self.state,
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
async def verify_cloud_models(
|
|
1178
|
+
self,
|
|
1179
|
+
force: bool = False,
|
|
1180
|
+
provider_filter: Optional[str] = None,
|
|
1181
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
1182
|
+
return await verify_cloud_models(
|
|
1183
|
+
force=force,
|
|
1184
|
+
provider_filter=provider_filter,
|
|
1185
|
+
state=self.state,
|
|
1186
|
+
cache=self._cloud_verify_cache,
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
async def prepare_and_load_model(
|
|
1190
|
+
self,
|
|
1191
|
+
model_id: str,
|
|
1192
|
+
request: Any,
|
|
1193
|
+
engine: Optional[str] = None,
|
|
1194
|
+
user_email: Optional[str] = None,
|
|
1195
|
+
adapter_path: Optional[str] = None,
|
|
1196
|
+
draft_model_id: Optional[str] = None,
|
|
1197
|
+
allow_download: bool = False,
|
|
1198
|
+
) -> Dict[str, object]:
|
|
1199
|
+
return await prepare_and_load_model(
|
|
1200
|
+
model_id,
|
|
1201
|
+
request,
|
|
1202
|
+
engine=engine,
|
|
1203
|
+
user_email=user_email,
|
|
1204
|
+
adapter_path=adapter_path,
|
|
1205
|
+
draft_model_id=draft_model_id,
|
|
1206
|
+
allow_download=allow_download,
|
|
1207
|
+
state=self.state,
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
async def prepare_and_load_model_stream(
|
|
1211
|
+
self,
|
|
1212
|
+
model_id: str,
|
|
1213
|
+
request: Any,
|
|
1214
|
+
engine: Optional[str] = None,
|
|
1215
|
+
user_email: Optional[str] = None,
|
|
1216
|
+
allow_download: bool = False,
|
|
1217
|
+
) -> AsyncIterator[str]:
|
|
1218
|
+
async for event in prepare_and_load_model_stream(
|
|
1219
|
+
model_id,
|
|
1220
|
+
request,
|
|
1221
|
+
engine=engine,
|
|
1222
|
+
user_email=user_email,
|
|
1223
|
+
allow_download=allow_download,
|
|
1224
|
+
state=self.state,
|
|
1225
|
+
):
|
|
1226
|
+
yield event
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def build_model_runtime(**deps: Any) -> ModelRuntimeService:
|
|
1230
|
+
"""Build the application's isolated model runtime service."""
|
|
1231
|
+
|
|
1232
|
+
return ModelRuntimeService(create_model_runtime_state(**deps))
|
|
@@ -217,7 +217,13 @@ class PReinforceGardener:
|
|
|
217
217
|
folders.append({"name": folder, "description": desc, "files": files, "count": len(files)})
|
|
218
218
|
return {"root": str(BRAIN_DIR), "folders": folders}
|
|
219
219
|
|
|
220
|
-
def get_relevant_context(
|
|
220
|
+
def get_relevant_context(
|
|
221
|
+
self,
|
|
222
|
+
query: str,
|
|
223
|
+
limit: int = 3,
|
|
224
|
+
*,
|
|
225
|
+
allowed_workspaces: Any = None,
|
|
226
|
+
) -> str:
|
|
221
227
|
"""질문과 관련된 정원 노트를 두뇌에서 검색해 컨텍스트로 반환.
|
|
222
228
|
|
|
223
229
|
v4: 채팅마다 vault 전체를 rglob 하던 O(n) 스캔을 브레인 검색으로
|
|
@@ -225,7 +231,16 @@ class PReinforceGardener:
|
|
|
225
231
|
"""
|
|
226
232
|
if self._kg is not None:
|
|
227
233
|
try:
|
|
228
|
-
|
|
234
|
+
scope_kwargs = (
|
|
235
|
+
{"allowed_workspaces": allowed_workspaces}
|
|
236
|
+
if allowed_workspaces is not None
|
|
237
|
+
else {}
|
|
238
|
+
)
|
|
239
|
+
matches = self._kg.search(
|
|
240
|
+
query,
|
|
241
|
+
max(limit * 4, 8),
|
|
242
|
+
**scope_kwargs,
|
|
243
|
+
).get("matches", [])
|
|
229
244
|
results = []
|
|
230
245
|
for match in matches:
|
|
231
246
|
meta = match.get("metadata") or {}
|
|
@@ -238,7 +253,11 @@ class PReinforceGardener:
|
|
|
238
253
|
break
|
|
239
254
|
return "\n\n".join(results)
|
|
240
255
|
except Exception as exc:
|
|
241
|
-
logging.debug("garden brain context failed
|
|
256
|
+
logging.debug("garden brain context failed: %s", exc)
|
|
257
|
+
if allowed_workspaces is not None:
|
|
258
|
+
return ""
|
|
259
|
+
elif allowed_workspaces is not None:
|
|
260
|
+
return ""
|
|
242
261
|
return self._scan_vault_context(query, limit)
|
|
243
262
|
|
|
244
263
|
def _scan_vault_context(self, query: str, limit: int = 3) -> str:
|