ltcai 11.2.0 → 11.4.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 (247) hide show
  1. package/README.md +46 -53
  2. package/docs/CHANGELOG.md +61 -0
  3. package/docs/COMMUNITY_AND_PLUGINS.md +1 -1
  4. package/docs/DEVELOPMENT.md +1 -1
  5. package/docs/MULTI_AGENT_RUNTIME.md +1 -1
  6. package/docs/ONBOARDING.md +1 -1
  7. package/docs/OPERATIONS.md +6 -2
  8. package/docs/PERMISSION_MODE.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 +2 -2
  12. package/docs/v11.3.0_PLAN.md +202 -0
  13. package/docs/v11.4.0_RUST_FOUNDATION_PLAN.md +176 -0
  14. package/lattice_brain/__init__.py +1 -1
  15. package/lattice_brain/graph/_kg_common/__init__.py +287 -0
  16. package/lattice_brain/graph/_kg_common/extraction.py +516 -0
  17. package/lattice_brain/graph/_kg_common/relations.py +161 -0
  18. package/lattice_brain/graph/_kg_common/text.py +479 -0
  19. package/lattice_brain/graph/discovery_index/__init__.py +35 -0
  20. package/lattice_brain/graph/discovery_index/cleanup.py +182 -0
  21. package/lattice_brain/graph/discovery_index/extract.py +137 -0
  22. package/lattice_brain/graph/discovery_index/scan.py +411 -0
  23. package/lattice_brain/graph/discovery_index/upsert.py +495 -0
  24. package/lattice_brain/graph/projection/__init__.py +42 -0
  25. package/lattice_brain/graph/projection/curation.py +500 -0
  26. package/lattice_brain/graph/{projection.py → projection/v2_schema.py} +15 -477
  27. package/lattice_brain/graph/retrieval/__init__.py +54 -0
  28. package/lattice_brain/graph/retrieval/context.py +197 -0
  29. package/lattice_brain/graph/retrieval/graph_view.py +319 -0
  30. package/lattice_brain/graph/retrieval/hybrid.py +488 -0
  31. package/lattice_brain/graph/retrieval/maintenance.py +121 -0
  32. package/lattice_brain/graph/retrieval/signals.py +95 -0
  33. package/lattice_brain/graph/retrieval_vector/__init__.py +42 -0
  34. package/lattice_brain/graph/retrieval_vector/fingerprint.py +97 -0
  35. package/lattice_brain/graph/retrieval_vector/indexing.py +347 -0
  36. package/lattice_brain/graph/retrieval_vector/search.py +560 -0
  37. package/lattice_brain/graph/retrieval_vector/status.py +374 -0
  38. package/lattice_brain/ingestion/__init__.py +130 -0
  39. package/lattice_brain/ingestion/_contract.py +90 -0
  40. package/lattice_brain/ingestion/constants.py +127 -0
  41. package/lattice_brain/ingestion/folder_scan.py +57 -0
  42. package/lattice_brain/ingestion/folders.py +258 -0
  43. package/lattice_brain/ingestion/hashing.py +26 -0
  44. package/lattice_brain/ingestion/jobs_api.py +107 -0
  45. package/lattice_brain/ingestion/models.py +80 -0
  46. package/lattice_brain/ingestion/pipeline.py +486 -0
  47. package/lattice_brain/ingestion/quality.py +209 -0
  48. package/lattice_brain/ingestion/routing.py +295 -0
  49. package/lattice_brain/multimodal/__init__.py +164 -0
  50. package/lattice_brain/multimodal/audio.py +77 -0
  51. package/lattice_brain/multimodal/common.py +118 -0
  52. package/lattice_brain/multimodal/images.py +498 -0
  53. package/lattice_brain/multimodal/ports.py +169 -0
  54. package/lattice_brain/multimodal/video.py +410 -0
  55. package/lattice_brain/portability/__init__.py +90 -0
  56. package/lattice_brain/portability/_contract.py +42 -0
  57. package/lattice_brain/portability/backups.py +338 -0
  58. package/lattice_brain/portability/bundles.py +136 -0
  59. package/lattice_brain/portability/constants.py +93 -0
  60. package/lattice_brain/portability/fsops.py +138 -0
  61. package/lattice_brain/portability/service.py +41 -0
  62. package/lattice_brain/{portability.py → portability/sharing.py} +44 -677
  63. package/lattice_brain/runtime/__init__.py +1 -1
  64. package/lattice_brain/runtime/multi_agent.py +1 -1
  65. package/latticeai/__init__.py +1 -1
  66. package/latticeai/api/chronicle.py +63 -0
  67. package/latticeai/core/agent/__init__.py +93 -0
  68. package/latticeai/core/agent/_contract.py +79 -0
  69. package/latticeai/core/agent/context.py +57 -0
  70. package/latticeai/core/agent/deps.py +125 -0
  71. package/latticeai/core/agent/execution.py +622 -0
  72. package/latticeai/core/agent/planning.py +145 -0
  73. package/latticeai/core/agent/recovery.py +157 -0
  74. package/latticeai/core/agent/runtime.py +210 -0
  75. package/latticeai/core/agent/verification.py +231 -0
  76. package/latticeai/core/embedding_providers/__init__.py +151 -0
  77. package/latticeai/core/embedding_providers/base.py +199 -0
  78. package/latticeai/core/embedding_providers/captions.py +162 -0
  79. package/latticeai/core/embedding_providers/profiles.py +126 -0
  80. package/latticeai/core/embedding_providers/text.py +350 -0
  81. package/latticeai/core/embedding_providers/vision.py +352 -0
  82. package/latticeai/core/file_generation/__init__.py +115 -0
  83. package/latticeai/core/file_generation/bundles.py +76 -0
  84. package/latticeai/core/file_generation/extraction.py +154 -0
  85. package/latticeai/core/file_generation/inference.py +235 -0
  86. package/latticeai/core/file_generation/orchestration.py +152 -0
  87. package/latticeai/core/file_generation/prompting.py +117 -0
  88. package/latticeai/core/file_generation/repair.py +114 -0
  89. package/latticeai/core/file_generation/sanitize.py +61 -0
  90. package/latticeai/core/file_generation/validation.py +201 -0
  91. package/latticeai/core/legacy_compatibility.py +1 -1
  92. package/latticeai/core/marketplace.py +1 -1
  93. package/latticeai/core/messages.py +9 -0
  94. package/latticeai/core/workspace_os_constants.py +1 -1
  95. package/latticeai/integrations/telegram_bot/__init__.py +123 -0
  96. package/latticeai/integrations/telegram_bot/__main__.py +17 -0
  97. package/latticeai/integrations/telegram_bot/config.py +86 -0
  98. package/latticeai/integrations/telegram_bot/dispatch.py +311 -0
  99. package/latticeai/integrations/telegram_bot/flows.py +478 -0
  100. package/latticeai/integrations/telegram_bot/helpers.py +322 -0
  101. package/latticeai/integrations/telegram_bot/screens.py +394 -0
  102. package/latticeai/models/router/__init__.py +88 -0
  103. package/latticeai/models/router/_contract.py +66 -0
  104. package/latticeai/models/router/branding.py +56 -0
  105. package/latticeai/models/router/catalog.py +69 -0
  106. package/latticeai/models/router/documents.py +199 -0
  107. package/latticeai/models/router/errors.py +37 -0
  108. package/latticeai/models/router/generation.py +258 -0
  109. package/latticeai/models/router/loading.py +291 -0
  110. package/latticeai/models/router/local_models.py +85 -0
  111. package/latticeai/models/router/registry.py +147 -0
  112. package/latticeai/runtime/build_phases/__init__.py +82 -0
  113. package/latticeai/runtime/build_phases/features.py +407 -0
  114. package/latticeai/runtime/build_phases/foundation.py +555 -0
  115. package/latticeai/runtime/build_phases/web.py +492 -0
  116. package/latticeai/runtime/runtime_context.py +1 -0
  117. package/latticeai/services/architecture_readiness.py +48 -19
  118. package/latticeai/services/brain_intelligence/__init__.py +58 -0
  119. package/latticeai/services/brain_intelligence/_contract.py +71 -0
  120. package/latticeai/services/brain_intelligence/consistency.py +193 -0
  121. package/latticeai/services/brain_intelligence/constants.py +47 -0
  122. package/latticeai/services/brain_intelligence/digest.py +258 -0
  123. package/latticeai/services/brain_intelligence/health.py +331 -0
  124. package/latticeai/services/brain_intelligence/proposals.py +264 -0
  125. package/latticeai/services/brain_intelligence/sampling.py +84 -0
  126. package/latticeai/services/brain_intelligence/service.py +48 -0
  127. package/latticeai/services/chronicle.py +557 -0
  128. package/latticeai/services/memory_service/__init__.py +52 -0
  129. package/latticeai/services/memory_service/_contract.py +100 -0
  130. package/latticeai/services/memory_service/brief.py +431 -0
  131. package/latticeai/services/memory_service/constants.py +57 -0
  132. package/latticeai/services/memory_service/maintenance.py +138 -0
  133. package/latticeai/services/memory_service/manager.py +186 -0
  134. package/latticeai/services/memory_service/proof.py +136 -0
  135. package/latticeai/services/memory_service/recall.py +225 -0
  136. package/latticeai/services/memory_service/service.py +48 -0
  137. package/latticeai/services/memory_service/stores.py +110 -0
  138. package/latticeai/services/model_runtime/__init__.py +322 -0
  139. package/latticeai/services/model_runtime/cloud.py +87 -0
  140. package/latticeai/services/model_runtime/download.py +282 -0
  141. package/latticeai/services/model_runtime/engines.py +341 -0
  142. package/latticeai/services/model_runtime/loading.py +178 -0
  143. package/latticeai/services/model_runtime/service.py +129 -0
  144. package/latticeai/services/model_runtime/state.py +131 -0
  145. package/latticeai/services/model_runtime/status.py +255 -0
  146. package/latticeai/services/product_readiness.py +15 -7
  147. package/latticeai/setup/wizard/__init__.py +126 -0
  148. package/latticeai/setup/wizard/catalog.py +172 -0
  149. package/latticeai/setup/wizard/detect.py +323 -0
  150. package/latticeai/setup/wizard/install.py +348 -0
  151. package/latticeai/setup/wizard/paths.py +168 -0
  152. package/latticeai/setup/wizard/plans.py +74 -0
  153. package/latticeai/setup/wizard/recommend.py +320 -0
  154. package/package.json +6 -2
  155. package/scripts/bump_version.py +14 -0
  156. package/scripts/capture_release_evidence.mjs +33 -21
  157. package/scripts/check_current_release_docs.mjs +1 -1
  158. package/scripts/check_i18n_namespace_coverage.mjs +41 -4
  159. package/scripts/check_max_file_lines.mjs +102 -0
  160. package/scripts/check_release_evidence_bound.mjs +30 -15
  161. package/scripts/check_screenshot_pixel_delta.py +34 -4
  162. package/scripts/check_server_i18n.mjs +1 -0
  163. package/scripts/generate_rust_parity_fixtures.py +562 -0
  164. package/scripts/lib/mock_server_fingerprint.mjs +94 -0
  165. package/scripts/release_screen_claims.json +31 -2
  166. package/src-tauri/Cargo.lock +361 -3
  167. package/src-tauri/Cargo.toml +6 -1
  168. package/src-tauri/src/backend.rs +349 -0
  169. package/src-tauri/src/folder.rs +33 -0
  170. package/src-tauri/src/main.rs +97 -399
  171. package/src-tauri/tauri.conf.json +1 -1
  172. package/static/app/asset-manifest.json +41 -37
  173. package/static/app/assets/Act-yYpYnn0v.js +1 -0
  174. package/static/app/assets/AdminConsole-DL3Cr5pL.js +1 -0
  175. package/static/app/assets/{Brain-tuhI4sOC.js → Brain-C1HBN0Wf.js} +2 -2
  176. package/static/app/assets/BrainHome-DoXRhUUC.js +2 -0
  177. package/static/app/assets/BrainSignals-6yR6ir5t.js +1 -0
  178. package/static/app/assets/Capture-CFIRsFNE.js +1 -0
  179. package/static/app/assets/Chronicle-BZbEgiwN.js +1 -0
  180. package/static/app/assets/CommandPalette-D2pMxC2I.js +1 -0
  181. package/static/app/assets/Library-DwO3yZST.js +1 -0
  182. package/static/app/assets/{LivingBrain-DBwhto14.js → LivingBrain-Jn1GK0-S.js} +1 -1
  183. package/static/app/assets/ProductFlow-B-w1R4Oo.js +1 -0
  184. package/static/app/assets/ReviewCard-6B27X8Vg.js +3 -0
  185. package/static/app/assets/System-DW8F-2xL.js +1 -0
  186. package/static/app/assets/arrow-left-DXvKg9U6.js +1 -0
  187. package/static/app/assets/{bot-Cia42c2h.js → bot-IM_E_Y12.js} +1 -1
  188. package/static/app/assets/brain-Ci1CkWjM.js +1 -0
  189. package/static/app/assets/{button-2j2Ijzgq.js → button-COwyqfHM.js} +1 -1
  190. package/static/app/assets/circle-check-DfInj-qD.js +1 -0
  191. package/static/app/assets/{circle-pause-BEFeWpVW.js → circle-pause-DEM4A1Y5.js} +1 -1
  192. package/static/app/assets/{circle-play-ujXMcHxl.js → circle-play-C9djDuLd.js} +1 -1
  193. package/static/app/assets/{cpu-k4awryFq.js → cpu-DFdo1gw-.js} +1 -1
  194. package/static/app/assets/{download-DFbLJ_ig.js → download-SnJL6oqk.js} +1 -1
  195. package/static/app/assets/{folder-open-7y_b6xkM.js → folder-open-CqZeDkjE.js} +1 -1
  196. package/static/app/assets/{hard-drive-Bidh02Kr.js → hard-drive-j1jJXYYf.js} +1 -1
  197. package/static/app/assets/{index-DwDl9-8Y.css → index-BLPb5lmE.css} +1 -1
  198. package/static/app/assets/index-_u5iUHDr.js +10 -0
  199. package/static/app/assets/input-B0lPdRQZ.js +1 -0
  200. package/static/app/assets/link-2-CoFbooHS.js +1 -0
  201. package/static/app/assets/{permissionCopy-Bpb83Hx9.js → permissionCopy-BsyLxtao.js} +1 -1
  202. package/static/app/assets/primitives-DEbN-d6p.js +1 -0
  203. package/static/app/assets/search-BybIWPNd.js +1 -0
  204. package/static/app/assets/{share-2-BH1M-WNi.js → share-2-CVtZ_ewX.js} +1 -1
  205. package/static/app/assets/{shield-alert-BlKdBXcG.js → shield-alert-CBi2GNWM.js} +1 -1
  206. package/static/app/assets/{textarea-CCWbUfFB.js → textarea-DNMpB5ih.js} +1 -1
  207. package/static/app/assets/{useFocusTrap-YdHQ7pJ1.js → useFocusTrap-C83t3GXF.js} +1 -1
  208. package/static/app/assets/useMutation-DtbJDoyz.js +1 -0
  209. package/static/app/assets/{useQuery-CXQiwbVT.js → useQuery-Dcp1OChy.js} +1 -1
  210. package/static/app/assets/utils-BlZr7Pd4.js +4 -0
  211. package/static/app/assets/workspace-jJY4RuAV.js +1 -0
  212. package/static/app/index.html +4 -4
  213. package/static/sw.js +1 -1
  214. package/lattice_brain/graph/_kg_common.py +0 -1331
  215. package/lattice_brain/graph/discovery_index.py +0 -1141
  216. package/lattice_brain/graph/retrieval.py +0 -1120
  217. package/lattice_brain/graph/retrieval_vector.py +0 -1293
  218. package/lattice_brain/ingestion.py +0 -1525
  219. package/lattice_brain/multimodal.py +0 -1258
  220. package/latticeai/core/agent.py +0 -1465
  221. package/latticeai/core/embedding_providers.py +0 -1196
  222. package/latticeai/core/file_generation.py +0 -1047
  223. package/latticeai/integrations/telegram_bot.py +0 -1390
  224. package/latticeai/models/router.py +0 -1007
  225. package/latticeai/runtime/build_phases.py +0 -1450
  226. package/latticeai/services/brain_intelligence.py +0 -1083
  227. package/latticeai/services/memory_service.py +0 -1177
  228. package/latticeai/services/model_runtime.py +0 -1281
  229. package/latticeai/setup/wizard.py +0 -1310
  230. package/static/app/assets/Act-AWf0SAKp.js +0 -1
  231. package/static/app/assets/AdminConsole-D0u8Tiyj.js +0 -1
  232. package/static/app/assets/BrainHome-Ts7G_Ila.js +0 -2
  233. package/static/app/assets/BrainSignals-jMYgQ2Ar.js +0 -1
  234. package/static/app/assets/Capture-CqOSzyPr.js +0 -1
  235. package/static/app/assets/CommandPalette-DC0Bzh-I.js +0 -1
  236. package/static/app/assets/Library-CX-bbhmK.js +0 -1
  237. package/static/app/assets/ProductFlow-BHA2cfKI.js +0 -1
  238. package/static/app/assets/ReviewCard-BUhCKRNM.js +0 -3
  239. package/static/app/assets/System-Bu2t5hn1.js +0 -1
  240. package/static/app/assets/arrow-left-Dzwa5zRb.js +0 -1
  241. package/static/app/assets/brain-DJMoqrwx.js +0 -1
  242. package/static/app/assets/index-BpYkzcVm.js +0 -10
  243. package/static/app/assets/input-DSlJJxRs.js +0 -1
  244. package/static/app/assets/primitives-BCx6TvfG.js +0 -1
  245. package/static/app/assets/search-Cgy8cCFJ.js +0 -1
  246. package/static/app/assets/utils-zqPZJxdx.js +0 -4
  247. package/static/app/assets/workspace-DXTihhfU.js +0 -1
