ltcai 6.0.0 β 6.2.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 +33 -35
- package/docs/CHANGELOG.md +68 -0
- package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +1 -1
- package/docs/V4_6_1_RELEASE_REFRESH_REPORT.md +1 -1
- package/docs/V4_7_0_ADMIN_SEPARATION_REPORT.md +1 -1
- package/docs/V4_7_1_ADMIN_OPERATIONS_REPORT.md +1 -1
- package/frontend/src/App.tsx +3 -1281
- package/frontend/src/components/LanguageSwitcher.tsx +23 -0
- package/frontend/src/components/ProductFlow.tsx +32 -662
- package/frontend/src/components/onboarding/ProductFlowScreens.tsx +688 -0
- package/frontend/src/features/admin/AdminConsole.tsx +294 -0
- package/frontend/src/features/brain/BrainHome.tsx +999 -0
- package/frontend/src/features/brain/brainData.ts +98 -0
- package/frontend/src/features/brain/graphLayout.ts +26 -0
- package/frontend/src/features/brain/types.ts +44 -0
- package/frontend/src/features/review/ReviewCard.tsx +15 -10
- package/frontend/src/i18n.ts +208 -0
- package/frontend/src/styles.css +240 -0
- package/lattice_brain/__init__.py +1 -1
- package/lattice_brain/runtime/multi_agent.py +1 -1
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/chat.py +52 -33
- package/latticeai/api/tools.py +50 -23
- package/latticeai/app_factory.py +65 -47
- package/latticeai/cli/__init__.py +1 -0
- package/latticeai/cli/entrypoint.py +283 -0
- package/latticeai/cli/runtime.py +37 -0
- package/latticeai/core/marketplace.py +1 -1
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/integrations/__init__.py +0 -0
- package/latticeai/integrations/telegram_bot.py +1009 -0
- package/latticeai/runtime/lifespan_runtime.py +1 -1
- package/latticeai/runtime/platform_runtime_wiring.py +87 -0
- package/latticeai/runtime/router_registration.py +49 -30
- package/latticeai/services/app_context.py +1 -0
- package/latticeai/services/p_reinforce.py +258 -0
- package/latticeai/services/router_context.py +52 -0
- package/latticeai/services/tool_dispatch.py +82 -25
- package/ltcai_cli.py +7 -305
- package/p_reinforce.py +4 -255
- package/package.json +2 -1
- package/scripts/release_smoke.py +133 -0
- package/scripts/wheel_smoke.py +4 -0
- 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 +5 -5
- package/static/app/assets/{index-xRn29gI8.css β index-B2-1Gm0q.css} +1 -1
- package/static/app/assets/index-D91Rz5--.js +16 -0
- package/static/app/assets/index-D91Rz5--.js.map +1 -0
- package/static/app/index.html +2 -2
- package/telegram_bot.py +9 -1008
- package/static/app/assets/index-D2zafMYb.js +0 -16
- package/static/app/assets/index-D2zafMYb.js.map +0 -1
|
@@ -102,7 +102,7 @@ def build_lifespan_runtime(
|
|
|
102
102
|
try:
|
|
103
103
|
print(f"π§ Lattice AI mode: {app_mode}")
|
|
104
104
|
if enable_telegram:
|
|
105
|
-
from telegram_bot import run_bot
|
|
105
|
+
from latticeai.integrations.telegram_bot import run_bot
|
|
106
106
|
|
|
107
107
|
spawn(run_bot(), name="telegram_bot")
|
|
108
108
|
print("π Telegram Bot Bridge activated!")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Platform and automation wiring for the application factory.
|
|
2
|
+
|
|
3
|
+
This module owns the cross-subsystem runtime assembly that used to live inline
|
|
4
|
+
inside ``app_factory._build``. Keeping it here reduces the app factory's global
|
|
5
|
+
surface without changing router order or the legacy exported runtime names.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, Dict
|
|
11
|
+
|
|
12
|
+
from latticeai.runtime.automation_runtime import build_automation_runtime
|
|
13
|
+
from latticeai.services.platform_runtime import PlatformRuntime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_platform_automation_runtime(
|
|
17
|
+
*,
|
|
18
|
+
model_router: Any,
|
|
19
|
+
workspace_store: Any,
|
|
20
|
+
workspace_service: Any,
|
|
21
|
+
plugin_registry: Any,
|
|
22
|
+
get_current_user: Callable[..., Any],
|
|
23
|
+
workspace_graph: Callable[[], Any],
|
|
24
|
+
workspace_scope_from_request: Callable[..., Any],
|
|
25
|
+
get_tool_permission: Callable[..., Any],
|
|
26
|
+
hooks: Any,
|
|
27
|
+
agent_registry: Any,
|
|
28
|
+
data_dir: Any,
|
|
29
|
+
append_audit_event: Callable[..., Any],
|
|
30
|
+
) -> Dict[str, Any]:
|
|
31
|
+
"""Build platform services, automation services, and hook bindings.
|
|
32
|
+
|
|
33
|
+
The returned names intentionally match the historical local variables in
|
|
34
|
+
``app_factory._build`` so ``dict(locals())`` continues to expose the legacy
|
|
35
|
+
``server_app`` compatibility surface.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def _llm_generate_sync(
|
|
39
|
+
message: str,
|
|
40
|
+
context: str = "",
|
|
41
|
+
max_tokens: int = 1024,
|
|
42
|
+
temperature: float = 0.1,
|
|
43
|
+
) -> str:
|
|
44
|
+
# Synchronous model bridge for the orchestrator's role runner. Safe
|
|
45
|
+
# because the agents run endpoint executes start() in a worker thread
|
|
46
|
+
# (asyncio.to_thread), where no event loop is running.
|
|
47
|
+
import asyncio as _asyncio
|
|
48
|
+
|
|
49
|
+
return str(_asyncio.run(model_router.generate(
|
|
50
|
+
message,
|
|
51
|
+
context=context,
|
|
52
|
+
max_tokens=max_tokens,
|
|
53
|
+
temperature=temperature,
|
|
54
|
+
)))
|
|
55
|
+
|
|
56
|
+
platform = PlatformRuntime(
|
|
57
|
+
store=workspace_store,
|
|
58
|
+
workspace_service=workspace_service,
|
|
59
|
+
plugin_registry=plugin_registry,
|
|
60
|
+
get_current_user=get_current_user,
|
|
61
|
+
workspace_graph=workspace_graph,
|
|
62
|
+
workspace_scope_from_request=workspace_scope_from_request,
|
|
63
|
+
get_tool_permission=get_tool_permission,
|
|
64
|
+
hooks=hooks,
|
|
65
|
+
llm_generate=_llm_generate_sync,
|
|
66
|
+
llm_available=lambda: bool(getattr(model_router, "current_model_id", None)),
|
|
67
|
+
agent_registry=agent_registry,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
automation_runtime = build_automation_runtime(
|
|
71
|
+
store=workspace_store,
|
|
72
|
+
platform=platform,
|
|
73
|
+
data_dir=data_dir,
|
|
74
|
+
workspace_graph=workspace_graph,
|
|
75
|
+
append_audit_event=append_audit_event,
|
|
76
|
+
hooks=hooks,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
"_llm_generate_sync": _llm_generate_sync,
|
|
81
|
+
"PLATFORM": platform,
|
|
82
|
+
"_automation_runtime": automation_runtime,
|
|
83
|
+
"REVIEW_QUEUE": automation_runtime["REVIEW_QUEUE"],
|
|
84
|
+
"TRIGGER_SERVICE": automation_runtime["TRIGGER_SERVICE"],
|
|
85
|
+
"AGENT_RUNTIME": automation_runtime["AGENT_RUNTIME"],
|
|
86
|
+
"RUN_EXECUTOR": automation_runtime["RUN_EXECUTOR"],
|
|
87
|
+
}
|
|
@@ -10,6 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
+
from latticeai.services.router_context import InteractionRouterContext
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def build_static_routes_bundle(
|
|
15
17
|
*,
|
|
@@ -412,45 +414,61 @@ def register_health_and_model_routers(
|
|
|
412
414
|
def register_interaction_routers(
|
|
413
415
|
app: Any,
|
|
414
416
|
*,
|
|
417
|
+
interaction_context: InteractionRouterContext | None = None,
|
|
415
418
|
create_chat_router: Any,
|
|
416
|
-
context: Any,
|
|
419
|
+
context: Any = None,
|
|
417
420
|
create_search_router: Any,
|
|
418
|
-
search_service: Any,
|
|
419
|
-
allowed_workspaces_for: Any,
|
|
420
|
-
require_user: Any,
|
|
421
|
-
embedding_info: Any,
|
|
421
|
+
search_service: Any = None,
|
|
422
|
+
allowed_workspaces_for: Any = None,
|
|
423
|
+
require_user: Any = None,
|
|
424
|
+
embedding_info: Any = None,
|
|
422
425
|
create_tools_router: Any,
|
|
423
|
-
ingestion_pipeline: Any,
|
|
424
|
-
config: Any,
|
|
425
|
-
data_dir: Any,
|
|
426
|
-
static_dir: Any,
|
|
427
|
-
model_router: Any,
|
|
428
|
-
require_admin: Any,
|
|
429
|
-
get_current_user: Any,
|
|
430
|
-
clear_history: Any,
|
|
431
|
-
append_audit_event: Any,
|
|
432
|
-
enforce_rate_limit: Any,
|
|
433
|
-
bytes_match_extension: Any,
|
|
434
|
-
classify_sensitive_message: Any,
|
|
435
|
-
save_to_history: Any,
|
|
436
|
-
enable_graph: bool,
|
|
437
|
-
knowledge_graph: Any,
|
|
438
|
-
require_graph: Any,
|
|
439
|
-
local_kg_watcher: Any,
|
|
440
|
-
load_mcp_installs: Any,
|
|
441
|
-
recommend_mcps: Any,
|
|
442
|
-
install_mcp: Any,
|
|
443
|
-
mcp_public_item: Any,
|
|
444
|
-
hooks: Any,
|
|
426
|
+
ingestion_pipeline: Any = None,
|
|
427
|
+
config: Any = None,
|
|
428
|
+
data_dir: Any = None,
|
|
429
|
+
static_dir: Any = None,
|
|
430
|
+
model_router: Any = None,
|
|
431
|
+
require_admin: Any = None,
|
|
432
|
+
get_current_user: Any = None,
|
|
433
|
+
clear_history: Any = None,
|
|
434
|
+
append_audit_event: Any = None,
|
|
435
|
+
enforce_rate_limit: Any = None,
|
|
436
|
+
bytes_match_extension: Any = None,
|
|
437
|
+
classify_sensitive_message: Any = None,
|
|
438
|
+
save_to_history: Any = None,
|
|
439
|
+
enable_graph: bool | None = None,
|
|
440
|
+
knowledge_graph: Any = None,
|
|
441
|
+
require_graph: Any = None,
|
|
442
|
+
local_kg_watcher: Any = None,
|
|
443
|
+
load_mcp_installs: Any = None,
|
|
444
|
+
recommend_mcps: Any = None,
|
|
445
|
+
install_mcp: Any = None,
|
|
446
|
+
mcp_public_item: Any = None,
|
|
447
|
+
hooks: Any = None,
|
|
445
448
|
create_hooks_router: Any,
|
|
446
449
|
create_agent_registry_router: Any,
|
|
447
|
-
agent_registry: Any,
|
|
450
|
+
agent_registry: Any = None,
|
|
448
451
|
create_memory_router: Any,
|
|
449
|
-
memory_service: Any,
|
|
450
|
-
platform: Any,
|
|
452
|
+
memory_service: Any = None,
|
|
453
|
+
platform: Any = None,
|
|
451
454
|
) -> tuple[Any, ...]:
|
|
452
455
|
"""Register chat/search/tools/hooks/registry/memory routes in order."""
|
|
453
456
|
|
|
457
|
+
tool_context = None
|
|
458
|
+
if interaction_context is not None:
|
|
459
|
+
context = interaction_context.chat_context
|
|
460
|
+
search_service = interaction_context.search_service
|
|
461
|
+
allowed_workspaces_for = interaction_context.allowed_workspaces_for
|
|
462
|
+
require_user = interaction_context.require_user
|
|
463
|
+
embedding_info = interaction_context.embedding_info
|
|
464
|
+
tool_context = interaction_context.tool_context
|
|
465
|
+
get_current_user = tool_context.get_current_user
|
|
466
|
+
append_audit_event = tool_context.append_audit_event
|
|
467
|
+
hooks = interaction_context.hooks
|
|
468
|
+
agent_registry = interaction_context.agent_registry
|
|
469
|
+
memory_service = interaction_context.memory_service
|
|
470
|
+
platform = interaction_context.platform
|
|
471
|
+
|
|
454
472
|
return register_routers(
|
|
455
473
|
app,
|
|
456
474
|
create_chat_router(context),
|
|
@@ -461,6 +479,7 @@ def register_interaction_routers(
|
|
|
461
479
|
embedding_info=embedding_info,
|
|
462
480
|
),
|
|
463
481
|
create_tools_router(
|
|
482
|
+
tool_context=tool_context,
|
|
464
483
|
ingestion_pipeline=ingestion_pipeline,
|
|
465
484
|
config=config,
|
|
466
485
|
data_dir=data_dir,
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
P-Reinforce Knowledge Gardener β notes capture with a brain-backed memory.
|
|
3
|
+
|
|
4
|
+
v4 (T4.3 garden absorption): the markdown vault is no longer a second brain.
|
|
5
|
+
The vault stays as the user-owned, Obsidian-compatible *mirror* (capability
|
|
6
|
+
preserved), but the Knowledge Graph is authoritative: notes created through
|
|
7
|
+
the API are ingested through the unified pipeline (provenance + hooks), the
|
|
8
|
+
existing vault is imported idempotently, and chat context comes from brain
|
|
9
|
+
queries instead of an O(n) vault scan per message.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import shutil
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
BRAIN_DIR = Path(
|
|
21
|
+
os.getenv("LATTICEAI_OBSIDIAN_VAULT_DIR")
|
|
22
|
+
or os.getenv("LATTICEAI_BRAIN_DIR")
|
|
23
|
+
or Path.home() / ".ltcai-brain"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
STRUCTURE = {
|
|
27
|
+
"10_Wiki": "κ²μ¦λ μ§μ, κ°λ
μ€λͺ
, λ νΌλ°μ€",
|
|
28
|
+
"00_Raw": "μ μ λμ§ μμ μμ λ°μ΄ν°, μμ΄λμ΄ λ©λͺ¨",
|
|
29
|
+
"20_Skills": "μ¬μ¬μ© κ°λ₯ν μ½λ μ€λν«, ν둬ννΈ, μν¬νλ‘",
|
|
30
|
+
"30_Projects": "νλ‘μ νΈλ³ 컨ν
μ€νΈ, μ§ν μν©",
|
|
31
|
+
"40_Log": "λ μ§λ³ μμ
λ‘κ·Έ",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PReinforceGardener:
|
|
36
|
+
def __init__(self, ingestion_pipeline: Any = None, knowledge_graph: Any = None):
|
|
37
|
+
self._pipeline = ingestion_pipeline
|
|
38
|
+
self._kg = knowledge_graph
|
|
39
|
+
self._ensure_structure()
|
|
40
|
+
|
|
41
|
+
def _ensure_structure(self):
|
|
42
|
+
for folder in STRUCTURE:
|
|
43
|
+
(BRAIN_DIR / folder).mkdir(parents=True, exist_ok=True)
|
|
44
|
+
# μΈλ±μ€ νμΌ
|
|
45
|
+
index_path = BRAIN_DIR / "INDEX.md"
|
|
46
|
+
if not index_path.exists():
|
|
47
|
+
index_path.write_text(self._render_index())
|
|
48
|
+
|
|
49
|
+
def _render_index(self) -> str:
|
|
50
|
+
lines = ["# π§ Lattice AI Brain β P-Reinforce Index\n"]
|
|
51
|
+
lines.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n")
|
|
52
|
+
lines.append("\nThis folder is an Obsidian-compatible Markdown vault.\n")
|
|
53
|
+
lines.append("\nThe Knowledge Graph is the authoritative store; this vault is the\nuser-owned markdown mirror of garden notes.\n")
|
|
54
|
+
for folder, desc in STRUCTURE.items():
|
|
55
|
+
lines.append(f"## [{folder}](./{folder}/)\n_{desc}_\n")
|
|
56
|
+
lines.append("## Connector Status\n")
|
|
57
|
+
lines.append(f"- OCR engine: `{'tesseract' if shutil.which('tesseract') else 'not installed'}`\n")
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
|
|
60
|
+
# ββ Classify ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
61
|
+
|
|
62
|
+
def _classify(self, text: str) -> str:
|
|
63
|
+
"""κ°λ¨ν κ·μΉ κΈ°λ° λΆλ₯ (LLM μμ΄λ λμ)"""
|
|
64
|
+
text_lower = text.lower()
|
|
65
|
+
|
|
66
|
+
code_signals = ["def ", "class ", "import ", "```", "function ", "const ", "let ", "var "]
|
|
67
|
+
if any(s in text for s in code_signals):
|
|
68
|
+
return "20_Skills"
|
|
69
|
+
|
|
70
|
+
wiki_signals = ["κ°λ
", "μ리", "μ΄λ", "what is", "how does", "definition", "explanation"]
|
|
71
|
+
if any(s in text_lower for s in wiki_signals):
|
|
72
|
+
return "10_Wiki"
|
|
73
|
+
|
|
74
|
+
project_signals = ["project", "νλ‘μ νΈ", "todo", "task", "μμ
", "κΈ°λ₯", "feature"]
|
|
75
|
+
if any(s in text_lower for s in project_signals):
|
|
76
|
+
return "30_Projects"
|
|
77
|
+
|
|
78
|
+
return "00_Raw"
|
|
79
|
+
|
|
80
|
+
# ββ File Naming βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
81
|
+
|
|
82
|
+
def _make_filename(self, text: str, folder: str) -> str:
|
|
83
|
+
# 첫 μ€μ μ λͺ©μΌλ‘
|
|
84
|
+
first_line = text.strip().split("\n")[0][:60]
|
|
85
|
+
# νμΌλͺ
μμ νκ²
|
|
86
|
+
safe = re.sub(r"[^\w\s-]", "", first_line).strip()
|
|
87
|
+
safe = re.sub(r"\s+", "_", safe)
|
|
88
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
89
|
+
return f"{timestamp}_{safe or 'note'}.md"
|
|
90
|
+
|
|
91
|
+
# ββ Process βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
92
|
+
|
|
93
|
+
async def process(self, raw_data: str, category: Optional[str] = None) -> dict:
|
|
94
|
+
folder = category if category in STRUCTURE else self._classify(raw_data)
|
|
95
|
+
filename = self._make_filename(raw_data, folder)
|
|
96
|
+
filepath = BRAIN_DIR / folder / filename
|
|
97
|
+
|
|
98
|
+
# λ§ν¬λ€μ΄ λ―Έλ¬ (μ¬μ©μ μμ Obsidian νΈν μν°ν©νΈ)
|
|
99
|
+
content = self._wrap_markdown(raw_data, folder)
|
|
100
|
+
filepath.write_text(content, encoding="utf-8")
|
|
101
|
+
|
|
102
|
+
# μ€λ λ‘κ·Έμλ κΈ°λ‘
|
|
103
|
+
self._append_log(raw_data[:200], folder, filename)
|
|
104
|
+
|
|
105
|
+
result = {
|
|
106
|
+
"status": "saved",
|
|
107
|
+
"folder": folder,
|
|
108
|
+
"filename": filename,
|
|
109
|
+
"path": str(filepath),
|
|
110
|
+
"classified_as": folder,
|
|
111
|
+
"description": STRUCTURE[folder],
|
|
112
|
+
}
|
|
113
|
+
# λλ(Knowledge Graph)κ° μ μ μ μ₯μ: ν΅ν© μμ§ νμ΄νλΌμΈμΌλ‘ ingest.
|
|
114
|
+
result.update(self._ingest_note(raw_data, source_uri=str(filepath), folder=folder))
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
def _ingest_note(self, text: str, *, source_uri: str, folder: str, title: Optional[str] = None) -> dict:
|
|
118
|
+
if self._pipeline is None:
|
|
119
|
+
return {"graph": "unavailable", "graph_detail": "ingestion pipeline not wired"}
|
|
120
|
+
try:
|
|
121
|
+
from lattice_brain.ingestion import IngestionItem
|
|
122
|
+
|
|
123
|
+
ingest = self._pipeline.ingest(
|
|
124
|
+
IngestionItem(
|
|
125
|
+
source_type="note",
|
|
126
|
+
title=title or text.strip().split("\n")[0][:80],
|
|
127
|
+
text=text,
|
|
128
|
+
source_uri=source_uri,
|
|
129
|
+
metadata={"garden_folder": folder, "pipeline": "p-reinforce"},
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
if ingest.status != "ok":
|
|
133
|
+
return {"graph": ingest.status, "graph_detail": ingest.detail}
|
|
134
|
+
return {
|
|
135
|
+
"graph": "ok",
|
|
136
|
+
"graph_node_id": ingest.node_id,
|
|
137
|
+
"provenance_id": ingest.provenance_id,
|
|
138
|
+
"duplicate": ingest.duplicate,
|
|
139
|
+
}
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
logging.warning("garden note ingest failed: %s", exc)
|
|
142
|
+
return {"graph": "failed", "graph_detail": str(exc)}
|
|
143
|
+
|
|
144
|
+
def import_vault(self) -> dict:
|
|
145
|
+
"""Idempotent import of every existing vault note into the brain.
|
|
146
|
+
|
|
147
|
+
Content-hash dedup in the store makes re-runs safe; vault files are
|
|
148
|
+
never modified or deleted. INDEX.md and the daily logs are skipped.
|
|
149
|
+
"""
|
|
150
|
+
if self._pipeline is None:
|
|
151
|
+
return {"status": "unavailable", "imported": 0}
|
|
152
|
+
imported = duplicates = failed = 0
|
|
153
|
+
for file_path in sorted(BRAIN_DIR.rglob("*.md")):
|
|
154
|
+
if file_path.name == "INDEX.md" or "40_Log" in file_path.parts:
|
|
155
|
+
continue
|
|
156
|
+
try:
|
|
157
|
+
text = file_path.read_text(encoding="utf-8")
|
|
158
|
+
except Exception:
|
|
159
|
+
failed += 1
|
|
160
|
+
continue
|
|
161
|
+
folder = file_path.parent.name if file_path.parent != BRAIN_DIR else "00_Raw"
|
|
162
|
+
outcome = self._ingest_note(
|
|
163
|
+
text, source_uri=str(file_path), folder=folder, title=file_path.stem
|
|
164
|
+
)
|
|
165
|
+
if outcome.get("graph") == "ok":
|
|
166
|
+
if outcome.get("duplicate"):
|
|
167
|
+
duplicates += 1
|
|
168
|
+
else:
|
|
169
|
+
imported += 1
|
|
170
|
+
else:
|
|
171
|
+
failed += 1
|
|
172
|
+
if imported:
|
|
173
|
+
logging.info("garden: imported %d vault notes into the brain (%d already known)", imported, duplicates)
|
|
174
|
+
return {"status": "ok", "imported": imported, "duplicates": duplicates, "failed": failed}
|
|
175
|
+
|
|
176
|
+
def _wrap_markdown(self, raw: str, folder: str) -> str:
|
|
177
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
178
|
+
first_line = raw.strip().split("\n")[0][:80]
|
|
179
|
+
lines = [
|
|
180
|
+
f"# {first_line}",
|
|
181
|
+
f"\n> π `{folder}` | π {now} | Lattice AI MLX\n",
|
|
182
|
+
"---\n",
|
|
183
|
+
raw,
|
|
184
|
+
"\n\n---",
|
|
185
|
+
"*Auto-organized by P-Reinforce Gardener*",
|
|
186
|
+
]
|
|
187
|
+
return "\n".join(lines)
|
|
188
|
+
|
|
189
|
+
def _append_log(self, preview: str, folder: str, filename: str):
|
|
190
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
191
|
+
log_path = BRAIN_DIR / "40_Log" / f"{today}.md"
|
|
192
|
+
entry = f"\n- [{datetime.now().strftime('%H:%M')}] β `{folder}/{filename}`\n > {preview[:100]}\n"
|
|
193
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
194
|
+
if log_path.stat().st_size == 0 if log_path.exists() else True:
|
|
195
|
+
f.write(f"# π
Log β {today}\n")
|
|
196
|
+
f.write(entry)
|
|
197
|
+
|
|
198
|
+
# ββ Tree ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
199
|
+
|
|
200
|
+
def get_tree(self) -> dict:
|
|
201
|
+
"""μ§μ μ μ νμΌνΈλ¦¬ (λ§ν¬λ€μ΄ λ―Έλ¬ κΈ°μ€)."""
|
|
202
|
+
folders = []
|
|
203
|
+
for folder, desc in STRUCTURE.items():
|
|
204
|
+
folder_path = BRAIN_DIR / folder
|
|
205
|
+
files = []
|
|
206
|
+
if folder_path.exists():
|
|
207
|
+
for file_path in sorted(folder_path.glob("*.md")):
|
|
208
|
+
try:
|
|
209
|
+
stat = file_path.stat()
|
|
210
|
+
files.append({
|
|
211
|
+
"name": file_path.name,
|
|
212
|
+
"size_bytes": stat.st_size,
|
|
213
|
+
"modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
|
|
214
|
+
})
|
|
215
|
+
except OSError:
|
|
216
|
+
continue
|
|
217
|
+
folders.append({"name": folder, "description": desc, "files": files, "count": len(files)})
|
|
218
|
+
return {"root": str(BRAIN_DIR), "folders": folders}
|
|
219
|
+
|
|
220
|
+
def get_relevant_context(self, query: str, limit: int = 3) -> str:
|
|
221
|
+
"""μ§λ¬Έκ³Ό κ΄λ ¨λ μ μ λ
ΈνΈλ₯Ό λλμμ κ²μν΄ μ»¨ν
μ€νΈλ‘ λ°ν.
|
|
222
|
+
|
|
223
|
+
v4: μ±ν
λ§λ€ vault μ 체λ₯Ό rglob νλ O(n) μ€μΊμ λΈλ μΈ κ²μμΌλ‘
|
|
224
|
+
λ체. κ·Έλνκ° μμΌλ©΄(λΉνμ±) κΈ°μ‘΄ νμΌ μ€μΊμΌλ‘ μ μ§νκ² ν΄λ°±.
|
|
225
|
+
"""
|
|
226
|
+
if self._kg is not None:
|
|
227
|
+
try:
|
|
228
|
+
matches = self._kg.search(query, max(limit * 4, 8)).get("matches", [])
|
|
229
|
+
results = []
|
|
230
|
+
for match in matches:
|
|
231
|
+
meta = match.get("metadata") or {}
|
|
232
|
+
if not (meta.get("garden_folder") or meta.get("pipeline") == "p-reinforce"):
|
|
233
|
+
continue
|
|
234
|
+
title = match.get("title") or "note"
|
|
235
|
+
body = match.get("summary") or ""
|
|
236
|
+
results.append(f"--- Document: {title} ---\n{body[:800]}")
|
|
237
|
+
if len(results) >= limit:
|
|
238
|
+
break
|
|
239
|
+
return "\n\n".join(results)
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
logging.debug("garden brain context failed, falling back to vault scan: %s", exc)
|
|
242
|
+
return self._scan_vault_context(query, limit)
|
|
243
|
+
|
|
244
|
+
def _scan_vault_context(self, query: str, limit: int = 3) -> str:
|
|
245
|
+
results = []
|
|
246
|
+
for file_path in BRAIN_DIR.rglob("*.md"):
|
|
247
|
+
if file_path.name == "INDEX.md" or "40_Log" in str(file_path):
|
|
248
|
+
continue
|
|
249
|
+
try:
|
|
250
|
+
content = file_path.read_text(encoding="utf-8")
|
|
251
|
+
keywords = [k for k in re.split(r"\s+", query) if len(k) > 1]
|
|
252
|
+
if any(k.lower() in content.lower() for k in keywords):
|
|
253
|
+
results.append(f"--- Document: {file_path.name} ---\n{content[:800]}")
|
|
254
|
+
if len(results) >= limit:
|
|
255
|
+
break
|
|
256
|
+
except Exception:
|
|
257
|
+
continue
|
|
258
|
+
return "\n\n".join(results)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Typed router assembly contexts for app-factory decomposition."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ToolRouterContext:
|
|
12
|
+
"""Runtime dependencies for direct tool, upload, MCP, and KG routes."""
|
|
13
|
+
|
|
14
|
+
config: Any
|
|
15
|
+
ingestion_pipeline: Any
|
|
16
|
+
data_dir: Path
|
|
17
|
+
static_dir: Path
|
|
18
|
+
model_router: Any
|
|
19
|
+
require_user: Any
|
|
20
|
+
require_admin: Any
|
|
21
|
+
get_current_user: Any
|
|
22
|
+
clear_history: Any
|
|
23
|
+
append_audit_event: Any
|
|
24
|
+
enforce_rate_limit: Any
|
|
25
|
+
bytes_match_extension: Any
|
|
26
|
+
classify_sensitive_message: Any
|
|
27
|
+
save_to_history: Any
|
|
28
|
+
enable_graph: bool
|
|
29
|
+
knowledge_graph: Any
|
|
30
|
+
require_graph: Any
|
|
31
|
+
local_kg_watcher: Any
|
|
32
|
+
load_mcp_installs: Any
|
|
33
|
+
recommend_mcps: Any
|
|
34
|
+
install_mcp: Any
|
|
35
|
+
mcp_public_item: Any
|
|
36
|
+
hooks: Any = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class InteractionRouterContext:
|
|
41
|
+
"""Runtime dependencies for chat/search/tools/hooks/memory route assembly."""
|
|
42
|
+
|
|
43
|
+
chat_context: Any
|
|
44
|
+
search_service: Any
|
|
45
|
+
allowed_workspaces_for: Any
|
|
46
|
+
require_user: Any
|
|
47
|
+
embedding_info: Any
|
|
48
|
+
tool_context: ToolRouterContext
|
|
49
|
+
hooks: Any
|
|
50
|
+
agent_registry: Any
|
|
51
|
+
memory_service: Any
|
|
52
|
+
platform: Any
|