codebuff-agent 2.1.74__py3-none-any.whl

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 (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,113 @@
1
+ """Centralized log path resolution.
2
+
3
+ All log file names include project code and user ID for traceability.
4
+ Environment variables:
5
+ - HDSP_PRJ_ID: project code
6
+ - JUPYTERHUB_USER / HDSP_BDP_ID: user identifier
7
+ - HDSP_USER_TEAM: team name (for idle shutdown logs)
8
+ - ANALYTICS_LOG_DIR: override for analytics log directory
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime
14
+ import json
15
+ import logging
16
+ import os
17
+ from pathlib import Path
18
+
19
+ from .team_access import resolve_team
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _PROJECT = os.environ.get("HDSP_PRJ_ID", "unknown")
24
+ _USER = os.environ.get("JUPYTERHUB_USER", "") or os.environ.get(
25
+ "HDSP_BDP_ID", "unknown"
26
+ )
27
+ _TEAM = resolve_team()
28
+
29
+
30
+ def get_log_dir() -> Path:
31
+ """Return the log directory, creating it if necessary.
32
+
33
+ Resolution order:
34
+ 1. ANALYTICS_LOG_DIR env var
35
+ 2. /home/sagemaker-user/.action_logs (SageMaker โ€” auto-created)
36
+ 3. <project_root>/logs (extensions/jupyter/jupyter_ext -> parents[3])
37
+ """
38
+ env = os.environ.get("ANALYTICS_LOG_DIR")
39
+ if env:
40
+ d = Path(env)
41
+ else:
42
+ sagemaker = Path("/home/sagemaker-user")
43
+ if sagemaker.is_dir():
44
+ d = sagemaker / ".action_logs"
45
+ else:
46
+ d = Path(__file__).resolve().parents[3] / ".action_logs"
47
+ try:
48
+ d.mkdir(parents=True, exist_ok=True)
49
+ return d
50
+ except OSError:
51
+ logger.warning("Failed to create log directory: %s", d)
52
+ # Local fallback
53
+ fallback = Path(__file__).resolve().parents[3] / ".action_logs"
54
+ try:
55
+ fallback.mkdir(parents=True, exist_ok=True)
56
+ return fallback
57
+ except OSError:
58
+ logger.warning("Log directory unavailable: %s", fallback)
59
+ return fallback
60
+
61
+
62
+ _LOG_DIR = get_log_dir()
63
+
64
+
65
+ def get_debug_log_path() -> str:
66
+ """Return path for agent_debug_{project}_{user}.log."""
67
+ return str(_LOG_DIR / f"agent_debug_{_PROJECT}_{_USER}.log")
68
+
69
+
70
+ def get_structured_log_path() -> Path:
71
+ """Return path for agent_structured_{project}_{user}.jsonl."""
72
+ return _LOG_DIR / f"agent_structured_{_PROJECT}_{_USER}.jsonl"
73
+
74
+
75
+ def get_analytics_log_path(event_type: str, date_str: str) -> Path:
76
+ """Return path for analytics_{type}_{project}_{user}_{YYYYMMDD}.jsonl."""
77
+ return _LOG_DIR / f"analytics_{event_type}_{_PROJECT}_{_USER}_{date_str}.jsonl"
78
+
79
+
80
+ def write_idle_shutdown_log(
81
+ idle_seconds: float,
82
+ timeout_setting: int,
83
+ trigger_source: str,
84
+ ) -> None:
85
+ """Write idle shutdown event to a JSONL file.
86
+
87
+ File: idle_shutdown_{project}_{user}_{YYYYMMDD_HHMMSS}.jsonl
88
+ """
89
+ now = datetime.datetime.now()
90
+ ts_file = now.strftime("%Y%m%d_%H%M%S")
91
+ filename = f"idle_shutdown_{_PROJECT}_{_USER}_{ts_file}.jsonl"
92
+ path = _LOG_DIR / filename
93
+ event = {
94
+ "event": "idle_shutdown",
95
+ "ts": now.isoformat(timespec="seconds"),
96
+ "project": _PROJECT,
97
+ "user_id": _USER,
98
+ "team": _TEAM,
99
+ "idle_seconds": round(idle_seconds, 1),
100
+ "timeout_setting": timeout_setting,
101
+ "trigger_source": trigger_source,
102
+ }
103
+ try:
104
+ with open(path, "a", encoding="utf-8") as f:
105
+ f.write(json.dumps(event, ensure_ascii=False) + "\n")
106
+ except OSError:
107
+ logger.warning("Failed to write idle shutdown log: %s", path)
108
+
109
+ # pod ๊ฐ€ ๊ณง ์ฃฝ์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ๋งŒ ๋™๊ธฐ ํ”Œ๋Ÿฌ์‹œ๋‹ค (์ด์Šˆ #30). ์ง€์—ฐ ์ƒํ•œ์€ 2์ดˆ์ด๊ณ 
110
+ # ์‹คํŒจํ•ด๋„ ์˜ˆ์™ธ๊ฐ€ ๋‚˜๊ฐ€์ง€ ์•Š๋Š”๋‹ค โ€” ์ข…๋ฃŒ ๊ฒฝ๋กœ๋ฅผ ๋ง‰์œผ๋ฉด ์•ˆ ๋œ๋‹ค.
111
+ from .analytics_ingest import enqueue_session_end
112
+
113
+ enqueue_session_end(event)
@@ -0,0 +1,519 @@
1
+ """
2
+ Resource Usage Utilities for the Jupyter server host.
3
+
4
+ Collects CPU, memory, disk, and GPU usage to guide client-side execution.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ from typing import Optional
13
+
14
+ try:
15
+ import psutil
16
+ except ImportError: # pragma: no cover - optional dependency fallback
17
+ psutil = None
18
+
19
+
20
+ def _read_cgroup_value(path: str) -> Optional[str]:
21
+ try:
22
+ with open(path, "r", encoding="utf-8") as handle:
23
+ return handle.read().strip()
24
+ except Exception:
25
+ return None
26
+
27
+
28
+ def _format_gb(value_bytes: float) -> float:
29
+ return round(value_bytes / (1024**3), 2)
30
+
31
+
32
+ def _safe_float(value: Optional[str]) -> Optional[float]:
33
+ if value is None:
34
+ return None
35
+ try:
36
+ return float(value)
37
+ except (TypeError, ValueError):
38
+ return None
39
+
40
+
41
+ def _safe_cpu_count() -> Optional[int]:
42
+ count = os.cpu_count()
43
+ return int(count) if count is not None else None
44
+
45
+
46
+ def get_integrated_resources(
47
+ env_type: str = "auto", workspace_root: str = "."
48
+ ) -> dict[str, object]:
49
+ """
50
+ Collect resource usage snapshot for the Jupyter server host.
51
+
52
+ Args:
53
+ env_type: "auto", "pod", or "host" (auto detects Kubernetes)
54
+ workspace_root: Path used to compute disk usage
55
+
56
+ Returns:
57
+ JSON-serializable resource snapshot dict.
58
+ """
59
+ is_pod = False
60
+ if env_type == "auto":
61
+ is_pod = os.path.exists("/var/run/secrets/kubernetes.io") or bool(
62
+ os.environ.get("KUBERNETES_SERVICE_HOST")
63
+ )
64
+ else:
65
+ is_pod = env_type.lower() == "pod"
66
+
67
+ environment = "Kubernetes Pod" if is_pod else "Host/VM"
68
+ cpu: dict[str, Optional[float]] = {"cores": None, "usage_percent": None}
69
+ memory: dict[str, Optional[float]] = {
70
+ "total_gb": None,
71
+ "available_gb": None,
72
+ "used_gb": None,
73
+ }
74
+ disk: dict[str, Optional[object]] = {
75
+ "path": None,
76
+ "total_gb": None,
77
+ "free_gb": None,
78
+ "used_gb": None,
79
+ }
80
+ gpus: list[dict[str, Optional[object]]] = []
81
+ gpu_status = "not_detected"
82
+
83
+ if is_pod:
84
+ try:
85
+ cpu_max = _read_cgroup_value("/sys/fs/cgroup/cpu.max")
86
+ if cpu_max:
87
+ quota, period = cpu_max.split()
88
+ if quota != "max":
89
+ cpu_limit = float(quota) / float(period)
90
+ else:
91
+ cpu_limit = psutil.cpu_count() if psutil else _safe_cpu_count() or 0
92
+ else:
93
+ cpu_limit = psutil.cpu_count() if psutil else _safe_cpu_count() or 0
94
+
95
+ mem_limit_raw = _read_cgroup_value("/sys/fs/cgroup/memory.max")
96
+ mem_current_raw = _read_cgroup_value("/sys/fs/cgroup/memory.current")
97
+
98
+ if mem_limit_raw and mem_limit_raw != "max":
99
+ mem_limit_gb = _format_gb(float(mem_limit_raw))
100
+ else:
101
+ mem_limit_gb = (
102
+ _format_gb(psutil.virtual_memory().total) if psutil else 0.0
103
+ )
104
+
105
+ mem_used_gb = _format_gb(float(mem_current_raw)) if mem_current_raw else 0.0
106
+
107
+ cpu["cores"] = round(cpu_limit, 2)
108
+ memory["total_gb"] = mem_limit_gb
109
+ memory["used_gb"] = mem_used_gb
110
+ if mem_limit_gb is not None and mem_used_gb is not None:
111
+ memory["available_gb"] = round(mem_limit_gb - mem_used_gb, 2)
112
+ except Exception:
113
+ cpu_count = psutil.cpu_count() if psutil else _safe_cpu_count()
114
+ cpu["cores"] = float(cpu_count) if cpu_count is not None else None
115
+ if psutil:
116
+ vm = psutil.virtual_memory()
117
+ memory["total_gb"] = _format_gb(vm.total)
118
+ memory["available_gb"] = _format_gb(vm.available)
119
+ memory["used_gb"] = _format_gb(vm.total - vm.available)
120
+ else:
121
+ cpu_count = psutil.cpu_count() if psutil else _safe_cpu_count()
122
+ cpu_percent = psutil.cpu_percent() if psutil else None
123
+ cpu["cores"] = float(cpu_count) if cpu_count is not None else None
124
+ cpu["usage_percent"] = cpu_percent
125
+ if psutil:
126
+ vm = psutil.virtual_memory()
127
+ memory["total_gb"] = _format_gb(vm.total)
128
+ memory["available_gb"] = _format_gb(vm.available)
129
+ memory["used_gb"] = _format_gb(vm.total - vm.available)
130
+
131
+ disk_path = workspace_root if workspace_root else "."
132
+ if not os.path.exists(disk_path):
133
+ disk_path = "."
134
+ disk["path"] = os.path.abspath(disk_path)
135
+ try:
136
+ total, used, free = shutil.disk_usage(disk_path)
137
+ disk["total_gb"] = _format_gb(total)
138
+ disk["free_gb"] = _format_gb(free)
139
+ disk["used_gb"] = _format_gb(used)
140
+ except Exception:
141
+ pass
142
+
143
+ if shutil.which("nvidia-smi"):
144
+ gpu_status = "available"
145
+ try:
146
+ result = subprocess.check_output(
147
+ [
148
+ "nvidia-smi",
149
+ "--query-gpu=name,utilization.gpu,memory.used,memory.total",
150
+ "--format=csv,noheader,nounits",
151
+ ],
152
+ encoding="utf-8",
153
+ timeout=2,
154
+ ).strip()
155
+ if result:
156
+ for line in result.split("\n"):
157
+ name, gpu_util, mem_used, mem_total = [
158
+ value.strip() for value in line.split(",")
159
+ ]
160
+ gpus.append(
161
+ {
162
+ "name": name,
163
+ "utilization_percent": _safe_float(gpu_util),
164
+ "memory_used_mb": _safe_float(mem_used),
165
+ "memory_total_mb": _safe_float(mem_total),
166
+ }
167
+ )
168
+ except Exception:
169
+ gpu_status = "unavailable"
170
+ else:
171
+ gpu_status = "not_detected"
172
+
173
+ return {
174
+ "environment": environment,
175
+ "cpu": cpu,
176
+ "memory": memory,
177
+ "disk": disk,
178
+ "gpus": gpus,
179
+ "gpu_status": gpu_status,
180
+ }
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # Resource Tier Classification & Guidance Generation
185
+ # ---------------------------------------------------------------------------
186
+
187
+
188
+ def _classify_cpu(cores: Optional[float]) -> str:
189
+ """Classify CPU into a tier based on core count."""
190
+ if cores is None:
191
+ return "unknown"
192
+ c = int(cores)
193
+ if c <= 3:
194
+ return "low"
195
+ if c <= 7:
196
+ return "medium"
197
+ if c <= 31:
198
+ return "high"
199
+ return "very_high"
200
+
201
+
202
+ def _classify_memory(total_gb: Optional[float]) -> str:
203
+ """Classify memory into a tier based on total capacity."""
204
+ if total_gb is None:
205
+ return "unknown"
206
+ if total_gb < 4:
207
+ return "constrained"
208
+ if total_gb < 16:
209
+ return "limited"
210
+ if total_gb < 64:
211
+ return "comfortable"
212
+ if total_gb < 256:
213
+ return "large"
214
+ return "very_large"
215
+
216
+
217
+ def _classify_single_gpu(name: str, vram_mb: Optional[float]) -> str:
218
+ """Classify a single GPU into a tier based on model name and VRAM."""
219
+ vram_gb = (vram_mb or 0) / 1024
220
+ upper = name.upper()
221
+
222
+ # Model-specific overrides (inference-optimized GPUs are entry-tier for training)
223
+ if "L4" in upper or "T4" in upper:
224
+ return "entry"
225
+ if "H200" in upper or "H100" in upper:
226
+ return "ultra"
227
+ # A100/V100: differentiate by VRAM
228
+ if "A100" in upper:
229
+ return "ultra" if vram_gb >= 48 else "high"
230
+ if "V100" in upper:
231
+ return "high" if vram_gb >= 24 else "mid"
232
+
233
+ # Fallback: classify by VRAM size
234
+ if vram_gb < 16:
235
+ return "entry"
236
+ if vram_gb < 24:
237
+ return "mid"
238
+ if vram_gb < 48:
239
+ return "high"
240
+ return "ultra"
241
+
242
+
243
+ def _classify_gpu(gpus: list) -> dict:
244
+ """Classify GPU setup into a tier dict."""
245
+ if not gpus:
246
+ return {"tier": "none", "count": 0, "details": []}
247
+
248
+ details = []
249
+ for g in gpus:
250
+ name = g.get("name", "GPU")
251
+ vram_mb = g.get("memory_total_mb")
252
+ tier = _classify_single_gpu(name, vram_mb)
253
+ details.append({"name": name, "vram_mb": vram_mb, "tier": tier})
254
+
255
+ count = len(details)
256
+ # Overall GPU tier: use the best single-GPU tier
257
+ tier_order = ["entry", "mid", "high", "ultra"]
258
+ best_tier = max(details, key=lambda d: tier_order.index(d["tier"]))["tier"]
259
+
260
+ return {
261
+ "tier": best_tier,
262
+ "count": count,
263
+ "multi_gpu": count > 1,
264
+ "details": details,
265
+ }
266
+
267
+
268
+ def classify_resource_tiers(data: dict) -> dict:
269
+ """Classify hardware resources into granular tiers for LLM guidance.
270
+
271
+ Args:
272
+ data: Output of ``get_integrated_resources()``.
273
+
274
+ Returns:
275
+ Dict with ``cpu_tier``, ``mem_tier``, ``gpu`` (sub-dict), and raw values.
276
+ """
277
+ cpu = data.get("cpu", {})
278
+ mem = data.get("memory", {})
279
+ gpus = data.get("gpus", [])
280
+
281
+ cores = cpu.get("cores")
282
+ total_gb = mem.get("total_gb")
283
+ avail_gb = mem.get("available_gb")
284
+
285
+ return {
286
+ "cpu_tier": _classify_cpu(cores),
287
+ "mem_tier": _classify_memory(total_gb),
288
+ "gpu": _classify_gpu(gpus),
289
+ "cores": cores,
290
+ "total_gb": total_gb,
291
+ "avail_gb": avail_gb,
292
+ }
293
+
294
+
295
+ # --- Guidance text generators ------------------------------------------------
296
+
297
+ _CPU_GUIDANCE = {
298
+ "low": (
299
+ "- CPU {cores}์ฝ”์–ด (์ €์‚ฌ์–‘): ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ด๋“์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. "
300
+ "์•Œ๊ณ ๋ฆฌ์ฆ˜ ๋ณต์žก๋„ ๊ฐœ์„ (O(nยฒ)โ†’O(n log n))๊ณผ ๋ฒกํ„ฐ ์—ฐ์‚ฐ(numpy/pandas vectorized ops)์— ์ง‘์ค‘ํ•˜์„ธ์š”. "
301
+ "multiprocessing/joblib ์‚ฌ์šฉ์„ ์ง€์–‘ํ•˜์„ธ์š”."
302
+ ),
303
+ "medium": (
304
+ "- CPU {cores}์ฝ”์–ด (์ค‘๊ฐ„): joblib(n_jobs={n_jobs})๋กœ sklearn ๊ต์ฐจ๊ฒ€์ฆยทํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ํƒ์ƒ‰์„ ๋ณ‘๋ ฌํ™”ํ•˜์„ธ์š”. "
305
+ "CPU ๋ฐ”์šด๋“œ ์ „์ฒ˜๋ฆฌ๋Š” multiprocessing.Pool(processes={n_jobs})๋กœ ๋ถ„ํ•  ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. "
306
+ "I/O ๋ฐ”์šด๋“œ ์ž‘์—…์€ threading ๋˜๋Š” asyncio๊ฐ€ ๋” ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค."
307
+ ),
308
+ "high": (
309
+ "- CPU {cores}์ฝ”์–ด (๊ณ ์‚ฌ์–‘): joblib(n_jobs=-1)๋กœ ๋ชจ๋“  sklearn ๋ณ‘๋ ฌ ์—ฐ์‚ฐ์„ ์ตœ๋Œ€ํ™”ํ•˜์„ธ์š”. "
310
+ "๋Œ€์šฉ๋Ÿ‰ DataFrame ์ฒ˜๋ฆฌ ์‹œ Dask LocalCluster(n_workers={workers}, threads_per_worker=2)๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”. "
311
+ "Polars์˜ ์ž๋™ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ์ด {cores}์ฝ”์–ด๋ฅผ ์™„์ „ํžˆ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. "
312
+ "๊ต์ฐจ๊ฒ€์ฆยท์•™์ƒ๋ธ” ํ•™์Šต์„ ๋ณ‘๋ ฌ ์‹คํ–‰ํ•˜์„ธ์š”."
313
+ ),
314
+ "very_high": (
315
+ "- CPU {cores}์ฝ”์–ด (์„œ๋ฒ„๊ธ‰): Dask/Ray ๋ถ„์‚ฐ ์Šค์ผ€์ค„๋Ÿฌ๋กœ DataFrame ์—ฐ์‚ฐ์„ ๋ณ‘๋ ฌํ™”ํ•˜์„ธ์š”. "
316
+ "Ray๋ฅผ ์‚ฌ์šฉํ•œ ๋ถ„์‚ฐ ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ํŠœ๋‹(ray.tune)์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. "
317
+ "Spark DataFrame์œผ๋กœ ๋Œ€๊ทœ๋ชจ ETL ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•˜์„ธ์š”. "
318
+ "๋ฉ€ํ‹ฐ ๋ชจ๋ธ ์•™์ƒ๋ธ”์„ ๋™์‹œ ํ•™์Šตํ•˜์„ธ์š”(joblib n_jobs=-1 ๋˜๋Š” Ray)."
319
+ ),
320
+ }
321
+
322
+ _MEM_GUIDANCE = {
323
+ "constrained": (
324
+ "- ๊ฐ€์šฉ ๋ฉ”๋ชจ๋ฆฌ {avail_gb:.1f}GB / ์ „์ฒด {total_gb:.1f}GB (์ œํ•œ์ ): "
325
+ "OOM ์œ„ํ—˜ ๋†’์Œ. pd.read_csv(chunksize=10000) ํ•„์ˆ˜, DataFrame ์ „์ฒด ๋กœ๋“œ ๊ธˆ์ง€. "
326
+ "del df; gc.collect() ์ ๊ทน ์‚ฌ์šฉ. generator/iterator ํŒจํ„ด ์šฐ์„ . "
327
+ "float64โ†’float32, objectโ†’category๋กœ dtypes ์ฆ‰์‹œ ๋ณ€ํ™˜."
328
+ ),
329
+ "limited": (
330
+ "- ๊ฐ€์šฉ ๋ฉ”๋ชจ๋ฆฌ {avail_gb:.1f}GB / ์ „์ฒด {total_gb:.1f}GB (๋ณดํ†ต): "
331
+ "dtypes ์ตœ์ ํ™” ํ•„์ˆ˜(float64โ†’float32: ~50% ์ ˆ๊ฐ, objectโ†’category). "
332
+ "usecols๋กœ ํ•„์š” ์ปฌ๋Ÿผ๋งŒ ๋กœ๋“œ. ๋ถˆํ•„์š”ํ•œ ์ค‘๊ฐ„ DataFrame ์ฆ‰์‹œ del. "
333
+ "๋ฐ์ดํ„ฐ์…‹์ด ๊ฐ€์šฉ ๋ฉ”๋ชจ๋ฆฌ์˜ 30% ์ดˆ๊ณผ ์‹œ chunked ์ฒ˜๋ฆฌ ๊ถŒ์žฅ."
334
+ ),
335
+ "comfortable": (
336
+ "- ๊ฐ€์šฉ ๋ฉ”๋ชจ๋ฆฌ {avail_gb:.1f}GB / ์ „์ฒด {total_gb:.1f}GB (์—ฌ์œ ): "
337
+ "~{safe_size:.0f}GB ์ดํ•˜ ๋ฐ์ดํ„ฐ์…‹์€ ์ „์ฒด ๋กœ๋“œ ๊ฐ€๋Šฅ. "
338
+ "Polars๋ฅผ pandas ๋Œ€์‹  ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ 2-5๋ฐฐ ๊ฐœ์„ . "
339
+ "dtypes ์ตœ์ ํ™” ๊ถŒ์žฅ(ํ•„์ˆ˜ ์•„๋‹˜). ๋Œ€๊ทœ๋ชจ ์กฐ์ธ ์‹œ ๋ฉ”๋ชจ๋ฆฌ ํ”ผํฌ ์ฃผ์˜."
340
+ ),
341
+ "large": (
342
+ "- ๊ฐ€์šฉ ๋ฉ”๋ชจ๋ฆฌ {avail_gb:.1f}GB / ์ „์ฒด {total_gb:.1f}GB (๋Œ€์šฉ๋Ÿ‰): "
343
+ "๋Œ€๋ถ€๋ถ„์˜ DS ๋ฐ์ดํ„ฐ์…‹์„ ์ „์ฒด ๋กœ๋“œ ๊ฐ€๋Šฅ. "
344
+ "Polars๋ฅผ pandas ๋Œ€์‹  ์‚ฌ์šฉํ•˜๋ฉด ์ฒ˜๋ฆฌ ์†๋„ 2-10๋ฐฐ ๊ฐœ์„ (๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ํ™œ์šฉ). "
345
+ "pd.read_parquet(engine='pyarrow')๋กœ ๋ณ‘๋ ฌ ์ฝ๊ธฐ. "
346
+ "๋ฉ”๋ชจ๋ฆฌ ์—ฌ์œ ๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ์บ์‹ฑยทmemoization ์ ๊ทน ํ™œ์šฉ."
347
+ ),
348
+ "very_large": (
349
+ "- ๊ฐ€์šฉ ๋ฉ”๋ชจ๋ฆฌ {avail_gb:.1f}GB / ์ „์ฒด {total_gb:.1f}GB (์ดˆ๋Œ€์šฉ๋Ÿ‰): "
350
+ "๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ์€ ์‚ฌ์‹ค์ƒ ์—†์Œ. ๋ถ„์‚ฐ ์ปดํ“จํŒ…์€ ์†๋„๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ(OOM ๋ฐฉ์ง€๊ฐ€ ์•„๋‹˜). "
351
+ "Dask/Spark๋กœ ๋ณ‘๋ ฌ I/O์™€ ์—ฐ์‚ฐ์„ ๊ฐ€์†ํ•˜์„ธ์š”. "
352
+ "Polars/cuDF๋กœ ๋‹จ์ผ ๋จธ์‹  ์ตœ๋Œ€ ์„ฑ๋Šฅ์„ ์ถ”์ถœํ•˜์„ธ์š”. "
353
+ "๋Œ€๊ทœ๋ชจ ํ”ผ์ฒ˜ ์—”์ง€๋‹ˆ์–ด๋ง์„ ์ธ๋ฉ”๋ชจ๋ฆฌ๋กœ ์ž์œ ๋กญ๊ฒŒ ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ."
354
+ ),
355
+ }
356
+
357
+ _GPU_GUIDANCE = {
358
+ "entry": (
359
+ "- GPU {name} ({vram_gb:.0f}GB VRAM, entry๊ธ‰): "
360
+ "mixed precision(fp16/bf16) ํ•„์ˆ˜. gradient checkpointing ํ•„์ˆ˜. batch_size ์ตœ์†Œํ™”. "
361
+ "ํฐ ๋ชจ๋ธ ํ•™์Šต ๋ถˆ๊ฐ€โ†’ํŒŒ์ธํŠœ๋‹ ์‹œ LoRA/QLoRA ํ•„์ˆ˜. "
362
+ "cuDF๋Š” ์†Œ๊ทœ๋ชจ ๋ฐ์ดํ„ฐ์—๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ."
363
+ ),
364
+ "mid": (
365
+ "- GPU {name} ({vram_gb:.0f}GB VRAM, mid๊ธ‰): "
366
+ "mixed precision ๊ฐ•๋ ฅ ๊ถŒ์žฅ. ์ค‘๊ฐ„ ๊ทœ๋ชจ ๋ชจ๋ธ ํ•™์Šต ๊ฐ€๋Šฅ. "
367
+ "gradient accumulation์œผ๋กœ effective batch size ํ™•๋Œ€. "
368
+ "DeepSpeed ZeRO Stage 1 ํ™œ์šฉ. cuDF๋กœ ์ค‘๊ฐ„ ๊ทœ๋ชจ DataFrame GPU ๊ฐ€์†."
369
+ ),
370
+ "high": (
371
+ "- GPU {name} ({vram_gb:.0f}GB VRAM, high๊ธ‰): "
372
+ "full precision ๊ฐ€๋Šฅํ•˜๋‚˜ fp16 ๊ถŒ์žฅ(์†๋„ ์ด์ ). ๋Œ€๋ถ€๋ถ„์˜ ๋ชจ๋ธ ํ•™์Šต ๊ฐ€๋Šฅ. "
373
+ "cuDF๋กœ DataFrame GPU ๊ฐ€์†. RAPIDS(cuML, cuGraph) ํ™œ์šฉ์œผ๋กœ "
374
+ "sklearn ๋Œ€๋น„ 10-100๋ฐฐ ๊ฐ€์†."
375
+ ),
376
+ "ultra": (
377
+ "- GPU {name} ({vram_gb:.0f}GB VRAM, ultra๊ธ‰): "
378
+ "๋Œ€๊ทœ๋ชจ ๋ชจ๋ธ ํ•™์Šต/์ถ”๋ก  ๊ฐ€๋Šฅ. flash attention ํ™œ์šฉ. "
379
+ "cuDF/cuML ์ ๊ทน ํ™œ์šฉ์œผ๋กœ ์ „์ฒด DS ํŒŒ์ดํ”„๋ผ์ธ GPU ๊ฐ€์†. "
380
+ "๋ชจ๋ธ ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ(tensor parallelism) ๊ฐ€๋Šฅ."
381
+ ),
382
+ }
383
+
384
+
385
+ def _gpu_guidance_lines(gpu_info: dict) -> list[str]:
386
+ """Generate GPU-specific guidance lines."""
387
+ lines: list[str] = []
388
+ if gpu_info["tier"] == "none":
389
+ lines.append(
390
+ "- GPU ๋ฏธ๊ฐ์ง€: GPU ์˜์กด ์ฝ”๋“œ๊ฐ€ ์žˆ๋‹ค๋ฉด device='cpu' ํด๋ฐฑ ํ™•์ธ. "
391
+ "sklearn/XGBoost CPU ๋ฒ„์ „ ์‚ฌ์šฉ. cuDF/RAPIDS ์‚ฌ์šฉ ๋ถˆ๊ฐ€."
392
+ )
393
+ return lines
394
+
395
+ # Per-GPU guidance
396
+ for detail in gpu_info["details"]:
397
+ name = detail["name"]
398
+ vram_gb = (detail["vram_mb"] or 0) / 1024
399
+ tier = detail["tier"]
400
+ template = _GPU_GUIDANCE.get(tier, _GPU_GUIDANCE["entry"])
401
+ lines.append(template.format(name=name, vram_gb=vram_gb))
402
+
403
+ # Multi-GPU additional guidance
404
+ if gpu_info["multi_gpu"]:
405
+ count = gpu_info["count"]
406
+ lines.append(
407
+ f"- ๋ฉ€ํ‹ฐGPU ({count}์žฅ): ๋ฐ์ดํ„ฐ ๋ณ‘๋ ฌ์ฒ˜๋ฆฌ(DDP) ๊ธฐ๋ณธ ์ ์šฉ. "
408
+ f"GPU {count}์žฅ ํ™œ์šฉ ์‹œ FSDP ๋˜๋Š” DeepSpeed ZeRO Stage 3 ๊ถŒ์žฅ. "
409
+ "NCCL ํ†ต์‹  ์ตœ์ ํ™”. ๋Œ€๊ทœ๋ชจ ๋ชจ๋ธ์€ tensor/pipeline parallelism ์ ์šฉ."
410
+ )
411
+
412
+ return lines
413
+
414
+
415
+ def _combined_tech_recommendation(cpu_tier: str, mem_tier: str, gpu_info: dict) -> str:
416
+ """Generate cross-cutting technology stack recommendation."""
417
+ gpu_tier = gpu_info["tier"]
418
+
419
+ # DataFrame library recommendation
420
+ if gpu_tier in ("high", "ultra"):
421
+ df_rec = "cuDF(GPU ๊ฐ€์†) ๊ธฐ๋ณธ. Polars(CPU ํด๋ฐฑ). pandas๋Š” ์†Œ๊ทœ๋ชจ ํƒ์ƒ‰์—๋งŒ."
422
+ elif mem_tier in ("large", "very_large") and cpu_tier in ("high", "very_high"):
423
+ df_rec = "Polars(๊ธฐ๋ณธ) โ€” ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ๋กœ ๋Œ€์šฉ๋Ÿ‰ ๊ณ ์† ์ฒ˜๋ฆฌ. pandas๋Š” ์†Œ๊ทœ๋ชจ์—๋งŒ."
424
+ elif mem_tier in ("comfortable", "large"):
425
+ df_rec = "Polars ๊ถŒ์žฅ(์†๋„ ์ด์ ). pandas๋„ ์ ์ • ๊ทœ๋ชจ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ."
426
+ elif mem_tier == "limited":
427
+ df_rec = "pandas + dtypes ์ตœ์ ํ™”. ๋Œ€์šฉ๋Ÿ‰ ์‹œ Dask DataFrame์œผ๋กœ ์ „ํ™˜."
428
+ else:
429
+ df_rec = "pandas chunked ์ฒ˜๋ฆฌ ํ•„์ˆ˜. ๊ฐ€๋Šฅํ•˜๋ฉด Polars(๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ ์šฐ์ˆ˜)."
430
+
431
+ # Parallelism recommendation
432
+ if cpu_tier == "very_high":
433
+ par_rec = "Ray/Dask ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ. joblib(n_jobs=-1) + Ray ๋ฐฑ์—”๋“œ."
434
+ elif cpu_tier == "high":
435
+ par_rec = "joblib(n_jobs=-1). ๋Œ€๊ทœ๋ชจ ์ž‘์—…์€ Dask LocalCluster."
436
+ elif cpu_tier == "medium":
437
+ par_rec = "joblib(n_jobs=cores-1). multiprocessing.Pool ์„ ํƒ์ ."
438
+ else:
439
+ par_rec = "๋ณ‘๋ ฌ์ฒ˜๋ฆฌ ๋น„๊ถŒ์žฅ. ๋ฒกํ„ฐ ์—ฐ์‚ฐ์œผ๋กœ ๋Œ€์ฒด."
440
+
441
+ # ML training recommendation
442
+ if gpu_tier == "ultra" and gpu_info.get("multi_gpu"):
443
+ ml_rec = "FSDP/DeepSpeed ๋ถ„์‚ฐ ํ•™์Šต. ๋Œ€๊ทœ๋ชจ ๋ชจ๋ธ full training ๊ฐ€๋Šฅ."
444
+ elif gpu_tier == "ultra":
445
+ ml_rec = "๋‹จ์ผ GPU๋กœ ๋Œ€๋ถ€๋ถ„์˜ ๋ชจ๋ธ full training. flash attention ํ™œ์šฉ."
446
+ elif gpu_tier == "high":
447
+ ml_rec = "full training ๊ฐ€๋Šฅ. RAPIDS cuML๋กœ ์ „ํ†ต ML ๊ฐ€์†."
448
+ elif gpu_tier == "mid":
449
+ ml_rec = "mixed precision ํ•™์Šต. gradient accumulation ํ•„์ˆ˜."
450
+ elif gpu_tier == "entry":
451
+ ml_rec = "LoRA/QLoRA ํŒŒ์ธํŠœ๋‹๋งŒ. ํฐ ๋ชจ๋ธ full training ๋ถˆ๊ฐ€."
452
+ else:
453
+ ml_rec = "CPU ํ•™์Šต. sklearn n_jobs ํ™œ์šฉ. XGBoost hist/gpu_hist ๋ถˆ๊ฐ€."
454
+
455
+ return (
456
+ f"- ๐Ÿ“Š ๊ถŒ์žฅ DataFrame ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: {df_rec}\n"
457
+ f"- โšก ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ์ „๋žต: {par_rec}\n"
458
+ f"- ๐Ÿง  ML ํ•™์Šต ์ „๋žต: {ml_rec}"
459
+ )
460
+
461
+
462
+ def generate_tier_guidance(tiers: dict, mode: str = "review") -> str:
463
+ """Generate Korean guidance text based on classified resource tiers.
464
+
465
+ Args:
466
+ tiers: Output of ``classify_resource_tiers()``.
467
+ mode: ``"review"`` for code review diagnostics,
468
+ ``"codegen"`` for imperative code generation instructions.
469
+
470
+ Returns:
471
+ Multi-line Korean guidance string.
472
+ """
473
+ lines: list[str] = []
474
+ cpu_tier = tiers.get("cpu_tier", "unknown")
475
+ mem_tier = tiers.get("mem_tier", "unknown")
476
+ gpu_info = tiers.get("gpu", {"tier": "none", "count": 0, "details": []})
477
+ cores = tiers.get("cores")
478
+ total_gb = tiers.get("total_gb") or 0
479
+ avail_gb = tiers.get("avail_gb") or 0
480
+
481
+ # CPU guidance
482
+ cpu_template = _CPU_GUIDANCE.get(cpu_tier)
483
+ if cpu_template and cores is not None:
484
+ n_jobs = max(1, int(cores) - 1)
485
+ workers = max(1, int(cores) // 2)
486
+ lines.append(
487
+ cpu_template.format(cores=int(cores), n_jobs=n_jobs, workers=workers)
488
+ )
489
+
490
+ # Memory guidance
491
+ mem_template = _MEM_GUIDANCE.get(mem_tier)
492
+ if mem_template:
493
+ safe_size = total_gb * 0.3
494
+ lines.append(
495
+ mem_template.format(
496
+ avail_gb=avail_gb, total_gb=total_gb, safe_size=safe_size
497
+ )
498
+ )
499
+
500
+ # Memory pressure warning (avail < 30% of total)
501
+ if total_gb > 0 and avail_gb > 0 and avail_gb < total_gb * 0.3:
502
+ lines.append(
503
+ f"- โš ๏ธ ๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ•: ๊ฐ€์šฉ {avail_gb:.1f}GB / ์ „์ฒด {total_gb:.1f}GB "
504
+ f"({avail_gb / total_gb * 100:.0f}%). ํ˜„์žฌ ์ƒํƒœ์—์„œ๋Š” "
505
+ "ํ•œ ๋‹จ๊ณ„ ๋‚ฎ์€ ๋ฉ”๋ชจ๋ฆฌ ์ „๋žต์„ ์ ์šฉํ•˜์„ธ์š”."
506
+ )
507
+
508
+ # GPU guidance
509
+ lines.extend(_gpu_guidance_lines(gpu_info))
510
+
511
+ # Combined technology recommendation
512
+ lines.append("")
513
+ header = (
514
+ "**ํ™˜๊ฒฝ ๋งž์ถค ๊ธฐ์ˆ  ์Šคํƒ ๊ถŒ์žฅ**:" if mode == "review" else "**๊ธฐ์ˆ  ์Šคํƒ ์ง€์นจ**:"
515
+ )
516
+ lines.append(header)
517
+ lines.append(_combined_tech_recommendation(cpu_tier, mem_tier, gpu_info))
518
+
519
+ return "\n".join(lines)