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
|
@@ -27,25 +27,40 @@ def build_automation_runtime(
|
|
|
27
27
|
from latticeai.services.triggers import TriggerService
|
|
28
28
|
|
|
29
29
|
review_queue = ReviewQueueService(store=store)
|
|
30
|
+
|
|
31
|
+
def _run_triggered_workflow(workflow_id: str, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
32
|
+
trigger = (inputs or {}).get("__trigger__") or {}
|
|
33
|
+
return platform.run_workflow_by_id(
|
|
34
|
+
workflow_id,
|
|
35
|
+
trigger.get("user_email"),
|
|
36
|
+
trigger.get("workspace_id"),
|
|
37
|
+
with_agent=True,
|
|
38
|
+
inputs=inputs,
|
|
39
|
+
)
|
|
40
|
+
|
|
30
41
|
trigger_service = TriggerService(
|
|
31
42
|
store=store,
|
|
32
|
-
run_workflow=
|
|
33
|
-
wf_id,
|
|
34
|
-
None,
|
|
35
|
-
None,
|
|
36
|
-
with_agent=False,
|
|
37
|
-
inputs=inputs,
|
|
38
|
-
),
|
|
43
|
+
run_workflow=_run_triggered_workflow,
|
|
39
44
|
data_dir=data_dir,
|
|
40
45
|
review_sink=review_queue,
|
|
41
46
|
tz_name=tz_name,
|
|
42
47
|
)
|
|
48
|
+
|
|
49
|
+
def _memory_ingest(**kwargs):
|
|
50
|
+
try:
|
|
51
|
+
# Delegate to store's upsert (enriches memories + KG automatically via internal graph.ingest_event)
|
|
52
|
+
return store.upsert_memory(**kwargs)
|
|
53
|
+
except Exception:
|
|
54
|
+
return None
|
|
55
|
+
|
|
43
56
|
agent_runtime = AgentRuntime(
|
|
44
57
|
store=store,
|
|
45
58
|
orchestrator_factory=platform.build_orchestrator,
|
|
46
59
|
workspace_graph=workspace_graph,
|
|
47
60
|
append_audit_event=append_audit_event,
|
|
48
61
|
hooks=hooks,
|
|
62
|
+
memory_ingest=_memory_ingest,
|
|
63
|
+
review_sink=review_queue,
|
|
49
64
|
)
|
|
50
65
|
run_executor = RunExecutor(
|
|
51
66
|
store=store,
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from latticeai.runtime.stages import RuntimeStage
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class BrainRuntime(RuntimeStage):
|
|
13
|
+
BRAIN_CORE: Any
|
|
14
|
+
KNOWLEDGE_GRAPH: Any
|
|
15
|
+
CONVERSATIONS: Any
|
|
6
16
|
|
|
7
17
|
|
|
8
18
|
def build_brain_runtime(
|
|
@@ -12,7 +22,7 @@ def build_brain_runtime(
|
|
|
12
22
|
enable_graph: bool,
|
|
13
23
|
embedder: Any,
|
|
14
24
|
storage_engine: Any,
|
|
15
|
-
) ->
|
|
25
|
+
) -> BrainRuntime:
|
|
16
26
|
"""Construct Brain Core storage/conversation primitives behind one seam."""
|
|
17
27
|
|
|
18
28
|
from lattice_brain import BrainCore, ConversationStore
|
|
@@ -33,9 +43,11 @@ def build_brain_runtime(
|
|
|
33
43
|
else ConversationStore(data_dir / "knowledge_graph.sqlite")
|
|
34
44
|
)
|
|
35
45
|
conversations.import_legacy_json(history_file)
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
return BrainRuntime(
|
|
47
|
+
BRAIN_CORE=brain_core,
|
|
48
|
+
KNOWLEDGE_GRAPH=knowledge_graph,
|
|
49
|
+
CONVERSATIONS=conversations,
|
|
50
|
+
)
|
|
51
|
+
|
|
41
52
|
|
|
53
|
+
__all__ = ["BrainRuntime", "build_brain_runtime"]
|
|
@@ -56,6 +56,7 @@ def build_interaction_contexts(
|
|
|
56
56
|
install_mcp: Any,
|
|
57
57
|
mcp_public_item: Any,
|
|
58
58
|
hooks: Any,
|
|
59
|
+
workspace_service: Any,
|
|
59
60
|
chat_context: Any,
|
|
60
61
|
search_service: Any,
|
|
61
62
|
allowed_workspaces_for: Any,
|
|
@@ -89,6 +90,7 @@ def build_interaction_contexts(
|
|
|
89
90
|
install_mcp=install_mcp,
|
|
90
91
|
mcp_public_item=mcp_public_item,
|
|
91
92
|
hooks=hooks,
|
|
93
|
+
workspace_service=workspace_service,
|
|
92
94
|
allowed_workspaces_for=allowed_workspaces_for,
|
|
93
95
|
)
|
|
94
96
|
interaction_router_context = InteractionRouterContext(
|
|
@@ -2,39 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
7
|
+
|
|
8
|
+
from latticeai.runtime.stages import RuntimeStage
|
|
6
9
|
|
|
7
10
|
if TYPE_CHECKING:
|
|
8
11
|
from latticeai.core.config import Config
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ConfigRuntime(RuntimeStage):
|
|
16
|
+
CONFIG: Any
|
|
17
|
+
APP_MODE: str
|
|
18
|
+
IS_PUBLIC_MODE: bool
|
|
19
|
+
DEFAULT_HOST: str
|
|
20
|
+
DEFAULT_PORT: int
|
|
21
|
+
NETWORK_EXPOSED: bool
|
|
22
|
+
ENABLE_TELEGRAM: bool
|
|
23
|
+
ENABLE_GRAPH: bool
|
|
24
|
+
AUTOLOAD_MODELS: bool
|
|
25
|
+
MODEL_IDLE_UNLOAD_SECONDS: int
|
|
26
|
+
ALLOW_LOCAL_MODELS: bool
|
|
27
|
+
REQUIRE_AUTH: bool
|
|
28
|
+
ALLOW_PLAINTEXT_API_KEYS: bool
|
|
29
|
+
CORS_ALLOW_NETWORK: bool
|
|
30
|
+
CORS_EXTRA_ORIGINS: Any
|
|
31
|
+
PUBLIC_MODEL: str
|
|
32
|
+
LOCAL_MODEL: str
|
|
33
|
+
LOCAL_DRAFT_MODEL: str
|
|
34
|
+
TIMEZONE: str
|
|
35
|
+
MAX_LOCAL_MODELS: int
|
|
36
|
+
ALLOW_MODEL_DOWNLOADS: bool
|
|
37
|
+
MODEL_DOWNLOAD_TIMEOUT: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_config_runtime(config: "Optional[Config]" = None) -> ConfigRuntime:
|
|
12
41
|
"""Build app configuration values without importing model/runtime code."""
|
|
13
42
|
|
|
14
43
|
from latticeai.core.config import Config
|
|
15
44
|
|
|
16
45
|
cfg = config if config is not None else Config.from_env()
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
return ConfigRuntime(
|
|
47
|
+
CONFIG=cfg,
|
|
48
|
+
APP_MODE=cfg.app_mode,
|
|
49
|
+
IS_PUBLIC_MODE=cfg.is_public,
|
|
50
|
+
DEFAULT_HOST=cfg.host,
|
|
51
|
+
DEFAULT_PORT=cfg.port,
|
|
52
|
+
NETWORK_EXPOSED=cfg.network_exposed,
|
|
53
|
+
ENABLE_TELEGRAM=cfg.enable_telegram,
|
|
54
|
+
ENABLE_GRAPH=cfg.enable_graph,
|
|
55
|
+
AUTOLOAD_MODELS=cfg.autoload_models,
|
|
56
|
+
MODEL_IDLE_UNLOAD_SECONDS=cfg.model_idle_unload_seconds,
|
|
57
|
+
ALLOW_LOCAL_MODELS=cfg.allow_local_models,
|
|
58
|
+
REQUIRE_AUTH=cfg.require_auth,
|
|
59
|
+
ALLOW_PLAINTEXT_API_KEYS=cfg.allow_plaintext_api_keys,
|
|
60
|
+
CORS_ALLOW_NETWORK=cfg.cors_allow_network,
|
|
61
|
+
CORS_EXTRA_ORIGINS=cfg.cors_extra_origins,
|
|
62
|
+
PUBLIC_MODEL=cfg.public_model,
|
|
63
|
+
LOCAL_MODEL=cfg.local_model,
|
|
64
|
+
LOCAL_DRAFT_MODEL=cfg.local_draft_model,
|
|
65
|
+
TIMEZONE=cfg.timezone,
|
|
66
|
+
MAX_LOCAL_MODELS=cfg.max_local_models,
|
|
67
|
+
ALLOW_MODEL_DOWNLOADS=cfg.allow_model_downloads,
|
|
68
|
+
MODEL_DOWNLOAD_TIMEOUT=cfg.model_download_timeout,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["ConfigRuntime", "build_config_runtime"]
|
|
@@ -23,16 +23,28 @@ def build_context_runtime(
|
|
|
23
23
|
search_service = SearchService(graph_store=graph_store)
|
|
24
24
|
brain_memory = BrainMemory(ingestion_pipeline)
|
|
25
25
|
|
|
26
|
-
def scoped_hybrid_search(q, user_email=None, **kw):
|
|
26
|
+
def scoped_hybrid_search(q, user_email=None, workspace_id=None, **kw):
|
|
27
27
|
allowed = None
|
|
28
|
-
if require_auth
|
|
29
|
-
|
|
28
|
+
if require_auth:
|
|
29
|
+
if workspace_id is not None:
|
|
30
|
+
allowed = {workspace_id}
|
|
31
|
+
elif user_email:
|
|
32
|
+
allowed = allowed_scopes_for_user(user_email)
|
|
30
33
|
return search_service.hybrid_search(q, allowed_workspaces=allowed, **kw)
|
|
31
34
|
|
|
35
|
+
def scoped_notes_context(q, user_email=None, workspace_id=None, **kw):
|
|
36
|
+
allowed = None
|
|
37
|
+
if require_auth:
|
|
38
|
+
if workspace_id is not None:
|
|
39
|
+
allowed = {workspace_id}
|
|
40
|
+
elif user_email:
|
|
41
|
+
allowed = allowed_scopes_for_user(user_email)
|
|
42
|
+
return gardener.get_relevant_context(q, allowed_workspaces=allowed, **kw)
|
|
43
|
+
|
|
32
44
|
context_assembler = ContextAssembler(
|
|
33
45
|
memory_recall=memory_service.recall,
|
|
34
46
|
hybrid_search=scoped_hybrid_search,
|
|
35
|
-
notes_context=
|
|
47
|
+
notes_context=scoped_notes_context,
|
|
36
48
|
)
|
|
37
49
|
|
|
38
50
|
return {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Conversation-history query/clear seam extracted from the app factory.
|
|
2
|
+
|
|
3
|
+
The read and clear helpers (scope resolution, ``get_history``, conversation
|
|
4
|
+
grouping, and the clear operations) used to be defined inline in
|
|
5
|
+
``app_factory._build``. They are behaviour-preserving closures over the durable
|
|
6
|
+
conversation store and the workspace service; moving them here keeps the factory
|
|
7
|
+
a wiring path. Names are returned unchanged for the legacy ``server_app``
|
|
8
|
+
compatibility namespace.
|
|
9
|
+
|
|
10
|
+
Note: ``save_to_history`` (the *write* path) stays in the factory — it is bound
|
|
11
|
+
up with redaction, audit, and the ingestion pipeline that are assembled around
|
|
12
|
+
it — so only the query/clear side moves here.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_history_query_runtime(
|
|
22
|
+
*,
|
|
23
|
+
conversations: Any,
|
|
24
|
+
workspace_service: Any,
|
|
25
|
+
require_auth: bool,
|
|
26
|
+
logging: Any,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""Return the history scope/query/clear helpers as a name → callable dict."""
|
|
29
|
+
|
|
30
|
+
def _history_allowed_workspaces_for(user_email: Optional[str]):
|
|
31
|
+
if not require_auth or not user_email:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return set(workspace_service.readable_workspaces(user_email))
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
logging.warning("history workspace scope resolution failed for %s: %s", user_email, exc)
|
|
37
|
+
return set()
|
|
38
|
+
|
|
39
|
+
def _history_include_legacy_global(user_email: Optional[str]) -> bool:
|
|
40
|
+
return not require_auth or not user_email
|
|
41
|
+
|
|
42
|
+
def get_history(
|
|
43
|
+
user_email: Optional[str] = None,
|
|
44
|
+
allowed_workspaces=None,
|
|
45
|
+
include_legacy_global: Optional[bool] = None,
|
|
46
|
+
):
|
|
47
|
+
try:
|
|
48
|
+
if allowed_workspaces is None and user_email:
|
|
49
|
+
allowed_workspaces = _history_allowed_workspaces_for(user_email)
|
|
50
|
+
if include_legacy_global is None:
|
|
51
|
+
include_legacy_global = _history_include_legacy_global(user_email)
|
|
52
|
+
return conversations.history(
|
|
53
|
+
user_email=user_email,
|
|
54
|
+
allowed_workspaces=allowed_workspaces,
|
|
55
|
+
include_legacy_global=include_legacy_global,
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
logging.warning("get_history failed: %s", e)
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
def conversation_title(item: Dict) -> str:
|
|
62
|
+
content = str(item.get("content") or "").strip()
|
|
63
|
+
content = re.sub(r"\s+", " ", content)
|
|
64
|
+
return content[:48] or "새 대화"
|
|
65
|
+
|
|
66
|
+
def group_history_conversations(history: Optional[List[Dict]] = None) -> List[Dict]:
|
|
67
|
+
history = history if history is not None else get_history()
|
|
68
|
+
grouped: Dict[str, Dict] = {}
|
|
69
|
+
order: List[str] = []
|
|
70
|
+
|
|
71
|
+
for index, item in enumerate(history):
|
|
72
|
+
conv_id = item.get("conversation_id")
|
|
73
|
+
if not conv_id:
|
|
74
|
+
conv_id = "legacy-previous-history"
|
|
75
|
+
|
|
76
|
+
if conv_id not in grouped:
|
|
77
|
+
grouped[conv_id] = {
|
|
78
|
+
"id": conv_id,
|
|
79
|
+
"title": "이전 대화 기록" if conv_id == "legacy-previous-history" else conversation_title(item),
|
|
80
|
+
"created_at": item.get("timestamp"),
|
|
81
|
+
"updated_at": item.get("timestamp"),
|
|
82
|
+
"message_count": 0,
|
|
83
|
+
"last_message": "",
|
|
84
|
+
"source": item.get("source"),
|
|
85
|
+
}
|
|
86
|
+
order.append(conv_id)
|
|
87
|
+
|
|
88
|
+
conv = grouped[conv_id]
|
|
89
|
+
conv["message_count"] += 1
|
|
90
|
+
conv["updated_at"] = item.get("timestamp") or conv.get("updated_at")
|
|
91
|
+
conv["last_message"] = conversation_title(item)
|
|
92
|
+
if conv_id != "legacy-previous-history" and item.get("role") == "user" and (not conv.get("title") or conv["title"] == "새 대화"):
|
|
93
|
+
conv["title"] = conversation_title(item)
|
|
94
|
+
|
|
95
|
+
return sorted((grouped[key] for key in order), key=lambda item: item.get("updated_at") or "", reverse=True)
|
|
96
|
+
|
|
97
|
+
def get_conversation_messages(
|
|
98
|
+
conversation_id: str,
|
|
99
|
+
*,
|
|
100
|
+
user_email: Optional[str] = None,
|
|
101
|
+
allowed_workspaces=None,
|
|
102
|
+
include_legacy_global: Optional[bool] = None,
|
|
103
|
+
) -> List[Dict]:
|
|
104
|
+
history = get_history(
|
|
105
|
+
user_email=user_email,
|
|
106
|
+
allowed_workspaces=allowed_workspaces,
|
|
107
|
+
include_legacy_global=include_legacy_global,
|
|
108
|
+
)
|
|
109
|
+
if conversation_id == "legacy-previous-history":
|
|
110
|
+
return [item for item in history if not item.get("conversation_id")]
|
|
111
|
+
return [item for item in history if item.get("conversation_id") == conversation_id]
|
|
112
|
+
|
|
113
|
+
def clear_history(
|
|
114
|
+
keep_last: int = 0,
|
|
115
|
+
*,
|
|
116
|
+
user_email: Optional[str] = None,
|
|
117
|
+
allowed_workspaces=None,
|
|
118
|
+
include_legacy_global: Optional[bool] = None,
|
|
119
|
+
) -> Dict:
|
|
120
|
+
if allowed_workspaces is None and user_email:
|
|
121
|
+
allowed_workspaces = _history_allowed_workspaces_for(user_email)
|
|
122
|
+
if include_legacy_global is None:
|
|
123
|
+
include_legacy_global = _history_include_legacy_global(user_email)
|
|
124
|
+
return conversations.clear_all(
|
|
125
|
+
keep_last=keep_last,
|
|
126
|
+
user_email=user_email,
|
|
127
|
+
allowed_workspaces=allowed_workspaces,
|
|
128
|
+
include_legacy_global=include_legacy_global,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def clear_conversation(
|
|
132
|
+
conversation_id: str,
|
|
133
|
+
started_at: Optional[str] = None,
|
|
134
|
+
*,
|
|
135
|
+
user_email: Optional[str] = None,
|
|
136
|
+
allowed_workspaces=None,
|
|
137
|
+
include_legacy_global: Optional[bool] = None,
|
|
138
|
+
) -> Dict:
|
|
139
|
+
if allowed_workspaces is None and user_email:
|
|
140
|
+
allowed_workspaces = _history_allowed_workspaces_for(user_email)
|
|
141
|
+
if include_legacy_global is None:
|
|
142
|
+
include_legacy_global = _history_include_legacy_global(user_email)
|
|
143
|
+
return conversations.clear_conversation(
|
|
144
|
+
conversation_id,
|
|
145
|
+
started_at=started_at,
|
|
146
|
+
user_email=user_email,
|
|
147
|
+
allowed_workspaces=allowed_workspaces,
|
|
148
|
+
include_legacy_global=include_legacy_global,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"_history_allowed_workspaces_for": _history_allowed_workspaces_for,
|
|
153
|
+
"_history_include_legacy_global": _history_include_legacy_global,
|
|
154
|
+
"get_history": get_history,
|
|
155
|
+
"conversation_title": conversation_title,
|
|
156
|
+
"group_history_conversations": group_history_conversations,
|
|
157
|
+
"get_conversation_messages": get_conversation_messages,
|
|
158
|
+
"clear_history": clear_history,
|
|
159
|
+
"clear_conversation": clear_conversation,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = ["build_history_query_runtime"]
|
|
@@ -21,7 +21,7 @@ def build_hooks_runtime(
|
|
|
21
21
|
"""Construct the hooks registry and local-knowledge watcher behind one seam."""
|
|
22
22
|
|
|
23
23
|
from lattice_brain.runtime.hooks import HooksRegistry
|
|
24
|
-
from
|
|
24
|
+
from latticeai.services.local_knowledge import LocalKnowledgeWatcher
|
|
25
25
|
|
|
26
26
|
hooks_registry = HooksRegistry(data_dir / "hooks.json")
|
|
27
27
|
local_kg_watcher = (
|
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
7
8
|
from latticeai.runtime.platform_services_runtime import build_model_service
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class ModelRuntime:
|
|
13
|
+
"""Typed model-provider stage registered with the HTTP application."""
|
|
14
|
+
|
|
15
|
+
router: Any
|
|
16
|
+
runtime_service: Any
|
|
17
|
+
service: Any
|
|
18
|
+
runtime_features: Any
|
|
19
|
+
is_public: bool
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def configure_model_runtime_from_context(**kwargs: Any) -> Any:
|
|
23
|
+
"""Build and return an isolated model runtime from app-owned values."""
|
|
24
|
+
|
|
25
|
+
build_model_runtime = kwargs.pop("build_model_runtime")
|
|
26
|
+
return build_model_runtime(**kwargs)
|
|
13
27
|
|
|
14
28
|
|
|
15
29
|
def register_model_runtime_routers(
|
|
@@ -19,10 +33,11 @@ def register_model_runtime_routers(
|
|
|
19
33
|
create_models_router: Any,
|
|
20
34
|
register_health_and_model_routers: Any,
|
|
21
35
|
model_router: Any,
|
|
36
|
+
runtime_service: Any,
|
|
22
37
|
runtime_features: Any,
|
|
23
38
|
is_public_mode: bool,
|
|
24
39
|
**kwargs: Any,
|
|
25
|
-
) ->
|
|
40
|
+
) -> ModelRuntime:
|
|
26
41
|
model_service = build_model_service(
|
|
27
42
|
model_router=model_router,
|
|
28
43
|
runtime_features=runtime_features,
|
|
@@ -37,4 +52,17 @@ def register_model_runtime_routers(
|
|
|
37
52
|
is_public_mode=is_public_mode,
|
|
38
53
|
**kwargs,
|
|
39
54
|
)
|
|
40
|
-
return
|
|
55
|
+
return ModelRuntime(
|
|
56
|
+
router=model_router,
|
|
57
|
+
runtime_service=runtime_service,
|
|
58
|
+
service=model_service,
|
|
59
|
+
runtime_features=runtime_features,
|
|
60
|
+
is_public=is_public_mode,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"ModelRuntime",
|
|
66
|
+
"configure_model_runtime_from_context",
|
|
67
|
+
"register_model_runtime_routers",
|
|
68
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Explicit runtime exports for the legacy :mod:`server_app` facade.
|
|
2
|
+
|
|
3
|
+
The composition root must never export ``locals()``. Every compatibility name
|
|
4
|
+
is selected here and every typed assembly stage remains available through the
|
|
5
|
+
``RuntimeBundle`` without leaking construction scratch state.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Dict, Mapping
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class RuntimeBundle:
|
|
16
|
+
"""Typed application assembly result and its five explicit stages."""
|
|
17
|
+
|
|
18
|
+
app: Any
|
|
19
|
+
CONFIG: Any
|
|
20
|
+
KNOWLEDGE_GRAPH: Any
|
|
21
|
+
INGESTION_PIPELINE: Any
|
|
22
|
+
AGENT_RUNTIME: Any
|
|
23
|
+
HOOKS_REGISTRY: Any
|
|
24
|
+
REVIEW_QUEUE: Any
|
|
25
|
+
AGENT_REGISTRY: Any
|
|
26
|
+
model_router: Any
|
|
27
|
+
build_runtime: Any
|
|
28
|
+
get_shared_runtime: Any
|
|
29
|
+
create_app: Any
|
|
30
|
+
config_runtime: Any
|
|
31
|
+
security_runtime: Any
|
|
32
|
+
brain_runtime: Any
|
|
33
|
+
model_runtime: Any
|
|
34
|
+
router_bundle: Any
|
|
35
|
+
|
|
36
|
+
def as_legacy_dict(self) -> Dict[str, Any]:
|
|
37
|
+
return {
|
|
38
|
+
name: getattr(self, name)
|
|
39
|
+
for name in RUNTIME_BUNDLE_EXPORTS
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def stages(self) -> Dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"config": self.config_runtime,
|
|
46
|
+
"security": self.security_runtime,
|
|
47
|
+
"brain": self.brain_runtime,
|
|
48
|
+
"models": self.model_runtime,
|
|
49
|
+
"routers": self.router_bundle,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
RUNTIME_BUNDLE_EXPORTS = frozenset(
|
|
54
|
+
{
|
|
55
|
+
"app",
|
|
56
|
+
"CONFIG",
|
|
57
|
+
"KNOWLEDGE_GRAPH",
|
|
58
|
+
"INGESTION_PIPELINE",
|
|
59
|
+
"AGENT_RUNTIME",
|
|
60
|
+
"HOOKS_REGISTRY",
|
|
61
|
+
"REVIEW_QUEUE",
|
|
62
|
+
"AGENT_REGISTRY",
|
|
63
|
+
"model_router",
|
|
64
|
+
"build_runtime",
|
|
65
|
+
"get_shared_runtime",
|
|
66
|
+
"create_app",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
LEGACY_UNDERSCORE_EXPORTS = {
|
|
71
|
+
"_LOCAL_WRITE_BLOCKED_PREFIXES",
|
|
72
|
+
"_RATE_LIMIT_ENABLED",
|
|
73
|
+
"_SESSION_TTL",
|
|
74
|
+
"_TOOL_CATALOG_BRIEF",
|
|
75
|
+
"_TOOL_GOVERNANCE_DEFAULT",
|
|
76
|
+
"_agent_risk",
|
|
77
|
+
"_allowed_workspaces_for",
|
|
78
|
+
"_build_admin_audit_report",
|
|
79
|
+
"_build_sensitivity_report",
|
|
80
|
+
"_bytes_match_extension",
|
|
81
|
+
"_check_ip_rate_limit",
|
|
82
|
+
"_check_rate_limit",
|
|
83
|
+
"_check_tool_role",
|
|
84
|
+
"_classify_sensitive_message",
|
|
85
|
+
"_client_ip",
|
|
86
|
+
"_create_security_router",
|
|
87
|
+
"_embedding_info",
|
|
88
|
+
"_fetch_skills_marketplace",
|
|
89
|
+
"_get_audit_log",
|
|
90
|
+
"_get_sso_discovery",
|
|
91
|
+
"_graph_stats_safe",
|
|
92
|
+
"_host_is_loopback",
|
|
93
|
+
"_list_compat_profiles",
|
|
94
|
+
"_llm_generate_sync",
|
|
95
|
+
"_product_hardening_status",
|
|
96
|
+
"_recent_chat_context",
|
|
97
|
+
"_redact_secret_text",
|
|
98
|
+
"_require_graph",
|
|
99
|
+
"_scoped_hybrid_search",
|
|
100
|
+
"_security_audit_events_safe",
|
|
101
|
+
"_security_list_uploaded_files",
|
|
102
|
+
"_spawn",
|
|
103
|
+
"_tool_response",
|
|
104
|
+
"_user_id_for_email",
|
|
105
|
+
"_workspace_graph",
|
|
106
|
+
"_workspace_models_payload",
|
|
107
|
+
"_workspace_scope_from_request",
|
|
108
|
+
"_workspace_settings_payload",
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
LEGACY_PUBLIC_EXPORTS = {
|
|
112
|
+
"ENGINE_MODEL_CATALOG",
|
|
113
|
+
"TOOL_GOVERNANCE",
|
|
114
|
+
"enforce_rate_limit",
|
|
115
|
+
"filter_lower_family_versions",
|
|
116
|
+
"hash_password",
|
|
117
|
+
"normalize_local_model_request",
|
|
118
|
+
"verify_password",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def build_runtime_namespace(
|
|
123
|
+
*, runtime_bundle: RuntimeBundle | Mapping[str, Any], legacy_exports: Mapping[str, Any]
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
"""Return only explicit compatibility exports and typed bundle handles."""
|
|
126
|
+
legacy_bundle = (
|
|
127
|
+
runtime_bundle.as_legacy_dict()
|
|
128
|
+
if isinstance(runtime_bundle, RuntimeBundle)
|
|
129
|
+
else dict(runtime_bundle)
|
|
130
|
+
)
|
|
131
|
+
exported: Dict[str, Any] = dict(legacy_bundle)
|
|
132
|
+
allowed = LEGACY_PUBLIC_EXPORTS | LEGACY_UNDERSCORE_EXPORTS
|
|
133
|
+
unexpected = set(legacy_exports) - allowed
|
|
134
|
+
if unexpected:
|
|
135
|
+
raise ValueError(f"unapproved runtime exports: {sorted(unexpected)}")
|
|
136
|
+
for name in sorted(allowed):
|
|
137
|
+
if name in exported:
|
|
138
|
+
continue
|
|
139
|
+
value = legacy_exports.get(name)
|
|
140
|
+
if value is None:
|
|
141
|
+
continue
|
|
142
|
+
exported[name] = value
|
|
143
|
+
exported["RUNTIME_BUNDLE"] = runtime_bundle
|
|
144
|
+
exported["_RUNTIME_BUNDLE"] = legacy_bundle
|
|
145
|
+
return exported
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
SERVER_APP_EXPORTS = frozenset(
|
|
149
|
+
RUNTIME_BUNDLE_EXPORTS
|
|
150
|
+
| LEGACY_PUBLIC_EXPORTS
|
|
151
|
+
| LEGACY_UNDERSCORE_EXPORTS
|
|
152
|
+
| {"RUNTIME_BUNDLE", "_RUNTIME_BUNDLE"}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
__all__ = [
|
|
157
|
+
"LEGACY_UNDERSCORE_EXPORTS",
|
|
158
|
+
"LEGACY_PUBLIC_EXPORTS",
|
|
159
|
+
"RUNTIME_BUNDLE_EXPORTS",
|
|
160
|
+
"RuntimeBundle",
|
|
161
|
+
"SERVER_APP_EXPORTS",
|
|
162
|
+
"build_runtime_namespace",
|
|
163
|
+
]
|