ltcai 10.0.0 → 10.3.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 +43 -32
- package/docs/CHANGELOG.md +204 -0
- package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
- package/docs/MYPY_BACKLOG.md +98 -0
- package/docs/ONBOARDING.md +1 -1
- package/docs/OPERATIONS.md +1 -1
- package/docs/TRUST_MODEL.md +1 -1
- package/docs/WHY_LATTICE.md +1 -1
- package/docs/kg-schema.md +1 -1
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/archive.py +5 -4
- package/lattice_brain/conversations.py +14 -3
- package/lattice_brain/embeddings.py +12 -2
- package/lattice_brain/graph/_kg_common.py +5 -5
- package/lattice_brain/graph/_kg_fsutil.py +5 -4
- package/lattice_brain/graph/discovery.py +4 -3
- package/lattice_brain/graph/discovery_index.py +0 -1
- package/lattice_brain/graph/documents.py +3 -2
- package/lattice_brain/graph/fusion.py +4 -0
- package/lattice_brain/graph/ingest.py +12 -2
- package/lattice_brain/graph/projection.py +3 -2
- package/lattice_brain/graph/provenance.py +5 -3
- package/lattice_brain/graph/rerank.py +4 -1
- package/lattice_brain/graph/retrieval.py +0 -1
- package/lattice_brain/graph/retrieval_docgen.py +0 -1
- package/lattice_brain/graph/retrieval_reads.py +0 -1
- package/lattice_brain/graph/retrieval_vector.py +0 -1
- package/lattice_brain/graph/schema.py +4 -3
- package/lattice_brain/graph/store.py +18 -4
- package/lattice_brain/graph/write_master.py +46 -1
- package/lattice_brain/ingestion.py +4 -1
- package/lattice_brain/portability.py +5 -2
- package/lattice_brain/quality.py +12 -5
- package/lattice_brain/quiet.py +43 -0
- package/lattice_brain/runtime/agent_runtime.py +19 -10
- package/lattice_brain/runtime/hooks.py +3 -2
- package/lattice_brain/runtime/multi_agent.py +2 -3
- package/lattice_brain/sensitivity.py +94 -0
- package/lattice_brain/storage/base.py +30 -2
- package/lattice_brain/storage/migration.py +3 -2
- package/lattice_brain/storage/postgres.py +2 -2
- package/lattice_brain/storage/sqlite.py +6 -4
- package/lattice_brain/workflow.py +4 -2
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/admin.py +1 -1
- package/latticeai/api/agents.py +4 -2
- package/latticeai/api/auth.py +5 -1
- package/latticeai/api/automation_intelligence.py +2 -1
- package/latticeai/api/browser.py +3 -2
- package/latticeai/api/chat.py +28 -17
- package/latticeai/api/chat_agent_http.py +22 -8
- package/latticeai/api/chat_contracts.py +4 -0
- package/latticeai/api/chat_documents.py +6 -2
- package/latticeai/api/chat_hybrid.py +82 -0
- package/latticeai/api/computer_use.py +8 -3
- package/latticeai/api/knowledge_graph.py +1 -1
- package/latticeai/api/mcp.py +4 -4
- package/latticeai/api/models.py +5 -2
- package/latticeai/api/network_boundary.py +220 -0
- package/latticeai/api/permissions.py +0 -1
- package/latticeai/api/realtime.py +1 -1
- package/latticeai/api/security_dashboard.py +1 -1
- package/latticeai/api/setup.py +16 -3
- package/latticeai/api/static_routes.py +2 -1
- package/latticeai/api/tools.py +12 -8
- package/latticeai/api/voice_capture.py +3 -1
- package/latticeai/api/workflow_designer.py +1 -1
- package/latticeai/api/workspace.py +1 -2
- package/latticeai/app_factory.py +174 -145
- package/latticeai/cli/entrypoint.py +6 -4
- package/latticeai/core/agent.py +55 -495
- package/latticeai/core/agent_eval.py +2 -2
- package/latticeai/core/agent_helpers.py +493 -0
- package/latticeai/core/agent_prompts.py +0 -1
- package/latticeai/core/agent_registry.py +3 -1
- package/latticeai/core/agent_state.py +41 -0
- package/latticeai/core/audit.py +1 -1
- package/latticeai/core/builtin_hooks.py +2 -1
- package/latticeai/core/config.py +0 -1
- package/latticeai/core/embedding_providers.py +12 -1
- package/latticeai/core/file_generation.py +3 -0
- package/latticeai/core/invitations.py +4 -1
- package/latticeai/core/io_utils.py +3 -1
- package/latticeai/core/legacy_compatibility.py +1 -2
- package/latticeai/core/local_embeddings.py +12 -1
- package/latticeai/core/marketplace.py +1 -2
- package/latticeai/core/mcp_registry.py +0 -1
- package/latticeai/core/model_compat.py +1 -1
- package/latticeai/core/model_resolution.py +1 -1
- package/latticeai/core/network_boundary.py +168 -0
- package/latticeai/core/oidc.py +3 -0
- package/latticeai/core/permission_mode.py +6 -6
- package/latticeai/core/plugins.py +3 -2
- package/latticeai/core/policy.py +0 -1
- package/latticeai/core/quiet.py +84 -0
- package/latticeai/core/realtime.py +3 -2
- package/latticeai/core/run_store.py +4 -2
- package/latticeai/core/security.py +4 -0
- package/latticeai/core/users.py +5 -3
- package/latticeai/core/workspace_graph_trace.py +3 -0
- package/latticeai/core/workspace_os.py +65 -273
- package/latticeai/core/workspace_os_constants.py +126 -0
- package/latticeai/core/workspace_os_state.py +180 -0
- package/latticeai/core/workspace_os_utils.py +3 -1
- package/latticeai/core/workspace_permissions.py +1 -0
- package/latticeai/core/workspace_snapshots.py +3 -1
- package/latticeai/core/workspace_timeline.py +3 -1
- package/latticeai/integrations/telegram_bot.py +25 -16
- package/latticeai/models/router.py +6 -3
- package/latticeai/runtime/access_runtime.py +3 -1
- package/latticeai/runtime/audit_runtime.py +3 -2
- package/latticeai/runtime/chat_wiring.py +4 -1
- package/latticeai/runtime/history_runtime.py +1 -1
- package/latticeai/runtime/history_writer.py +138 -0
- package/latticeai/runtime/lifespan_runtime.py +3 -1
- package/latticeai/runtime/network_boundary_wiring.py +124 -0
- package/latticeai/runtime/persistence_runtime.py +3 -1
- package/latticeai/runtime/router_registration.py +11 -1
- package/latticeai/services/architecture_readiness.py +1 -2
- package/latticeai/services/change_proposals.py +2 -1
- package/latticeai/services/cloud_egress_audit.py +85 -0
- package/latticeai/services/cloud_extraction.py +129 -0
- package/latticeai/services/cloud_streaming.py +266 -0
- package/latticeai/services/cloud_token_guard.py +84 -0
- package/latticeai/services/command_center.py +2 -1
- package/latticeai/services/folder_watch.py +3 -0
- package/latticeai/services/funnel_metrics.py +3 -1
- package/latticeai/services/hybrid_chat.py +265 -0
- package/latticeai/services/hybrid_context.py +228 -0
- package/latticeai/services/hybrid_policy.py +178 -0
- package/latticeai/services/memory_service.py +1 -1
- package/latticeai/services/model_catalog.py +10 -1
- package/latticeai/services/model_engines.py +35 -14
- package/latticeai/services/model_loading.py +3 -2
- package/latticeai/services/model_runtime.py +68 -17
- package/latticeai/services/multimodal_streaming.py +123 -0
- package/latticeai/services/network_boundary_service.py +154 -0
- package/latticeai/services/openai_compatible_adapter.py +100 -0
- package/latticeai/services/p_reinforce.py +4 -0
- package/latticeai/services/platform_runtime.py +9 -4
- package/latticeai/services/process_audit.py +0 -1
- package/latticeai/services/product_readiness.py +1 -1
- package/latticeai/services/run_executor.py +3 -2
- package/latticeai/services/search_service.py +1 -4
- package/latticeai/services/setup_detection.py +2 -1
- package/latticeai/services/tool_dispatch.py +7 -2
- package/latticeai/services/upload_service.py +2 -1
- package/latticeai/setup/auto_setup.py +10 -7
- package/latticeai/setup/wizard.py +15 -11
- package/latticeai/tools/__init__.py +3 -3
- package/latticeai/tools/commands.py +7 -7
- package/latticeai/tools/computer.py +3 -2
- package/latticeai/tools/documents.py +10 -9
- package/latticeai/tools/filesystem.py +7 -4
- package/latticeai/tools/knowledge.py +2 -0
- package/latticeai/tools/local_files.py +1 -1
- package/latticeai/tools/network.py +2 -1
- package/package.json +7 -3
- package/scripts/bench_agent_smoke.py +0 -1
- package/scripts/bench_models.py +0 -1
- package/scripts/brain_quality_eval.py +7 -2
- package/scripts/bump_version.py +2 -1
- package/scripts/check_current_release_docs.mjs +1 -1
- package/scripts/migrate_brain_storage.py +5 -1
- package/scripts/profile_kg.py +2 -7
- package/scripts/verify_hf_model_registry.py +2 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/tauri.conf.json +1 -1
- package/static/app/asset-manifest.json +37 -37
- package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
- package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
- package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
- package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
- package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
- package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
- package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
- package/static/app/assets/Library-CXnbNiQW.js +1 -0
- package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
- package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
- package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
- package/static/app/assets/System-Bp9ZY4ER.js +1 -0
- package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
- package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
- package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
- package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
- package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
- package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
- package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
- package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
- package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
- package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
- package/static/app/assets/index-CEkL4_M-.css +2 -0
- package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
- package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
- package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
- package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
- package/static/app/assets/search-CwEJWBEA.js +1 -0
- package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
- package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
- package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
- package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
- package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
- package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
- package/static/app/assets/workspace-BMUEiXUm.js +1 -0
- package/static/app/index.html +4 -4
- package/static/sw.js +1 -1
- package/static/app/assets/Library-DhvoPvC7.js +0 -1
- package/static/app/assets/System-CSMdYLMy.js +0 -1
- package/static/app/assets/index-FxDusbr0.css +0 -2
- package/static/app/assets/search-DhbSgW6m.js +0 -1
- package/static/app/assets/workspace-DBPB0jkX.js +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Workspace OS state: the shape of a new file, and upgrades to old ones.
|
|
2
|
+
|
|
3
|
+
Split out of ``workspace_os.py`` in 10.2.0. Neither concern belongs to the
|
|
4
|
+
store: ``default_state`` describes a brand-new workspace file, ``new_workspace_record``
|
|
5
|
+
builds one workspace entry, and ``migrate_workspaces`` upgrades a file written
|
|
6
|
+
by an older version. All three were already free of instance state — two were
|
|
7
|
+
``@staticmethod`` in all but name — so they move without behaviour change.
|
|
8
|
+
|
|
9
|
+
``WorkspaceOSStore`` keeps thin delegating methods, so its public surface and
|
|
10
|
+
every existing call site are unchanged.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from .timeutil import now_iso as _now
|
|
18
|
+
from .workspace_os_constants import (
|
|
19
|
+
DEFAULT_AGENTS,
|
|
20
|
+
DEFAULT_WORKSPACE_ID,
|
|
21
|
+
ONBOARDING_STEPS,
|
|
22
|
+
ROLE_PERMISSIONS,
|
|
23
|
+
WORKSPACE_AREAS,
|
|
24
|
+
WORKSPACE_OS_VERSION,
|
|
25
|
+
WORKSPACE_TYPES,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def new_workspace_record(
|
|
30
|
+
*,
|
|
31
|
+
workspace_id: str,
|
|
32
|
+
name: str,
|
|
33
|
+
workspace_type: str,
|
|
34
|
+
owner_user_id: Optional[str],
|
|
35
|
+
settings: Optional[Dict[str, Any]] = None,
|
|
36
|
+
members: Optional[List[Dict[str, Any]]] = None,
|
|
37
|
+
) -> Dict[str, Any]:
|
|
38
|
+
if workspace_type not in WORKSPACE_TYPES:
|
|
39
|
+
raise ValueError(f"unknown workspace type: {workspace_type}")
|
|
40
|
+
now = _now()
|
|
41
|
+
member_list = list(members or [])
|
|
42
|
+
if owner_user_id and not any(m.get("user_id") == owner_user_id for m in member_list):
|
|
43
|
+
member_list.insert(0, {"user_id": owner_user_id, "role": "owner", "added_at": now})
|
|
44
|
+
return {
|
|
45
|
+
"workspace_id": workspace_id,
|
|
46
|
+
"id": workspace_id,
|
|
47
|
+
"name": name,
|
|
48
|
+
"type": workspace_type,
|
|
49
|
+
"owner_user_id": owner_user_id,
|
|
50
|
+
"members": member_list,
|
|
51
|
+
"roles": {role: sorted(perms) for role, perms in ROLE_PERMISSIONS.items()},
|
|
52
|
+
"status": "active",
|
|
53
|
+
"areas": list(WORKSPACE_AREAS),
|
|
54
|
+
"settings": settings or {},
|
|
55
|
+
"created_at": now,
|
|
56
|
+
"updated_at": now,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def migrate_workspaces(state: Dict[str, Any]) -> Dict[str, Any]:
|
|
61
|
+
"""Non-destructive upgrade of legacy workspace entries to the v1.1 model.
|
|
62
|
+
|
|
63
|
+
Existing 1.0.x state files stored minimal ``{id,name,type,areas}`` dicts.
|
|
64
|
+
This backfills membership/role/timestamp fields without dropping data and
|
|
65
|
+
guarantees the default Personal workspace always exists.
|
|
66
|
+
"""
|
|
67
|
+
workspaces = state.get("workspaces")
|
|
68
|
+
if not isinstance(workspaces, dict):
|
|
69
|
+
workspaces = {}
|
|
70
|
+
migrated: Dict[str, Any] = {}
|
|
71
|
+
for ws_id, ws in workspaces.items():
|
|
72
|
+
if not isinstance(ws, dict):
|
|
73
|
+
continue
|
|
74
|
+
ws_type = ws.get("type") if ws.get("type") in WORKSPACE_TYPES else "organization"
|
|
75
|
+
if ws_id == DEFAULT_WORKSPACE_ID:
|
|
76
|
+
ws_type = "personal"
|
|
77
|
+
base = new_workspace_record(
|
|
78
|
+
workspace_id=ws_id,
|
|
79
|
+
name=ws.get("name") or ws_id,
|
|
80
|
+
workspace_type=ws_type,
|
|
81
|
+
owner_user_id=ws.get("owner_user_id"),
|
|
82
|
+
settings=ws.get("settings") or {},
|
|
83
|
+
members=ws.get("members") if isinstance(ws.get("members"), list) else None,
|
|
84
|
+
)
|
|
85
|
+
# Preserve any pre-existing timestamps / status from the loaded record.
|
|
86
|
+
base["created_at"] = ws.get("created_at") or base["created_at"]
|
|
87
|
+
base["updated_at"] = ws.get("updated_at") or base["updated_at"]
|
|
88
|
+
base["status"] = ws.get("status") or base["status"]
|
|
89
|
+
migrated[ws_id] = base
|
|
90
|
+
if DEFAULT_WORKSPACE_ID not in migrated:
|
|
91
|
+
migrated[DEFAULT_WORKSPACE_ID] = new_workspace_record(
|
|
92
|
+
workspace_id=DEFAULT_WORKSPACE_ID,
|
|
93
|
+
name="Personal Workspace",
|
|
94
|
+
workspace_type="personal",
|
|
95
|
+
owner_user_id=None,
|
|
96
|
+
)
|
|
97
|
+
state["workspaces"] = migrated
|
|
98
|
+
active = state.get("active_workspace")
|
|
99
|
+
if active not in migrated:
|
|
100
|
+
state["active_workspace"] = DEFAULT_WORKSPACE_ID
|
|
101
|
+
return state
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def default_state() -> Dict[str, Any]:
|
|
105
|
+
return {
|
|
106
|
+
"version": WORKSPACE_OS_VERSION,
|
|
107
|
+
"identity": "AI Workspace OS",
|
|
108
|
+
"created_at": _now(),
|
|
109
|
+
"updated_at": _now(),
|
|
110
|
+
"active_workspace": DEFAULT_WORKSPACE_ID,
|
|
111
|
+
"workspaces": {
|
|
112
|
+
DEFAULT_WORKSPACE_ID: new_workspace_record(
|
|
113
|
+
workspace_id=DEFAULT_WORKSPACE_ID,
|
|
114
|
+
name="Personal Workspace",
|
|
115
|
+
workspace_type="personal",
|
|
116
|
+
owner_user_id=None,
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
"feature_flags": {
|
|
120
|
+
"workspace_os": True,
|
|
121
|
+
"graph_trace": True,
|
|
122
|
+
"snapshots": True,
|
|
123
|
+
"personal_memory": True,
|
|
124
|
+
"multi_agent_graph": True,
|
|
125
|
+
"workflow_graph": True,
|
|
126
|
+
"skill_marketplace": True,
|
|
127
|
+
"local_computer_memory": False,
|
|
128
|
+
"organization_workspaces": True,
|
|
129
|
+
"enterprise_seam": True,
|
|
130
|
+
"plugin_sdk": True,
|
|
131
|
+
"workflow_designer": True,
|
|
132
|
+
"multi_agent_runtime": True,
|
|
133
|
+
"realtime_collaboration": True,
|
|
134
|
+
"agent_handoff": True,
|
|
135
|
+
"agent_context_packets": True,
|
|
136
|
+
"review_retry_loops": True,
|
|
137
|
+
"timeline_replay": True,
|
|
138
|
+
"agent_memory": True,
|
|
139
|
+
"agent_planning": True,
|
|
140
|
+
"marketplace_foundation": True,
|
|
141
|
+
"realtime_execution_observability": True,
|
|
142
|
+
},
|
|
143
|
+
"onboarding": {
|
|
144
|
+
"completed": False,
|
|
145
|
+
"current_step": "account",
|
|
146
|
+
"steps": {
|
|
147
|
+
step: {
|
|
148
|
+
"id": step,
|
|
149
|
+
"status": "pending",
|
|
150
|
+
"data": {},
|
|
151
|
+
"error": "",
|
|
152
|
+
"updated_at": None,
|
|
153
|
+
}
|
|
154
|
+
for step in ONBOARDING_STEPS
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
"snapshots": [],
|
|
158
|
+
"traces": [],
|
|
159
|
+
"memories": [],
|
|
160
|
+
"memory_snapshots": [],
|
|
161
|
+
"agents": list(DEFAULT_AGENTS),
|
|
162
|
+
"agent_runs": [],
|
|
163
|
+
"handoffs": [],
|
|
164
|
+
"workflows": [],
|
|
165
|
+
"workflow_runs": [],
|
|
166
|
+
"review_items": [],
|
|
167
|
+
"skill_registry": {},
|
|
168
|
+
"plugin_registry": {},
|
|
169
|
+
"template_registry": {},
|
|
170
|
+
"computer_memory": {
|
|
171
|
+
"enabled": False,
|
|
172
|
+
"approved": False,
|
|
173
|
+
"approved_at": None,
|
|
174
|
+
"approved_by": None,
|
|
175
|
+
"scopes": ["Downloads", "Documents", "Repositories"],
|
|
176
|
+
"activities": [],
|
|
177
|
+
"notice": "Local Computer Memory is OFF by default and requires explicit approval.",
|
|
178
|
+
},
|
|
179
|
+
"timeline": [],
|
|
180
|
+
}
|
|
@@ -11,7 +11,9 @@ import shutil
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any, Dict, List, Optional
|
|
13
13
|
|
|
14
|
-
from .io_utils import
|
|
14
|
+
from .io_utils import (
|
|
15
|
+
atomic_write_json as _atomic_write_json, # noqa: F401 - legacy helper re-export
|
|
16
|
+
)
|
|
15
17
|
from .io_utils import parse_iso as _parse_iso # noqa: F401 - legacy helper re-export
|
|
16
18
|
|
|
17
19
|
|
|
@@ -10,6 +10,7 @@ from typing import Any, Dict, Optional
|
|
|
10
10
|
from .timeutil import now_iso as _now
|
|
11
11
|
from .workspace_os_utils import _listify
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
# Avoid circular at import time: pull constants lazily from parent module.
|
|
14
15
|
def _get_role_permissions():
|
|
15
16
|
from .workspace_os import ROLE_PERMISSIONS
|
|
@@ -10,6 +10,8 @@ from datetime import datetime
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Dict, Iterable, Optional
|
|
12
12
|
|
|
13
|
+
from latticeai.core.quiet import quiet
|
|
14
|
+
|
|
13
15
|
from .timeutil import now_iso as _now
|
|
14
16
|
from .workspace_os_utils import _atomic_write_json, _json_hash, _listify, _safe_slug
|
|
15
17
|
|
|
@@ -137,7 +139,7 @@ class WorkspaceSnapshots:
|
|
|
137
139
|
imported = graph.import_graph(snapshot.get("graph") or {}, mode="merge")
|
|
138
140
|
result["imported"] = imported
|
|
139
141
|
except Exception:
|
|
140
|
-
|
|
142
|
+
quiet()
|
|
141
143
|
# Always set for test compatibility (additive restore)
|
|
142
144
|
data = snapshot.get("graph") or {}
|
|
143
145
|
if "counts" not in data:
|
|
@@ -6,6 +6,8 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
from typing import Any, Dict, Iterable, List, Optional
|
|
8
8
|
|
|
9
|
+
from latticeai.core.quiet import quiet
|
|
10
|
+
|
|
9
11
|
from .timeutil import now_iso as _now
|
|
10
12
|
from .workspace_os_utils import _listify, _parse_iso
|
|
11
13
|
|
|
@@ -54,7 +56,7 @@ class WorkspaceTimeline:
|
|
|
54
56
|
try:
|
|
55
57
|
self._store.event_sink({**entry, "type": "timeline"})
|
|
56
58
|
except Exception:
|
|
57
|
-
|
|
59
|
+
quiet()
|
|
58
60
|
return entry
|
|
59
61
|
|
|
60
62
|
def filter_audit_timeline(
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import httpx
|
|
3
|
-
import logging
|
|
4
2
|
import base64
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
5
|
import os
|
|
6
6
|
import socket
|
|
7
7
|
import tempfile
|
|
8
8
|
import zipfile
|
|
9
|
-
import json
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from latticeai.cli.runtime import _load_env_file
|
|
12
14
|
from latticeai.core.io_utils import atomic_write_json
|
|
13
15
|
from latticeai.core.logging_safety import install_sensitive_log_filter, safe_log_text
|
|
14
|
-
from latticeai.
|
|
16
|
+
from latticeai.core.quiet import quiet
|
|
15
17
|
|
|
16
18
|
install_sensitive_log_filter()
|
|
17
19
|
|
|
@@ -182,14 +184,14 @@ async def send_chat_action(client, chat_id, action="typing"):
|
|
|
182
184
|
try:
|
|
183
185
|
await client.post(f"{API_URL}/sendChatAction", json={"chat_id": chat_id, "action": action})
|
|
184
186
|
except Exception:
|
|
185
|
-
|
|
187
|
+
quiet()
|
|
186
188
|
|
|
187
189
|
async def answer_callback(client, callback_query_id, text=""):
|
|
188
190
|
try:
|
|
189
191
|
await client.post(f"{API_URL}/answerCallbackQuery",
|
|
190
192
|
json={"callback_query_id": callback_query_id, "text": text})
|
|
191
193
|
except Exception:
|
|
192
|
-
|
|
194
|
+
quiet()
|
|
193
195
|
|
|
194
196
|
async def edit_message(client, chat_id, message_id, text, reply_markup=None):
|
|
195
197
|
try:
|
|
@@ -198,7 +200,7 @@ async def edit_message(client, chat_id, message_id, text, reply_markup=None):
|
|
|
198
200
|
payload["reply_markup"] = reply_markup
|
|
199
201
|
await client.post(f"{API_URL}/editMessageText", json=payload)
|
|
200
202
|
except Exception:
|
|
201
|
-
|
|
203
|
+
quiet()
|
|
202
204
|
|
|
203
205
|
# ── Network helpers ───────────────────────────────────────────────────────────
|
|
204
206
|
|
|
@@ -210,14 +212,14 @@ def get_lan_ip():
|
|
|
210
212
|
if not ip.startswith("127."):
|
|
211
213
|
return ip
|
|
212
214
|
except OSError:
|
|
213
|
-
|
|
215
|
+
quiet()
|
|
214
216
|
try:
|
|
215
217
|
hostname = socket.gethostname()
|
|
216
218
|
for ip in socket.gethostbyname_ex(hostname)[2]:
|
|
217
219
|
if not ip.startswith("127."):
|
|
218
220
|
return ip
|
|
219
221
|
except OSError:
|
|
220
|
-
|
|
222
|
+
quiet()
|
|
221
223
|
return "127.0.0.1"
|
|
222
224
|
|
|
223
225
|
def get_web_url():
|
|
@@ -332,7 +334,7 @@ async def _mac_ram_used_gb() -> str:
|
|
|
332
334
|
try:
|
|
333
335
|
stats[k.strip()] = int(v.strip().rstrip(".")) * page_size
|
|
334
336
|
except ValueError:
|
|
335
|
-
|
|
337
|
+
quiet()
|
|
336
338
|
|
|
337
339
|
used = stats.get("Pages active", 0) + stats.get("Pages wired down", 0)
|
|
338
340
|
|
|
@@ -452,7 +454,12 @@ async def show_graph_stats(client, chat_id):
|
|
|
452
454
|
|
|
453
455
|
async def take_screenshot(client, chat_id):
|
|
454
456
|
await send_chat_action(client, chat_id, "upload_photo")
|
|
455
|
-
|
|
457
|
+
# mkstemp, not mktemp: mktemp only predicts an unused name, leaving a
|
|
458
|
+
# window in which anything can create that path first. mkstemp creates
|
|
459
|
+
# the file atomically with 0600.
|
|
460
|
+
_fd, _name = tempfile.mkstemp(suffix=".jpg")
|
|
461
|
+
os.close(_fd)
|
|
462
|
+
tmp = Path(_name)
|
|
456
463
|
try:
|
|
457
464
|
proc = await asyncio.create_subprocess_exec(
|
|
458
465
|
"screencapture", "-x", str(tmp),
|
|
@@ -474,7 +481,7 @@ async def take_screenshot(client, chat_id):
|
|
|
474
481
|
try:
|
|
475
482
|
tmp.unlink(missing_ok=True)
|
|
476
483
|
except Exception:
|
|
477
|
-
|
|
484
|
+
quiet()
|
|
478
485
|
|
|
479
486
|
# ── History ───────────────────────────────────────────────────────────────────
|
|
480
487
|
|
|
@@ -570,7 +577,9 @@ async def process_document_file(client, chat_id, file_id: str, filename: str, ca
|
|
|
570
577
|
f"지원 형식: {', '.join(sorted(allowed))}")
|
|
571
578
|
return
|
|
572
579
|
|
|
573
|
-
|
|
580
|
+
_fd, _name = tempfile.mkstemp(suffix=suffix) # see take_screenshot
|
|
581
|
+
os.close(_fd)
|
|
582
|
+
tmp = Path(_name)
|
|
574
583
|
try:
|
|
575
584
|
tmp.write_bytes(raw)
|
|
576
585
|
async with _server_client() as lc:
|
|
@@ -632,7 +641,7 @@ async def ask_ai(client, message, image_data=None, agent_mode=False,
|
|
|
632
641
|
chunk = json.loads(line[5:].strip()).get("chunk", "")
|
|
633
642
|
text += chunk
|
|
634
643
|
except Exception:
|
|
635
|
-
|
|
644
|
+
quiet()
|
|
636
645
|
return {"response": text.strip() or "⚠️ 빈 응답"}
|
|
637
646
|
return res.json()
|
|
638
647
|
try:
|
|
@@ -1050,7 +1059,7 @@ async def process_ai_request(client, chat_id, user_text, image_data=None):
|
|
|
1050
1059
|
try:
|
|
1051
1060
|
await send_message(client, chat_id, "⚠️ 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.")
|
|
1052
1061
|
except Exception:
|
|
1053
|
-
|
|
1062
|
+
quiet()
|
|
1054
1063
|
|
|
1055
1064
|
# ── Command dispatch ──────────────────────────────────────────────────────────
|
|
1056
1065
|
|
|
@@ -1309,4 +1318,4 @@ if __name__ == "__main__":
|
|
|
1309
1318
|
try:
|
|
1310
1319
|
asyncio.run(run_bot())
|
|
1311
1320
|
except KeyboardInterrupt:
|
|
1312
|
-
|
|
1321
|
+
quiet()
|
|
@@ -19,9 +19,12 @@ from pathlib import Path
|
|
|
19
19
|
os.environ.setdefault("MLX_VLM_DRAFT_KIND", "mtp")
|
|
20
20
|
|
|
21
21
|
from concurrent.futures import ThreadPoolExecutor
|
|
22
|
-
from typing import AsyncIterator, Dict, Optional, Tuple
|
|
22
|
+
from typing import AsyncIterator, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
23
24
|
from PIL import Image
|
|
24
25
|
|
|
26
|
+
from latticeai.core.quiet import quiet
|
|
27
|
+
|
|
25
28
|
# Cloud provider catalog data lives in .model_providers; re-exported here so
|
|
26
29
|
# ``from latticeai.models.router import OPENAI_COMPATIBLE_PROVIDERS`` (and the
|
|
27
30
|
# model_runtime re-export chain) resolve unchanged after the split.
|
|
@@ -265,7 +268,7 @@ def _mlx_sampler(temperature: float):
|
|
|
265
268
|
package into the runtime contract.
|
|
266
269
|
"""
|
|
267
270
|
_ = temperature
|
|
268
|
-
return
|
|
271
|
+
return
|
|
269
272
|
|
|
270
273
|
class LLMRouter:
|
|
271
274
|
def __init__(self):
|
|
@@ -525,7 +528,7 @@ class LLMRouter:
|
|
|
525
528
|
msgs = [{"role": "system", "content": system}, {"role": "user", "content": message}]
|
|
526
529
|
return tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
|
527
530
|
except Exception:
|
|
528
|
-
|
|
531
|
+
quiet()
|
|
529
532
|
return f"<|im_start|>system\n{system}<|im_end|>\n<|im_start|>user\n{message}<|im_end|>\n<|im_start|>assistant\n"
|
|
530
533
|
|
|
531
534
|
def _build_vlm_prompt(self, model, processor, message: str, context: Optional[str], num_images: int) -> str:
|
|
@@ -4,6 +4,8 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import Any, Callable, Dict, Optional
|
|
6
6
|
|
|
7
|
+
from latticeai.core.quiet import quiet
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
def build_access_runtime(
|
|
9
11
|
*,
|
|
@@ -129,7 +131,7 @@ def build_access_runtime(
|
|
|
129
131
|
require_capability(role, "admin:users")
|
|
130
132
|
return email, users
|
|
131
133
|
except PermissionError:
|
|
132
|
-
|
|
134
|
+
quiet()
|
|
133
135
|
raise http_exception(status_code=403, detail="관리자 권한이 필요합니다.")
|
|
134
136
|
|
|
135
137
|
def public_user(email: str, user: Dict, users: Dict) -> Dict:
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any, Callable, Dict, List, Optional
|
|
10
10
|
|
|
11
11
|
from latticeai.core import timezones
|
|
12
|
+
from latticeai.core.quiet import quiet
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def build_audit_runtime(
|
|
@@ -40,7 +41,7 @@ def build_audit_runtime(
|
|
|
40
41
|
if isinstance(item, dict):
|
|
41
42
|
events.append(item)
|
|
42
43
|
except Exception:
|
|
43
|
-
|
|
44
|
+
quiet()
|
|
44
45
|
return events[-5000:]
|
|
45
46
|
|
|
46
47
|
def _append(event_type: str, **payload: Any) -> None:
|
|
@@ -53,7 +54,7 @@ def build_audit_runtime(
|
|
|
53
54
|
if k in entry and isinstance(entry[k], str):
|
|
54
55
|
entry[k] = redact_fn(entry[k])
|
|
55
56
|
except Exception:
|
|
56
|
-
|
|
57
|
+
quiet()
|
|
57
58
|
with _audit_lock:
|
|
58
59
|
audit_file.parent.mkdir(parents=True, exist_ok=True)
|
|
59
60
|
with audit_jsonl.open("a", encoding="utf-8") as fh:
|
|
@@ -8,7 +8,10 @@ from latticeai.runtime.permission_mode_wiring import (
|
|
|
8
8
|
bind_dispatch_permission_mode,
|
|
9
9
|
resolve_active_permission_mode,
|
|
10
10
|
)
|
|
11
|
-
from latticeai.services.router_context import
|
|
11
|
+
from latticeai.services.router_context import (
|
|
12
|
+
InteractionRouterContext,
|
|
13
|
+
ToolRouterContext,
|
|
14
|
+
)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def build_chat_agent_runtime_from_context(
|
|
@@ -68,7 +68,7 @@ def build_history_query_runtime(
|
|
|
68
68
|
grouped: Dict[str, Dict] = {}
|
|
69
69
|
order: List[str] = []
|
|
70
70
|
|
|
71
|
-
for index, item in enumerate(history):
|
|
71
|
+
for index, item in enumerate(history): # noqa: B007 — index documents the enumerate contract for readers
|
|
72
72
|
conv_id = item.get("conversation_id")
|
|
73
73
|
if not conv_id:
|
|
74
74
|
conv_id = "legacy-previous-history"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Persisting one chat turn: redact, audit, store, then grow the Brain.
|
|
2
|
+
|
|
3
|
+
Extracted from the `app_factory._build` closure in 10.3.0. It was 66 lines
|
|
4
|
+
deep inside a 1,343-line composition root, which meant the one function that
|
|
5
|
+
decides what a chat message looks like *after* redaction — and what the audit
|
|
6
|
+
log records about it — could not be exercised without standing up the whole
|
|
7
|
+
application.
|
|
8
|
+
|
|
9
|
+
The order here is the contract, and it is deliberate:
|
|
10
|
+
|
|
11
|
+
1. **Redact first.** Everything downstream (the audit preview, the durable
|
|
12
|
+
store, the knowledge graph) sees the redacted text, never the original.
|
|
13
|
+
2. **Audit before storing.** The audit row carries a masked preview and the
|
|
14
|
+
sensitivity verdict, so a message that should not have been sent is
|
|
15
|
+
visible in the log even if the store write later fails.
|
|
16
|
+
3. **Ingest through the pipeline, not the store.** Chat messages enter the
|
|
17
|
+
Brain the same way files and web pages do, so they get provenance and the
|
|
18
|
+
hook lifecycle rather than a direct write that bypasses both.
|
|
19
|
+
4. **A failed ingest does not lose the message.** Graph growth is best-effort;
|
|
20
|
+
the conversation store is not.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from typing import Any, Callable, Dict, Optional
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class HistoryWriterDeps:
|
|
35
|
+
"""Everything one chat turn needs, injected so this is testable in isolation."""
|
|
36
|
+
|
|
37
|
+
conversations: Any
|
|
38
|
+
append_audit_event: Callable[..., None]
|
|
39
|
+
classify_sensitive_message: Callable[[Dict[str, Any], int], Dict[str, Any]]
|
|
40
|
+
redact_secret_text: Callable[[str], str]
|
|
41
|
+
normalize_branding: Callable[[str], str]
|
|
42
|
+
ingestion_pipeline: Any = None
|
|
43
|
+
ingestion_item_factory: Any = None
|
|
44
|
+
enable_graph: bool = False
|
|
45
|
+
knowledge_graph: Any = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_chat_turn(
|
|
49
|
+
role: str,
|
|
50
|
+
message: str,
|
|
51
|
+
*,
|
|
52
|
+
user_email: Optional[str] = None,
|
|
53
|
+
user_nickname: Optional[str] = None,
|
|
54
|
+
source: Optional[str] = None,
|
|
55
|
+
conversation_id: Optional[str] = None,
|
|
56
|
+
workspace_id: Optional[str] = None,
|
|
57
|
+
deps: HistoryWriterDeps,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Persist one chat turn. Never raises: a failure here must not lose a reply."""
|
|
60
|
+
redact_secret_text = deps.redact_secret_text
|
|
61
|
+
normalize_branding = deps.normalize_branding
|
|
62
|
+
classify_sensitive_message = deps.classify_sensitive_message
|
|
63
|
+
append_audit_event = deps.append_audit_event
|
|
64
|
+
conversations = deps.conversations
|
|
65
|
+
ingestion_pipeline = deps.ingestion_pipeline
|
|
66
|
+
ingestion_item_factory = deps.ingestion_item_factory
|
|
67
|
+
enable_graph = deps.enable_graph
|
|
68
|
+
knowledge_graph = deps.knowledge_graph
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# 1. Redact before anything else sees the text.
|
|
72
|
+
message = redact_secret_text(message)
|
|
73
|
+
if role == "assistant":
|
|
74
|
+
message = normalize_branding(message)
|
|
75
|
+
|
|
76
|
+
item: Dict[str, Any] = {
|
|
77
|
+
"role": role,
|
|
78
|
+
"content": message,
|
|
79
|
+
"timestamp": datetime.now().isoformat(),
|
|
80
|
+
}
|
|
81
|
+
for key, value in (
|
|
82
|
+
("user_email", user_email),
|
|
83
|
+
("user_nickname", user_nickname),
|
|
84
|
+
("source", source),
|
|
85
|
+
("conversation_id", conversation_id),
|
|
86
|
+
("workspace_id", workspace_id),
|
|
87
|
+
):
|
|
88
|
+
if value:
|
|
89
|
+
item[key] = value
|
|
90
|
+
|
|
91
|
+
# 2. Audit carries a masked preview and the verdict, never the body.
|
|
92
|
+
sensitive = classify_sensitive_message(item, -1)
|
|
93
|
+
append_audit_event(
|
|
94
|
+
"chat_message",
|
|
95
|
+
role=role,
|
|
96
|
+
user_email=user_email,
|
|
97
|
+
user_nickname=user_nickname,
|
|
98
|
+
source=source,
|
|
99
|
+
conversation_id=conversation_id,
|
|
100
|
+
workspace_id=workspace_id,
|
|
101
|
+
content_preview=sensitive.get("preview"),
|
|
102
|
+
content_chars=len(message or ""),
|
|
103
|
+
sensitivity=sensitive.get("sensitivity"),
|
|
104
|
+
sensitive_labels=sensitive.get("labels") or [],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 3. Durable episodic memory. Unbounded SQLite; the old 50-message
|
|
108
|
+
# chat_history.json cap is gone.
|
|
109
|
+
conversations.append(item)
|
|
110
|
+
|
|
111
|
+
# 4. Best-effort Brain growth through the unified pipeline.
|
|
112
|
+
if enable_graph and knowledge_graph and ingestion_pipeline and ingestion_item_factory:
|
|
113
|
+
try:
|
|
114
|
+
ingestion_pipeline.ingest(
|
|
115
|
+
ingestion_item_factory(
|
|
116
|
+
source_type="chat_message",
|
|
117
|
+
text=message,
|
|
118
|
+
owner=user_email,
|
|
119
|
+
workspace_id=workspace_id,
|
|
120
|
+
conversation_id=conversation_id,
|
|
121
|
+
metadata={
|
|
122
|
+
"role": role,
|
|
123
|
+
"user_nickname": user_nickname,
|
|
124
|
+
"source": source,
|
|
125
|
+
"raw": item,
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
user_email=user_email,
|
|
129
|
+
)
|
|
130
|
+
except Exception as graph_error: # noqa: BLE001
|
|
131
|
+
# The message is already stored; a graph failure must not
|
|
132
|
+
# look like a lost turn.
|
|
133
|
+
logger.warning("knowledge graph message ingest failed: %s", graph_error)
|
|
134
|
+
except Exception as exc: # noqa: BLE001
|
|
135
|
+
logger.warning("save_to_history failed: %s", exc)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
__all__ = ["HistoryWriterDeps", "write_chat_turn"]
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
from contextlib import asynccontextmanager
|
|
6
6
|
from typing import Any, Dict
|
|
7
7
|
|
|
8
|
+
from latticeai.core.quiet import quiet
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def build_lifespan_runtime(
|
|
10
12
|
*,
|
|
@@ -128,7 +130,7 @@ def build_lifespan_runtime(
|
|
|
128
130
|
proc.terminate()
|
|
129
131
|
proc.wait(timeout=5)
|
|
130
132
|
except Exception:
|
|
131
|
-
|
|
133
|
+
quiet()
|
|
132
134
|
|
|
133
135
|
return {
|
|
134
136
|
"autoload_default_model": autoload_default_model,
|