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.
Files changed (214) hide show
  1. package/README.md +43 -32
  2. package/docs/CHANGELOG.md +204 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/HYBRID_CLOUD_KG_STREAMING.md +101 -0
  6. package/docs/MYPY_BACKLOG.md +98 -0
  7. package/docs/ONBOARDING.md +1 -1
  8. package/docs/OPERATIONS.md +1 -1
  9. package/docs/TRUST_MODEL.md +1 -1
  10. package/docs/WHY_LATTICE.md +1 -1
  11. package/docs/kg-schema.md +1 -1
  12. package/lattice_brain/__init__.py +1 -1
  13. package/lattice_brain/archive.py +5 -4
  14. package/lattice_brain/conversations.py +14 -3
  15. package/lattice_brain/embeddings.py +12 -2
  16. package/lattice_brain/graph/_kg_common.py +5 -5
  17. package/lattice_brain/graph/_kg_fsutil.py +5 -4
  18. package/lattice_brain/graph/discovery.py +4 -3
  19. package/lattice_brain/graph/discovery_index.py +0 -1
  20. package/lattice_brain/graph/documents.py +3 -2
  21. package/lattice_brain/graph/fusion.py +4 -0
  22. package/lattice_brain/graph/ingest.py +12 -2
  23. package/lattice_brain/graph/projection.py +3 -2
  24. package/lattice_brain/graph/provenance.py +5 -3
  25. package/lattice_brain/graph/rerank.py +4 -1
  26. package/lattice_brain/graph/retrieval.py +0 -1
  27. package/lattice_brain/graph/retrieval_docgen.py +0 -1
  28. package/lattice_brain/graph/retrieval_reads.py +0 -1
  29. package/lattice_brain/graph/retrieval_vector.py +0 -1
  30. package/lattice_brain/graph/schema.py +4 -3
  31. package/lattice_brain/graph/store.py +18 -4
  32. package/lattice_brain/graph/write_master.py +46 -1
  33. package/lattice_brain/ingestion.py +4 -1
  34. package/lattice_brain/portability.py +5 -2
  35. package/lattice_brain/quality.py +12 -5
  36. package/lattice_brain/quiet.py +43 -0
  37. package/lattice_brain/runtime/agent_runtime.py +19 -10
  38. package/lattice_brain/runtime/hooks.py +3 -2
  39. package/lattice_brain/runtime/multi_agent.py +2 -3
  40. package/lattice_brain/sensitivity.py +94 -0
  41. package/lattice_brain/storage/base.py +30 -2
  42. package/lattice_brain/storage/migration.py +3 -2
  43. package/lattice_brain/storage/postgres.py +2 -2
  44. package/lattice_brain/storage/sqlite.py +6 -4
  45. package/lattice_brain/workflow.py +4 -2
  46. package/latticeai/__init__.py +1 -1
  47. package/latticeai/api/admin.py +1 -1
  48. package/latticeai/api/agents.py +4 -2
  49. package/latticeai/api/auth.py +5 -1
  50. package/latticeai/api/automation_intelligence.py +2 -1
  51. package/latticeai/api/browser.py +3 -2
  52. package/latticeai/api/chat.py +28 -17
  53. package/latticeai/api/chat_agent_http.py +22 -8
  54. package/latticeai/api/chat_contracts.py +4 -0
  55. package/latticeai/api/chat_documents.py +6 -2
  56. package/latticeai/api/chat_hybrid.py +82 -0
  57. package/latticeai/api/computer_use.py +8 -3
  58. package/latticeai/api/knowledge_graph.py +1 -1
  59. package/latticeai/api/mcp.py +4 -4
  60. package/latticeai/api/models.py +5 -2
  61. package/latticeai/api/network_boundary.py +220 -0
  62. package/latticeai/api/permissions.py +0 -1
  63. package/latticeai/api/realtime.py +1 -1
  64. package/latticeai/api/security_dashboard.py +1 -1
  65. package/latticeai/api/setup.py +16 -3
  66. package/latticeai/api/static_routes.py +2 -1
  67. package/latticeai/api/tools.py +12 -8
  68. package/latticeai/api/voice_capture.py +3 -1
  69. package/latticeai/api/workflow_designer.py +1 -1
  70. package/latticeai/api/workspace.py +1 -2
  71. package/latticeai/app_factory.py +174 -145
  72. package/latticeai/cli/entrypoint.py +6 -4
  73. package/latticeai/core/agent.py +55 -495
  74. package/latticeai/core/agent_eval.py +2 -2
  75. package/latticeai/core/agent_helpers.py +493 -0
  76. package/latticeai/core/agent_prompts.py +0 -1
  77. package/latticeai/core/agent_registry.py +3 -1
  78. package/latticeai/core/agent_state.py +41 -0
  79. package/latticeai/core/audit.py +1 -1
  80. package/latticeai/core/builtin_hooks.py +2 -1
  81. package/latticeai/core/config.py +0 -1
  82. package/latticeai/core/embedding_providers.py +12 -1
  83. package/latticeai/core/file_generation.py +3 -0
  84. package/latticeai/core/invitations.py +4 -1
  85. package/latticeai/core/io_utils.py +3 -1
  86. package/latticeai/core/legacy_compatibility.py +1 -2
  87. package/latticeai/core/local_embeddings.py +12 -1
  88. package/latticeai/core/marketplace.py +1 -2
  89. package/latticeai/core/mcp_registry.py +0 -1
  90. package/latticeai/core/model_compat.py +1 -1
  91. package/latticeai/core/model_resolution.py +1 -1
  92. package/latticeai/core/network_boundary.py +168 -0
  93. package/latticeai/core/oidc.py +3 -0
  94. package/latticeai/core/permission_mode.py +6 -6
  95. package/latticeai/core/plugins.py +3 -2
  96. package/latticeai/core/policy.py +0 -1
  97. package/latticeai/core/quiet.py +84 -0
  98. package/latticeai/core/realtime.py +3 -2
  99. package/latticeai/core/run_store.py +4 -2
  100. package/latticeai/core/security.py +4 -0
  101. package/latticeai/core/users.py +5 -3
  102. package/latticeai/core/workspace_graph_trace.py +3 -0
  103. package/latticeai/core/workspace_os.py +65 -273
  104. package/latticeai/core/workspace_os_constants.py +126 -0
  105. package/latticeai/core/workspace_os_state.py +180 -0
  106. package/latticeai/core/workspace_os_utils.py +3 -1
  107. package/latticeai/core/workspace_permissions.py +1 -0
  108. package/latticeai/core/workspace_snapshots.py +3 -1
  109. package/latticeai/core/workspace_timeline.py +3 -1
  110. package/latticeai/integrations/telegram_bot.py +25 -16
  111. package/latticeai/models/router.py +6 -3
  112. package/latticeai/runtime/access_runtime.py +3 -1
  113. package/latticeai/runtime/audit_runtime.py +3 -2
  114. package/latticeai/runtime/chat_wiring.py +4 -1
  115. package/latticeai/runtime/history_runtime.py +1 -1
  116. package/latticeai/runtime/history_writer.py +138 -0
  117. package/latticeai/runtime/lifespan_runtime.py +3 -1
  118. package/latticeai/runtime/network_boundary_wiring.py +124 -0
  119. package/latticeai/runtime/persistence_runtime.py +3 -1
  120. package/latticeai/runtime/router_registration.py +11 -1
  121. package/latticeai/services/architecture_readiness.py +1 -2
  122. package/latticeai/services/change_proposals.py +2 -1
  123. package/latticeai/services/cloud_egress_audit.py +85 -0
  124. package/latticeai/services/cloud_extraction.py +129 -0
  125. package/latticeai/services/cloud_streaming.py +266 -0
  126. package/latticeai/services/cloud_token_guard.py +84 -0
  127. package/latticeai/services/command_center.py +2 -1
  128. package/latticeai/services/folder_watch.py +3 -0
  129. package/latticeai/services/funnel_metrics.py +3 -1
  130. package/latticeai/services/hybrid_chat.py +265 -0
  131. package/latticeai/services/hybrid_context.py +228 -0
  132. package/latticeai/services/hybrid_policy.py +178 -0
  133. package/latticeai/services/memory_service.py +1 -1
  134. package/latticeai/services/model_catalog.py +10 -1
  135. package/latticeai/services/model_engines.py +35 -14
  136. package/latticeai/services/model_loading.py +3 -2
  137. package/latticeai/services/model_runtime.py +68 -17
  138. package/latticeai/services/multimodal_streaming.py +123 -0
  139. package/latticeai/services/network_boundary_service.py +154 -0
  140. package/latticeai/services/openai_compatible_adapter.py +100 -0
  141. package/latticeai/services/p_reinforce.py +4 -0
  142. package/latticeai/services/platform_runtime.py +9 -4
  143. package/latticeai/services/process_audit.py +0 -1
  144. package/latticeai/services/product_readiness.py +1 -1
  145. package/latticeai/services/run_executor.py +3 -2
  146. package/latticeai/services/search_service.py +1 -4
  147. package/latticeai/services/setup_detection.py +2 -1
  148. package/latticeai/services/tool_dispatch.py +7 -2
  149. package/latticeai/services/upload_service.py +2 -1
  150. package/latticeai/setup/auto_setup.py +10 -7
  151. package/latticeai/setup/wizard.py +15 -11
  152. package/latticeai/tools/__init__.py +3 -3
  153. package/latticeai/tools/commands.py +7 -7
  154. package/latticeai/tools/computer.py +3 -2
  155. package/latticeai/tools/documents.py +10 -9
  156. package/latticeai/tools/filesystem.py +7 -4
  157. package/latticeai/tools/knowledge.py +2 -0
  158. package/latticeai/tools/local_files.py +1 -1
  159. package/latticeai/tools/network.py +2 -1
  160. package/package.json +7 -3
  161. package/scripts/bench_agent_smoke.py +0 -1
  162. package/scripts/bench_models.py +0 -1
  163. package/scripts/brain_quality_eval.py +7 -2
  164. package/scripts/bump_version.py +2 -1
  165. package/scripts/check_current_release_docs.mjs +1 -1
  166. package/scripts/migrate_brain_storage.py +5 -1
  167. package/scripts/profile_kg.py +2 -7
  168. package/scripts/verify_hf_model_registry.py +2 -2
  169. package/src-tauri/Cargo.lock +1 -1
  170. package/src-tauri/Cargo.toml +1 -1
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +37 -37
  173. package/static/app/assets/{Act-BtCREeN1.js → Act-DrKIGZeJ.js} +1 -1
  174. package/static/app/assets/{AdminConsole-TPeeN18T.js → AdminConsole-CEV1VefA.js} +1 -1
  175. package/static/app/assets/{Brain-BKs6JAp0.js → Brain-BZSXnwWU.js} +1 -1
  176. package/static/app/assets/{BrainHome-BPGOvSd6.js → BrainHome-CEBd86k6.js} +1 -1
  177. package/static/app/assets/{BrainSignals-CtzQZ15J.js → BrainSignals-O4s6R7av.js} +1 -1
  178. package/static/app/assets/{Capture-1_NaHWqB.js → Capture-AGbOxsg-.js} +1 -1
  179. package/static/app/assets/{CommandPalette-pqvQOXe4.js → CommandPalette-CeqBjCn2.js} +1 -1
  180. package/static/app/assets/Library-CXnbNiQW.js +1 -0
  181. package/static/app/assets/{LivingBrain-DlQ20Q75.js → LivingBrain-D9Iei2ZJ.js} +1 -1
  182. package/static/app/assets/{ProductFlow-BZvGDRi_.js → ProductFlow-JlXiDfMM.js} +1 -1
  183. package/static/app/assets/{ReviewCard-BWgI0D2s.js → ReviewCard-QQly2bTV.js} +1 -1
  184. package/static/app/assets/System-Bp9ZY4ER.js +1 -0
  185. package/static/app/assets/{activity-Dlfk8YC7.js → activity-CxRPOQ2F.js} +1 -1
  186. package/static/app/assets/{bot-CDvUB76P.js → bot-1bcsWB2s.js} +1 -1
  187. package/static/app/assets/{brain-xczrohrt.js → brain-CxaeKCRM.js} +1 -1
  188. package/static/app/assets/{button-SOdH3Oyf.js → button-ZYFqGan7.js} +1 -1
  189. package/static/app/assets/{circle-pause-CG1ythH4.js → circle-pause-B5NOIqvk.js} +1 -1
  190. package/static/app/assets/{circle-play-HXwvjS6W.js → circle-play-BiJVFXf7.js} +1 -1
  191. package/static/app/assets/{cpu-B0d-rGyk.js → cpu-Vvq0OKTr.js} +1 -1
  192. package/static/app/assets/{download-BGIkTQL6.js → download-SEcXzkx1.js} +1 -1
  193. package/static/app/assets/{folder-open-Dst_Z0_K.js → folder-open-Dw1ff_Qa.js} +1 -1
  194. package/static/app/assets/{hard-drive-D53MsWkV.js → hard-drive-B00fR7_r.js} +1 -1
  195. package/static/app/assets/index-CEkL4_M-.css +2 -0
  196. package/static/app/assets/{index-C_IrlQMV.js → index-Dm9VXl1o.js} +3 -3
  197. package/static/app/assets/{input-C5m0riF6.js → input-DtdWYQEa.js} +1 -1
  198. package/static/app/assets/{network-C5a-E5iS.js → network-CpjBu0kI.js} +1 -1
  199. package/static/app/assets/{primitives-vNXYf58F.js → primitives-CwsudBwc.js} +1 -1
  200. package/static/app/assets/search-CwEJWBEA.js +1 -0
  201. package/static/app/assets/{shield-alert-Cc-WVXqN.js → shield-alert-CpjBc-gQ.js} +1 -1
  202. package/static/app/assets/{textarea-BkZ0EqVO.js → textarea-DUPyjHQs.js} +1 -1
  203. package/static/app/assets/{useFocusTrap-CTtKbAOU.js → useFocusTrap-BWcMtCWH.js} +1 -1
  204. package/static/app/assets/{useQuery-Dx1fi4Wu.js → useQuery-Dg5fPqTS.js} +1 -1
  205. package/static/app/assets/{users-BFpQXtEF.js → users-BgcdTQGM.js} +1 -1
  206. package/static/app/assets/{utils-BA_lmW3J.js → utils-a3YXFtEO.js} +2 -2
  207. package/static/app/assets/workspace-BMUEiXUm.js +1 -0
  208. package/static/app/index.html +4 -4
  209. package/static/sw.js +1 -1
  210. package/static/app/assets/Library-DhvoPvC7.js +0 -1
  211. package/static/app/assets/System-CSMdYLMy.js +0 -1
  212. package/static/app/assets/index-FxDusbr0.css +0 -2
  213. package/static/app/assets/search-DhbSgW6m.js +0 -1
  214. 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 atomic_write_json as _atomic_write_json # noqa: F401 - legacy helper re-export
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
- pass
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
- pass
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.cli.runtime import _load_env_file
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
- pass
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
- pass
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
- pass
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
- pass
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
- pass
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
- pass
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
- tmp = Path(tempfile.mktemp(suffix=".jpg"))
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
- pass
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
- tmp = Path(tempfile.mktemp(suffix=suffix))
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
- pass
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
- pass
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
- pass
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, List
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 None
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
- pass
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
- pass
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
- pass
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
- pass
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 InteractionRouterContext, ToolRouterContext
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
- pass
133
+ quiet()
132
134
 
133
135
  return {
134
136
  "autoload_default_model": autoload_default_model,