@@ -0,0 +1,311 @@
1
+ """Routing and lifecycle: the poll loop and the two things it can route to.
2
+
3
+ Slash commands (:func:`handle_command`), inline-button callbacks
4
+ (:func:`handle_callback_query`), and the long-poll loop (:func:`run_bot`) that
5
+ feeds both. This is the only layer that decides *what* happens for an incoming
6
+ update; every branch delegates to a screen or a flow and holds no logic of its
7
+ own beyond the routing table.
8
+
9
+ Fail-closed startup, unchanged: no bot token, no allowlist, or no server
10
+ capability means the loop never starts. Every allowed update is dispatched onto
11
+ its own task with an exception logger attached, so one failing handler cannot
12
+ stop the poll.
13
+
14
+ Stubbing note: ``get_updates``, ``TOKEN``, ``download_as_base64``,
15
+ ``process_ai_request``, ``handle_command``, ``handle_callback_query``, and every
16
+ screen/flow handler are read as *this* module's globals, so a test standing in
17
+ for any of them patches this module.
18
+ """
19
+
20
+ import asyncio
21
+
22
+ import httpx
23
+
24
+ from latticeai.core.logging_safety import safe_log_text
25
+
26
+ from .config import TOKEN, _get_server_session, logger
27
+ from .flows import (
28
+ _is_approval_pause,
29
+ ask_ai,
30
+ handle_plan_callback,
31
+ handle_proposal_callback,
32
+ process_ai_request,
33
+ send_plan_for_approval,
34
+ show_review_center,
35
+ )
36
+ from .helpers import (
37
+ allowed_chat_ids,
38
+ answer_callback,
39
+ download_as_base64,
40
+ get_updates,
41
+ is_chat_allowed,
42
+ register_chat_id,
43
+ send_chat_action,
44
+ send_message,
45
+ )
46
+ from .screens import (
47
+ clear_server_history,
48
+ do_unload_model,
49
+ process_document_file,
50
+ send_mcp_tools,
51
+ send_web_link,
52
+ show_graph_stats,
53
+ show_history_summary,
54
+ show_menu,
55
+ show_model_info,
56
+ show_status,
57
+ take_screenshot,
58
+ )
59
+
60
+ # ── Command dispatch ──────────────────────────────────────────────────────────
61
+
62
+ HELP_TEXT = """\
63
+ 🧠 Lattice AI 원격 제어 명령어
64
+
65
+ /menu — 메인 메뉴 (인라인 키보드)
66
+ /status — 서버 상태 및 메모리
67
+ /model — 현재 모델 + 언로드 버튼
68
+ /unload — 모든 모델 언로드 (RAM 해제)
69
+ /graph — Knowledge Graph 통계
70
+ /ss 또는 /screenshot — 현재 화면 캡처
71
+ /history [n] — 최근 대화 n건 (기본 5)
72
+ /clear [n] — 기록 정리 (마지막 n건 유지)
73
+ /web — 웹 UI 링크
74
+ /mcp — MCP 도구 목록
75
+ /review — 변경 제안 검토함 (승인/거절)
76
+ /help — 이 도움말
77
+
78
+ /agent <작업> — 멀티 LLM 에이전트 (계획 확인 후 실행)
79
+ /agent <작업> --exec <모델> --review <모델> — 실행/검토 LLM 지정
80
+
81
+ 일반 텍스트 → AI에게 질문
82
+ 사진 전송 → AI 이미지 분석
83
+ 문서 전송(PDF, DOCX, XLSX, PPTX, TXT, CSV) → Knowledge Graph 수집
84
+ """
85
+
86
+ async def handle_command(client, chat_id, command: str, args: str):
87
+ cmd = command.lower().lstrip("/").split("@")[0]
88
+
89
+ if cmd == "start":
90
+ await send_message(client, chat_id, "🧠 Lattice AI 원격 제어 준비 완료!")
91
+ await show_menu(client, chat_id)
92
+ elif cmd == "menu":
93
+ await show_menu(client, chat_id)
94
+ elif cmd == "status":
95
+ await show_status(client, chat_id)
96
+ elif cmd == "model":
97
+ await show_model_info(client, chat_id)
98
+ elif cmd == "unload":
99
+ await do_unload_model(client, chat_id)
100
+ elif cmd == "graph":
101
+ await show_graph_stats(client, chat_id)
102
+ elif cmd in {"ss", "screenshot"}:
103
+ await take_screenshot(client, chat_id)
104
+ elif cmd == "history":
105
+ n = int(args.strip()) if args.strip().isdigit() else 5
106
+ await show_history_summary(client, chat_id, n)
107
+ elif cmd in {"clear", "clear_history", "forget"}:
108
+ keep = int(args.strip()) if args.strip().isdigit() else 0
109
+ await clear_server_history(client, chat_id, keep)
110
+ elif cmd == "web":
111
+ await send_web_link(client, chat_id)
112
+ elif cmd == "mcp":
113
+ await send_mcp_tools(client, chat_id)
114
+ elif cmd in {"review", "proposals"}:
115
+ await show_review_center(client, chat_id)
116
+ elif cmd in {"help", "h"}:
117
+ await send_message(client, chat_id, HELP_TEXT)
118
+ elif cmd == "agent":
119
+ if not args:
120
+ await send_message(client, chat_id, "사용법: /agent <작업 내용>\n예: /agent 쇼핑몰 메인 페이지 HTML 만들어줘\n\n특정 AI 지정:\n/agent <작업> --exec openai/gpt-4o --review together:Qwen/Qwen3-VL-32B-Instruct")
121
+ return
122
+ # Parse optional --exec / --review flags
123
+ exec_model = reviewing_model = None
124
+ task_text = args
125
+ import re as _re
126
+ em = _re.search(r'--exec\s+(\S+)', args)
127
+ rm = _re.search(r'--review\s+(\S+)', args)
128
+ if em:
129
+ exec_model = em.group(1)
130
+ task_text = task_text.replace(em.group(0), "").strip()
131
+ if rm:
132
+ reviewing_model = rm.group(1)
133
+ task_text = task_text.replace(rm.group(0), "").strip()
134
+ await send_chat_action(client, chat_id, "typing")
135
+ data = await ask_ai(client, task_text, agent_mode=True,
136
+ executing_model=exec_model, reviewing_model=reviewing_model)
137
+ if isinstance(data, dict) and _is_approval_pause(data):
138
+ await send_plan_for_approval(client, chat_id, data)
139
+ else:
140
+ ans = data.get("response", str(data)) if isinstance(data, dict) else str(data)
141
+ await send_message(client, chat_id, ans)
142
+ else:
143
+ await send_message(client, chat_id, f"알 수 없는 명령어: /{cmd}\n/help 로 명령어 목록을 확인하세요.")
144
+
145
+ # ── Callback query handler ────────────────────────────────────────────────────
146
+
147
+ async def handle_callback_query(client, callback_query):
148
+ cq_id = callback_query.get("id", "")
149
+ chat_id = (
150
+ (callback_query.get("message") or {}).get("chat") or {}
151
+ ).get("id")
152
+ data = callback_query.get("data", "")
153
+
154
+ if not is_chat_allowed(chat_id):
155
+ logger.warning("허용되지 않은 텔레그램 callback 차단: %s", safe_log_text(chat_id))
156
+ await answer_callback(client, cq_id, "허용되지 않은 채팅입니다.")
157
+ return
158
+
159
+ await answer_callback(client, cq_id)
160
+
161
+ if data == "cmd:status":
162
+ await show_status(client, chat_id)
163
+ elif data == "cmd:model":
164
+ await show_model_info(client, chat_id)
165
+ elif data == "cmd:graph":
166
+ await show_graph_stats(client, chat_id)
167
+ elif data == "cmd:screenshot":
168
+ await take_screenshot(client, chat_id)
169
+ elif data == "cmd:history":
170
+ await show_history_summary(client, chat_id, 5)
171
+ elif data == "cmd:clear":
172
+ await clear_server_history(client, chat_id, 0)
173
+ elif data == "cmd:web":
174
+ await send_web_link(client, chat_id)
175
+ elif data == "cmd:mcp":
176
+ await send_mcp_tools(client, chat_id)
177
+ elif data == "cmd:review":
178
+ await show_review_center(client, chat_id)
179
+ elif data == "cmd:menu":
180
+ await show_menu(client, chat_id)
181
+ elif data.startswith("model:unload:"):
182
+ model_id = data[len("model:unload:"):]
183
+ await do_unload_model(client, chat_id, model_id)
184
+ elif data.startswith("plan:"):
185
+ task = asyncio.create_task(handle_plan_callback(client, chat_id, data))
186
+ task.add_done_callback(_log_task_exception)
187
+ elif data.startswith("proposal:"):
188
+ task = asyncio.create_task(handle_proposal_callback(client, chat_id, data))
189
+ task.add_done_callback(_log_task_exception)
190
+
191
+ # ── Main loop ─────────────────────────────────────────────────────────────────
192
+
193
+ async def run_bot():
194
+ if not TOKEN:
195
+ logger.warning("LATTICEAI_TELEGRAM_BOT_TOKEN이 설정되지 않아 텔레그램 봇을 시작하지 않습니다.")
196
+ return
197
+ if not allowed_chat_ids():
198
+ logger.error(
199
+ "LATTICEAI_TELEGRAM_ALLOWED_CHAT_IDS가 없어 텔레그램 봇을 시작하지 않습니다."
200
+ )
201
+ return
202
+ if not _get_server_session():
203
+ logger.error(
204
+ "LATTICEAI_SERVER_SESSION_TOKEN이 없어 텔레그램 봇을 시작하지 않습니다."
205
+ )
206
+ return
207
+
208
+ logger.info("🚀 비동기 텔레그램 봇 모드 시작!")
209
+ last_update_id = None
210
+ retry_delay = 1
211
+
212
+ async with httpx.AsyncClient() as client:
213
+ while True:
214
+ try:
215
+ updates = await get_updates(client, last_update_id)
216
+ retry_delay = 1
217
+ except Exception as e:
218
+ logger.error("get_updates 실패: %s", safe_log_text(e))
219
+ await asyncio.sleep(min(retry_delay, 30))
220
+ retry_delay = min(retry_delay * 2, 30)
221
+ continue
222
+
223
+ if not (updates and updates.get("ok")):
224
+ await asyncio.sleep(0.5)
225
+ continue
226
+
227
+ for update in updates.get("result", []):
228
+ try:
229
+ last_update_id = update.get("update_id") + 1
230
+
231
+ # ── Callback query (inline button press) ──────────────────
232
+ if "callback_query" in update:
233
+ task = asyncio.create_task(handle_callback_query(client, update["callback_query"]))
234
+ task.add_done_callback(_log_task_exception)
235
+ continue
236
+
237
+ if "message" not in update:
238
+ continue
239
+
240
+ msg = update["message"]
241
+ chat_id = msg["chat"]["id"]
242
+ if not is_chat_allowed(chat_id):
243
+ logger.warning(
244
+ "허용되지 않은 텔레그램 메시지 차단: %s",
245
+ safe_log_text(chat_id),
246
+ )
247
+ continue
248
+ register_chat_id(chat_id)
249
+ text = msg.get("text", "")
250
+ caption = msg.get("caption", "")
251
+
252
+ # ── Photo → vision AI ─────────────────────────────────────
253
+ if "photo" in msg:
254
+ file_id = msg["photo"][-1]["file_id"]
255
+ await send_message(client, chat_id, "📸 사진을 받았습니다. 분석을 시작합니다...")
256
+ image_data = await download_as_base64(client, file_id)
257
+ prompt = caption or text or "이 이미지를 분석해줘."
258
+ task = asyncio.create_task(process_ai_request(client, chat_id, prompt, image_data))
259
+ task.add_done_callback(_log_task_exception)
260
+ continue
261
+
262
+ # ── Document ──────────────────────────────────────────────
263
+ if "document" in msg:
264
+ doc = msg["document"]
265
+ mime = doc.get("mime_type", "")
266
+ filename = doc.get("file_name", "file")
267
+ if mime.startswith("image/"):
268
+ image_data = await download_as_base64(client, doc["file_id"])
269
+ prompt = caption or text or "이 이미지를 분석해줘."
270
+ task = asyncio.create_task(process_ai_request(client, chat_id, prompt, image_data))
271
+ else:
272
+ await send_message(client, chat_id, f"📄 {filename} 을 Knowledge Graph에 수집합니다...")
273
+ task = asyncio.create_task(
274
+ process_document_file(client, chat_id, doc["file_id"], filename, caption)
275
+ )
276
+ task.add_done_callback(_log_task_exception)
277
+ continue
278
+
279
+ # ── Voice / audio ─────────────────────────────────────────
280
+ if "voice" in msg or "audio" in msg:
281
+ await send_message(
282
+ client, chat_id,
283
+ "🎤 음성 메시지를 받았습니다. 현재 음성 인식(Whisper)이 설정되어 있지 않습니다.\n"
284
+ "텍스트로 질문을 보내주세요."
285
+ )
286
+ continue
287
+
288
+ if not text:
289
+ continue
290
+
291
+ # ── Commands ──────────────────────────────────────────────
292
+ if text.startswith("/"):
293
+ parts = text.split(None, 1)
294
+ command = parts[0]
295
+ args = parts[1] if len(parts) > 1 else ""
296
+ task = asyncio.create_task(handle_command(client, chat_id, command, args))
297
+ task.add_done_callback(_log_task_exception)
298
+ continue
299
+
300
+ # ── Plain text → AI ───────────────────────────────────────
301
+ task = asyncio.create_task(process_ai_request(client, chat_id, text))
302
+ task.add_done_callback(_log_task_exception)
303
+
304
+ except Exception as e:
305
+ logger.error("업데이트 처리 중 예외: %s", safe_log_text(e))
306
+
307
+ await asyncio.sleep(0.5)
308
+
309
+ def _log_task_exception(task):
310
+ if not task.cancelled() and task.exception():
311
+ logger.error("백그라운드 태스크 예외: %s", safe_log_text(task.exception()))