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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Machine-checkable architecture readiness gates for release work.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The current release keeps the major architecture priorities under an explicit release
|
|
4
4
|
contract while product maturity work reduces visible beta seams. AgentRuntime, ToolRegistry,
|
|
5
5
|
central Config, decomposed server runtime, and Knowledge Graph stabilization
|
|
6
6
|
must remain discoverable, ordered, and backed by tests before the release can be
|
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List
|
|
|
17
17
|
from latticeai.core.legacy_compatibility import legacy_shim_report
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
ARCHITECTURE_VERSION_TARGET = "
|
|
20
|
+
ARCHITECTURE_VERSION_TARGET = "9.1.0"
|
|
21
21
|
|
|
22
22
|
PREFERRED_REFACTORING_ORDER = [
|
|
23
23
|
"agent-runtime",
|
|
@@ -54,16 +54,64 @@ def _symbol_exists(dotted: str) -> bool:
|
|
|
54
54
|
return False
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def _forbidden_patterns(root: Path, relative_path: str, patterns: List[str]) -> List[str]:
|
|
58
|
+
path = root / relative_path
|
|
59
|
+
try:
|
|
60
|
+
source = path.read_text(encoding="utf-8")
|
|
61
|
+
except OSError:
|
|
62
|
+
return [f"missing:{relative_path}"]
|
|
63
|
+
return [pattern for pattern in patterns if pattern in source]
|
|
64
|
+
|
|
65
|
+
|
|
57
66
|
def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
58
67
|
if root is None:
|
|
59
68
|
root = Path(__file__).resolve().parents[2]
|
|
60
69
|
shim_report = legacy_shim_report(root)
|
|
70
|
+
factory_forbidden = _forbidden_patterns(
|
|
71
|
+
root,
|
|
72
|
+
"latticeai/app_factory.py",
|
|
73
|
+
["build_runtime_namespace(locals", "dict(locals())"],
|
|
74
|
+
)
|
|
75
|
+
model_runtime_forbidden = _forbidden_patterns(
|
|
76
|
+
root,
|
|
77
|
+
"latticeai/services/model_runtime.py",
|
|
78
|
+
[
|
|
79
|
+
"def _sync_globals",
|
|
80
|
+
"global router",
|
|
81
|
+
"STATE = ModelRuntimeState",
|
|
82
|
+
"_STATE_EXPORTS",
|
|
83
|
+
"def __getattr__(name:",
|
|
84
|
+
"from fastapi import HTTPException",
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
model_runtime_forbidden.extend(
|
|
88
|
+
_forbidden_patterns(
|
|
89
|
+
root,
|
|
90
|
+
"latticeai/services/model_loading.py",
|
|
91
|
+
["from .model_runtime import STATE", "from fastapi import HTTPException"],
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
model_runtime_forbidden.extend(
|
|
95
|
+
_forbidden_patterns(
|
|
96
|
+
root,
|
|
97
|
+
"latticeai/services/model_engines.py",
|
|
98
|
+
["from fastapi import HTTPException", "raise HTTPException"],
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
agent_alias_forbidden = _forbidden_patterns(
|
|
102
|
+
root,
|
|
103
|
+
"latticeai/core/agent.py",
|
|
104
|
+
["AgentRuntime = SingleAgentRuntime"],
|
|
105
|
+
)
|
|
61
106
|
|
|
62
107
|
gates = [
|
|
63
108
|
ArchitectureGate(
|
|
64
109
|
id="agent-runtime",
|
|
65
110
|
title="AgentRuntime boundary",
|
|
66
|
-
status="complete" if
|
|
111
|
+
status="complete" if (
|
|
112
|
+
_symbol_exists("lattice_brain.runtime.agent_runtime.AgentRuntime")
|
|
113
|
+
and not agent_alias_forbidden
|
|
114
|
+
) else "incomplete",
|
|
67
115
|
evidence=[
|
|
68
116
|
"lattice_brain.runtime.agent_runtime.AgentRuntime",
|
|
69
117
|
"latticeai.api.agents.create_agents_router(agent_runtime=...)",
|
|
@@ -83,7 +131,11 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
83
131
|
ArchitectureGate(
|
|
84
132
|
id="config-centralization",
|
|
85
133
|
title="Central app Config",
|
|
86
|
-
status="complete" if
|
|
134
|
+
status="complete" if (
|
|
135
|
+
_symbol_exists("latticeai.core.config.Config")
|
|
136
|
+
and _symbol_exists("latticeai.runtime.config_runtime.ConfigRuntime")
|
|
137
|
+
and not model_runtime_forbidden
|
|
138
|
+
) else "incomplete",
|
|
87
139
|
evidence=[
|
|
88
140
|
"latticeai.core.config.Config.from_env",
|
|
89
141
|
"latticeai.runtime.config_runtime.ConfigRuntime",
|
|
@@ -93,7 +145,19 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
93
145
|
ArchitectureGate(
|
|
94
146
|
id="server-decomposition",
|
|
95
147
|
title="Server decomposition",
|
|
96
|
-
status="complete"
|
|
148
|
+
status="complete" if (
|
|
149
|
+
all(
|
|
150
|
+
_symbol_exists(symbol)
|
|
151
|
+
for symbol in [
|
|
152
|
+
"latticeai.runtime.config_runtime.ConfigRuntime",
|
|
153
|
+
"latticeai.runtime.security_runtime.SecurityRuntime",
|
|
154
|
+
"latticeai.runtime.brain_runtime.BrainRuntime",
|
|
155
|
+
"latticeai.runtime.model_wiring.ModelRuntime",
|
|
156
|
+
"latticeai.runtime.router_registration.RouterBundle",
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
and not factory_forbidden
|
|
160
|
+
) else "incomplete",
|
|
97
161
|
evidence=[
|
|
98
162
|
"latticeai.app_factory.create_app composition root",
|
|
99
163
|
"latticeai.api.* domain routers",
|
|
@@ -192,6 +256,11 @@ def architecture_readiness(root: Path | None = None) -> Dict[str, Any]:
|
|
|
192
256
|
"runtime_modules": runtime_module_count,
|
|
193
257
|
"architecture_gates": len(gates),
|
|
194
258
|
"legacy_shims_remaining": shim_report["remaining_count"],
|
|
259
|
+
"forbidden_patterns": {
|
|
260
|
+
"app_factory": factory_forbidden,
|
|
261
|
+
"model_runtime": model_runtime_forbidden,
|
|
262
|
+
"agent_alias": agent_alias_forbidden,
|
|
263
|
+
},
|
|
195
264
|
},
|
|
196
265
|
"legacy_compatibility": shim_report,
|
|
197
266
|
}
|
|
@@ -136,6 +136,7 @@ def build_brain_automation_workflow(recipe_id: str, *, enabled: bool = False) ->
|
|
|
136
136
|
trigger_config = {
|
|
137
137
|
**deepcopy(recipe.trigger),
|
|
138
138
|
"enabled": bool(enabled),
|
|
139
|
+
"review_queue": True,
|
|
139
140
|
"consent_required": True,
|
|
140
141
|
"local_only": True,
|
|
141
142
|
"external_actions": False,
|
|
@@ -156,7 +157,9 @@ def build_brain_automation_workflow(recipe_id: str, *, enabled: bool = False) ->
|
|
|
156
157
|
"name": "Draft Brain review",
|
|
157
158
|
"config": {
|
|
158
159
|
"agent": "agent:planner",
|
|
160
|
+
"goal": recipe.prompt,
|
|
159
161
|
"prompt": recipe.prompt,
|
|
162
|
+
"roles": ["researcher", "planner", "executor", "reviewer"],
|
|
160
163
|
"mode": "draft",
|
|
161
164
|
"local_only": True,
|
|
162
165
|
"external_actions": False,
|
|
@@ -186,29 +189,3 @@ def build_brain_automation_workflow(recipe_id: str, *, enabled: bool = False) ->
|
|
|
186
189
|
"creates": list(recipe.creates),
|
|
187
190
|
},
|
|
188
191
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# === A방향 (Act/automation + BrainAutomationPanel) E2E 시나리오 초안 ===
|
|
192
|
-
# (backend 인터페이스 list_brain_automation_recipes / find_installed... / build_... 완료 후 즉시 작성)
|
|
193
|
-
# 1. Recipe 목록 노출: frontend BrainAutomationPanel이 list_brain_automation_recipes() 호출 → recipes + consent metadata 표시.
|
|
194
|
-
# 2. "Create reviewable draft" 클릭:
|
|
195
|
-
# - build_brain_automation_workflow(recipe_id, enabled=False) 로 draft 생성 (metadata.recipe_id + created_from=brain_automation_recipe)
|
|
196
|
-
# - find_installed_recipe_workflow 로 사전 dedup 체크 → 이미 있으면 기존 반환, UI disabled + "✓ Reviewable draft ready" 피드백.
|
|
197
|
-
# - 생성된 workflow는 automation_state="draft_disabled", trigger.enabled=False → TriggerService 무시.
|
|
198
|
-
# 3. Dedup guard (UI + backend): metadata.recipe_id + created_from 기준. 중복 클릭 가드 (no-dup) 로 double submit 방지.
|
|
199
|
-
# 4. User가 draft를 review/수정 후 enabled=True 로 전환 → TriggerService._triggered_workflows 에서 enabled True인 것만 arm.
|
|
200
|
-
# 5. Interval trigger E2E:
|
|
201
|
-
# - reconcile_missed() : 다운타임 중 missed → "skipped" 이벤트 기록 (catch-up storm 없음).
|
|
202
|
-
# - tick_intervals() : last_fired_at + interval + last_attempt_at dedup 가드 (10s cooldown) 로 중복 실행 방지.
|
|
203
|
-
# - LATTICE_TZ 환경변수: describe()에 "tz" 노출, 이벤트 at 값은 epoch이지만 클라이언트가 LATTICE_TZ로 현지화.
|
|
204
|
-
# 6. Brain event trigger E2E: kg_ingest.* post_tool hook → on_brain_event → matching source_type 필터 → _fire (dedup 5s).
|
|
205
|
-
# 7. Failure degraded:
|
|
206
|
-
# - _fire 에서 run_workflow 예외 → _record_fire_outcome → consecutive_failures++ , describe() "status":"degraded" ( >=3 ).
|
|
207
|
-
# - 성공 시 reset. (실행 내부 실패는 workflow run record에 남음, scheduler는 launch health만).
|
|
208
|
-
# 8. Run provenance: fired run의 inputs["__trigger__"] = {"type": "interval"|"brain_event", ...} 로 감사/디버그 가능.
|
|
209
|
-
# 9. Consent-first: draft_disabled 기본, user enable 전까지 절대 실행 안 됨. "review_before_run": True.
|
|
210
|
-
# 10. End-to-end Act: draft → enable → trigger fire → agent:planner "Draft Brain review" 노드 → output (requires_review) → user review → save or discard.
|
|
211
|
-
#
|
|
212
|
-
# 다음: 실제 API (e.g. POST /brain/automation/install-draft) 가 UI에서 호출되면 위 시나리오에 대한 통합 테스트 + RunExecutor 경로 검증 즉시 추가.
|
|
213
|
-
# 현재 상태: backend recipe 인터페이스 + TriggerService edge 하드닝 + AgentRuntime wiring 완료. UI (App.tsx + styles) + test_brain_automation.py 는 별도 완료 보고됨.
|
|
214
|
-
|
|
@@ -1,37 +1,191 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Non-HTTP chat orchestration: history persistence and answer traces.
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Workspace-OS answer-trace recording) so those concerns are named and wrapped
|
|
7
|
-
rather than reaching into the store directly from the streaming handler.
|
|
3
|
+
The API layer owns FastAPI requests/responses and SSE framing. This service
|
|
4
|
+
owns the reusable bookkeeping that must be identical across normal answers,
|
|
5
|
+
streamed answers, document generation, and fast-path intents:
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
* workspace/user-scoped history reads;
|
|
8
|
+
* asynchronous persistence of user/assistant exchanges;
|
|
9
|
+
* Graph-RAG answer trace construction and recording;
|
|
10
|
+
* history search/grouping independent of HTTP.
|
|
11
|
+
|
|
12
|
+
``coerce`` preserves lightweight test/embedding contexts that provide the old
|
|
13
|
+
``build_graph_trace``/``record_trace`` façade without requiring them to grow a
|
|
14
|
+
full service implementation.
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
from __future__ import annotations
|
|
15
18
|
|
|
19
|
+
import asyncio
|
|
16
20
|
from typing import Any, Callable, Dict, List, Optional
|
|
17
21
|
|
|
18
22
|
from latticeai.core.workspace_os import WorkspaceOSStore
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
class ChatService:
|
|
22
|
-
def __init__(
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
store: WorkspaceOSStore,
|
|
30
|
+
get_history: Callable[..., List[Dict[str, Any]]],
|
|
31
|
+
save_to_history: Optional[Callable[..., None]] = None,
|
|
32
|
+
get_history_user: Optional[Callable[..., Dict[str, Any]]] = None,
|
|
33
|
+
trace_delegate: Any = None,
|
|
34
|
+
):
|
|
23
35
|
self._store = store
|
|
24
36
|
self._get_history = get_history
|
|
37
|
+
self._save_to_history = save_to_history
|
|
38
|
+
self._get_history_user = get_history_user
|
|
39
|
+
self._trace_delegate = trace_delegate
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def coerce(
|
|
43
|
+
cls,
|
|
44
|
+
candidate: Any,
|
|
45
|
+
*,
|
|
46
|
+
store: Any,
|
|
47
|
+
get_history: Callable[..., List[Dict[str, Any]]],
|
|
48
|
+
save_to_history: Callable[..., None],
|
|
49
|
+
get_history_user: Callable[..., Dict[str, Any]],
|
|
50
|
+
) -> "ChatService":
|
|
51
|
+
"""Return a fully wired service while preserving legacy trace fakes."""
|
|
52
|
+
|
|
53
|
+
if isinstance(candidate, cls):
|
|
54
|
+
candidate._get_history = get_history
|
|
55
|
+
candidate._save_to_history = save_to_history
|
|
56
|
+
candidate._get_history_user = get_history_user
|
|
57
|
+
return candidate
|
|
58
|
+
return cls(
|
|
59
|
+
store=store,
|
|
60
|
+
get_history=get_history,
|
|
61
|
+
save_to_history=save_to_history,
|
|
62
|
+
get_history_user=get_history_user,
|
|
63
|
+
trace_delegate=candidate,
|
|
64
|
+
)
|
|
25
65
|
|
|
26
66
|
# ── conversation history ─────────────────────────────────────────────
|
|
27
67
|
|
|
28
|
-
def history(self) -> List[Dict[str, Any]]:
|
|
29
|
-
return self._get_history()
|
|
68
|
+
def history(self, **scope: Any) -> List[Dict[str, Any]]:
|
|
69
|
+
return self._get_history(**scope)
|
|
70
|
+
|
|
71
|
+
def history_scope(
|
|
72
|
+
self,
|
|
73
|
+
user_email: Optional[str],
|
|
74
|
+
*,
|
|
75
|
+
require_auth: bool,
|
|
76
|
+
allowed_workspaces_for: Optional[Callable[[str], Any]] = None,
|
|
77
|
+
) -> Dict[str, Any]:
|
|
78
|
+
scoped_user = user_email if require_auth else None
|
|
79
|
+
allowed = None
|
|
80
|
+
if require_auth and scoped_user and allowed_workspaces_for is not None:
|
|
81
|
+
allowed = allowed_workspaces_for(scoped_user)
|
|
82
|
+
return {
|
|
83
|
+
"user_email": scoped_user,
|
|
84
|
+
"allowed_workspaces": allowed,
|
|
85
|
+
"include_legacy_global": not require_auth,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def history_user(
|
|
89
|
+
self,
|
|
90
|
+
user_email: Optional[str],
|
|
91
|
+
user_nickname: Optional[str],
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
if self._get_history_user is None:
|
|
94
|
+
return {}
|
|
95
|
+
return self._get_history_user(user_email, user_nickname)
|
|
96
|
+
|
|
97
|
+
async def persist_entry(
|
|
98
|
+
self,
|
|
99
|
+
role: str,
|
|
100
|
+
content: str,
|
|
101
|
+
*,
|
|
102
|
+
history_meta: Optional[Dict[str, Any]] = None,
|
|
103
|
+
history_user: Optional[Dict[str, Any]] = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
if self._save_to_history is None:
|
|
106
|
+
raise RuntimeError("chat history writer is not configured")
|
|
107
|
+
await asyncio.to_thread(
|
|
108
|
+
self._save_to_history,
|
|
109
|
+
role,
|
|
110
|
+
content,
|
|
111
|
+
**(history_meta or {}),
|
|
112
|
+
**(history_user or {}),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def persist_exchange(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
request_message: str,
|
|
119
|
+
stored_user_message: str,
|
|
120
|
+
answer: str,
|
|
121
|
+
source: Optional[str],
|
|
122
|
+
history_meta: Dict[str, Any],
|
|
123
|
+
history_user: Dict[str, Any],
|
|
124
|
+
notify: Optional[Callable[[str, str, Optional[str]], None]] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
await self.persist_entry(
|
|
127
|
+
"user",
|
|
128
|
+
stored_user_message,
|
|
129
|
+
history_meta=history_meta,
|
|
130
|
+
history_user=history_user,
|
|
131
|
+
)
|
|
132
|
+
await self.persist_entry(
|
|
133
|
+
"assistant",
|
|
134
|
+
answer,
|
|
135
|
+
history_meta=history_meta,
|
|
136
|
+
history_user=history_user,
|
|
137
|
+
)
|
|
138
|
+
if notify is not None:
|
|
139
|
+
notify("user", request_message, source)
|
|
140
|
+
notify("assistant", answer, source)
|
|
141
|
+
|
|
142
|
+
def search_history(
|
|
143
|
+
self,
|
|
144
|
+
query: str,
|
|
145
|
+
*,
|
|
146
|
+
scope: Dict[str, Any],
|
|
147
|
+
conversation_title: Callable[[Dict[str, Any]], str],
|
|
148
|
+
limit: int = 30,
|
|
149
|
+
) -> List[Dict[str, Any]]:
|
|
150
|
+
q_lower = str(query or "").strip().lower()
|
|
151
|
+
if not q_lower:
|
|
152
|
+
return []
|
|
153
|
+
matches = [
|
|
154
|
+
item
|
|
155
|
+
for item in self.history(**scope)
|
|
156
|
+
if q_lower in str(item.get("content") or "").lower()
|
|
157
|
+
]
|
|
158
|
+
grouped: Dict[str, Dict[str, Any]] = {}
|
|
159
|
+
for item in matches:
|
|
160
|
+
conversation_id = item.get("conversation_id") or "legacy"
|
|
161
|
+
if conversation_id not in grouped:
|
|
162
|
+
grouped[conversation_id] = {
|
|
163
|
+
"conversation_id": conversation_id,
|
|
164
|
+
"title": conversation_title(item),
|
|
165
|
+
"messages": [],
|
|
166
|
+
}
|
|
167
|
+
grouped[conversation_id]["messages"].append(item)
|
|
168
|
+
return list(grouped.values())[-max(1, int(limit or 30)) :]
|
|
30
169
|
|
|
31
170
|
# ── answer-trace recording (Graph RAG) ───────────────────────────────
|
|
32
171
|
|
|
33
|
-
def build_graph_trace(
|
|
34
|
-
|
|
172
|
+
def build_graph_trace(
|
|
173
|
+
self,
|
|
174
|
+
question: str,
|
|
175
|
+
graph: Any,
|
|
176
|
+
context: str = "",
|
|
177
|
+
*,
|
|
178
|
+
limit: int = 8,
|
|
179
|
+
allowed_workspaces=None,
|
|
180
|
+
) -> Dict[str, Any]:
|
|
181
|
+
target = self._trace_delegate or self._store
|
|
182
|
+
return target.build_graph_trace(
|
|
183
|
+
question,
|
|
184
|
+
graph,
|
|
185
|
+
context,
|
|
186
|
+
limit=limit,
|
|
187
|
+
allowed_workspaces=allowed_workspaces,
|
|
188
|
+
)
|
|
35
189
|
|
|
36
190
|
def record_trace(
|
|
37
191
|
self,
|
|
@@ -43,7 +197,37 @@ class ChatService:
|
|
|
43
197
|
trace: Dict[str, Any],
|
|
44
198
|
workspace_id: Optional[str] = None,
|
|
45
199
|
) -> Dict[str, Any]:
|
|
46
|
-
|
|
200
|
+
target = self._trace_delegate or self._store
|
|
201
|
+
return target.record_trace(
|
|
202
|
+
question=question,
|
|
203
|
+
response=response,
|
|
204
|
+
conversation_id=conversation_id,
|
|
205
|
+
user_email=user_email,
|
|
206
|
+
trace=trace,
|
|
207
|
+
workspace_id=workspace_id,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def persist_answer(
|
|
211
|
+
self,
|
|
212
|
+
*,
|
|
213
|
+
question: str,
|
|
214
|
+
response: str,
|
|
215
|
+
conversation_id: Optional[str],
|
|
216
|
+
user_email: Optional[str],
|
|
217
|
+
user_nickname: Optional[str],
|
|
218
|
+
source: Optional[str],
|
|
219
|
+
trace: Dict[str, Any],
|
|
220
|
+
workspace_id: Optional[str],
|
|
221
|
+
history_meta: Dict[str, Any],
|
|
222
|
+
notify: Optional[Callable[[str, str, Optional[str]], None]] = None,
|
|
223
|
+
) -> Dict[str, Any]:
|
|
224
|
+
await self.persist_entry(
|
|
225
|
+
"assistant",
|
|
226
|
+
response,
|
|
227
|
+
history_meta=history_meta,
|
|
228
|
+
history_user=self.history_user(user_email, user_nickname),
|
|
229
|
+
)
|
|
230
|
+
trace_record = self.record_trace(
|
|
47
231
|
question=question,
|
|
48
232
|
response=response,
|
|
49
233
|
conversation_id=conversation_id,
|
|
@@ -51,3 +235,9 @@ class ChatService:
|
|
|
51
235
|
trace=trace,
|
|
52
236
|
workspace_id=workspace_id,
|
|
53
237
|
)
|
|
238
|
+
if notify is not None:
|
|
239
|
+
notify("assistant", response, source)
|
|
240
|
+
return trace_record
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
__all__ = ["ChatService"]
|