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,114 @@
1
+ """이 사용자가 연 codex 스레드 목록 (이슈 #121 — `/resume`).
2
+
3
+ codex 의 `thread/list` 는 **서버의 모든 스레드**를 준다. centri 처럼 여러 사용자가 한
4
+ app-server 를 쓰면 남의 스레드가 섞이고, `originators`·`sourceKinds` 로도 사용자를
5
+ 가를 수 없다(스키마 확인). 그래서 목록의 정본은 codex 가 아니라 이 파일이다 — 이 pod
6
+ (=이 사용자)의 세션이 `thread/start`·`thread/fork` 로 만든 id 만 적는다. `/resume` 은
7
+ 여기 있는 id 만 받는다.
8
+
9
+ 쓰기는 매번 다시 읽어 합치고 `os.replace` 로 바꾼다. 같은 사용자의 탭 두 개가 거의
10
+ 동시에 스레드를 열어도 한쪽 기록이 통째로 사라지지 않게 하려는 것이고, 락은 걸지 않는다
11
+ (같은 순간의 두 쓰기 중 하나를 잃는 것은 목록 한 줄이라 감수한다).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import tempfile
19
+ import time
20
+ from typing import Any, Callable, Dict, List, Optional
21
+
22
+ #: 테스트와 운영이 다른 곳을 쓰게 하는 환경 변수. 없으면 `$CODEBUFF_HOME` (워크트리
23
+ #: 격리, 이슈 #116), 그것도 없으면 `~/.codebuff` 아래 `codex_threads.json`.
24
+ PATH_ENV = "CODEBUFF_CODEX_THREADS"
25
+
26
+ #: 오래된 것부터 버린다. `/resume` 목록은 이 중 앞 `LIST_LIMIT` 개만 codex 에 묻는다.
27
+ MAX_THREADS = 50
28
+ LIST_LIMIT = 20
29
+
30
+
31
+ def registry_path() -> str:
32
+ home = os.environ.get("CODEBUFF_HOME") or os.path.expanduser("~/.codebuff")
33
+ return os.environ.get(PATH_ENV) or os.path.join(home, "codex_threads.json")
34
+
35
+
36
+ def load() -> List[Dict[str, Any]]:
37
+ """새것부터. 파일이 없거나 깨졌으면 빈 목록이다."""
38
+ try:
39
+ with open(registry_path(), encoding="utf-8") as f:
40
+ data = json.load(f)
41
+ except (OSError, ValueError):
42
+ return []
43
+ threads = data.get("threads") if isinstance(data, dict) else None
44
+ return [t for t in threads or [] if isinstance(t, dict) and t.get("id")]
45
+
46
+
47
+ def _save(threads: List[Dict[str, Any]]) -> None:
48
+ path = registry_path()
49
+ os.makedirs(os.path.dirname(path), exist_ok=True)
50
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), prefix=".codex_threads.")
51
+ try:
52
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
53
+ json.dump(
54
+ {"version": 1, "threads": threads[:MAX_THREADS]},
55
+ f,
56
+ ensure_ascii=False,
57
+ indent=2,
58
+ )
59
+ os.replace(tmp, path)
60
+ except BaseException:
61
+ try:
62
+ os.unlink(tmp)
63
+ except OSError:
64
+ pass
65
+ raise
66
+
67
+
68
+ def record(
69
+ thread: Dict[str, Any], *, kind: str, workspace: str, parent: str = ""
70
+ ) -> None:
71
+ """새로 연 스레드를 맨 앞에 적는다. 이미 있으면 그 줄을 앞으로 옮긴다."""
72
+ tid = str(thread.get("id") or "")
73
+ if not tid:
74
+ return
75
+ threads = [t for t in load() if t.get("id") != tid]
76
+ entry: Dict[str, Any] = {
77
+ "id": tid,
78
+ "kind": kind,
79
+ "workspace": workspace,
80
+ "createdAt": int(time.time()),
81
+ }
82
+ if parent:
83
+ entry["parent"] = parent
84
+ _save([entry] + threads)
85
+
86
+
87
+ def update(thread_id: str, **fields: Any) -> None:
88
+ threads = load()
89
+ for t in threads:
90
+ if t.get("id") == thread_id:
91
+ t.update(fields)
92
+ _save(threads)
93
+
94
+
95
+ def forget(thread_id: str) -> None:
96
+ _save([t for t in load() if t.get("id") != thread_id])
97
+
98
+
99
+ def find(thread_id: str) -> Optional[Dict[str, Any]]:
100
+ return next((t for t in load() if t.get("id") == thread_id), None)
101
+
102
+
103
+ def recorder(workspace: str) -> Callable[[str, Dict[str, Any]], None]:
104
+ """`CodexSession(on_thread=...)` 에 넘기는 기록기."""
105
+
106
+ def _on_thread(kind: str, thread: Dict[str, Any]) -> None:
107
+ record(
108
+ thread,
109
+ kind=kind,
110
+ workspace=workspace,
111
+ parent=str(thread.get("forkedFromId") or ""),
112
+ )
113
+
114
+ return _on_thread
@@ -0,0 +1,377 @@
1
+ """
2
+ ContentInjectionMiddleware for multi-agent code/SQL auto-injection.
3
+
4
+ Parses [CODE]/[SQL] blocks from subagent ToolMessage responses and injects
5
+ them into target tool calls (jupyter_cell, file_write, jupyter_markdown).
6
+
7
+ Ported from chat-mode-enhancement branch, adapted for DeepAgents ToolMessage
8
+ parsing instead of State-based approach (DeepAgents has no state_schema param).
9
+ """
10
+
11
+ import logging
12
+ import re
13
+ from typing import Optional
14
+
15
+ from ..log_paths import get_debug_log_path
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Direct file-based debug logging (same as agent_factory._dbg)
20
+
21
+ _DEBUG_LOG = get_debug_log_path()
22
+
23
+
24
+ def _dbg(msg: str) -> None:
25
+ """Write debug message directly to agent_debug.log."""
26
+ try:
27
+ import datetime
28
+
29
+ ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
30
+ with open(_DEBUG_LOG, "a") as f:
31
+ f.write(f"[{ts}] {msg}\n")
32
+ f.flush()
33
+ except OSError:
34
+ pass
35
+
36
+
37
+ def _extract_code_block(response: str, lang: str = "python") -> Optional[str]:
38
+ """Extract code/SQL from response using [CODE]/[SQL] markers or fenced blocks.
39
+
40
+ Tries in order:
41
+ 1. [CODE] or [SQL] section marker with fenced block
42
+ 2. [CODE] or [SQL] marker with content (no fenced block)
43
+ 3. Fenced code block with language
44
+ 4. Generic fenced code block (fallback)
45
+ """
46
+ if not response:
47
+ return None
48
+
49
+ marker = "[CODE]" if lang == "python" else "[SQL]"
50
+
51
+ # Pattern 1: [CODE]/[SQL] marker + fenced block
52
+ marker_pattern = re.escape(marker) + r"\s*\n\s*```(?:\w+)?\s*\n(.*?)```"
53
+ match = re.search(marker_pattern, response, re.DOTALL)
54
+ if match:
55
+ code = match.group(1).strip()
56
+ if code:
57
+ return code
58
+
59
+ # Pattern 2: [CODE]/[SQL] marker with content after it (no fenced block)
60
+ marker_pattern2 = re.escape(marker) + r"\s*\n(.*?)(?=\n\[|\Z)"
61
+ match = re.search(marker_pattern2, response, re.DOTALL)
62
+ if match:
63
+ code = match.group(1).strip()
64
+ if code.startswith(f"```{lang}"):
65
+ code = code[len(f"```{lang}") :].strip()
66
+ if code.startswith("```"):
67
+ code = code[3:].strip()
68
+ if code.endswith("```"):
69
+ code = code[:-3].strip()
70
+ if code:
71
+ return code
72
+
73
+ # Pattern 3: Fenced code block with language
74
+ fenced_pattern = rf"```{lang}\s*\n(.*?)```"
75
+ match = re.search(fenced_pattern, response, re.DOTALL)
76
+ if match:
77
+ code = match.group(1).strip()
78
+ if code:
79
+ return code
80
+
81
+ # Pattern 4: Generic fenced code block (fallback)
82
+ generic_pattern = r"```\s*\n(.*?)```"
83
+ match = re.search(generic_pattern, response, re.DOTALL)
84
+ if match:
85
+ code = match.group(1).strip()
86
+ if code and len(code) > 10:
87
+ return code
88
+
89
+ return None
90
+
91
+
92
+ def _extract_description(response: str) -> Optional[str]:
93
+ """Extract [DESCRIPTION] section from subagent response."""
94
+ if not response:
95
+ return None
96
+
97
+ patterns = [
98
+ r"\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*\[(?:CODE|SQL|ANALYSIS|RECOMMENDATION)\])",
99
+ r"\[DESCRIPTION\]\s*(.*?)(?=\s*\[(?:CODE|SQL|ANALYSIS|RECOMMENDATION)\])",
100
+ r"\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*```)",
101
+ r"\[DESCRIPTION\]\s*\n(.+?)(?=\n\n|\Z)",
102
+ ]
103
+
104
+ for pattern in patterns:
105
+ match = re.search(pattern, response, re.DOTALL | re.IGNORECASE)
106
+ if match:
107
+ description = match.group(1).strip()
108
+ if description and len(description) > 5:
109
+ return description
110
+
111
+ return None
112
+
113
+
114
+ try:
115
+ from langchain.agents.middleware.types import AgentMiddleware
116
+ except ImportError:
117
+ # Fallback: define a minimal base class
118
+ class AgentMiddleware: # type: ignore[no-redef]
119
+ tools: list = []
120
+
121
+
122
+ class ContentInjectionMiddleware(AgentMiddleware):
123
+ """Inject subagent-generated code/SQL into target tool calls.
124
+
125
+ DeepAgents' task tool returns subagent responses as ToolMessage(text).
126
+ This middleware:
127
+ 1. In before_model(): scans recent ToolMessages for [CODE]/[SQL] blocks
128
+ 2. In after_model(): injects content into non-HITL tool calls (e.g. jupyter_markdown)
129
+ 3. In awrap_tool_call(): fallback injection during tool execution
130
+
131
+ Note: For HITL-interrupted tools (jupyter_cell, file_write), content injection
132
+ happens in ds_tool_description() callback (agent_factory.py) so the frontend
133
+ sees injected args in the approval request.
134
+ """
135
+
136
+ def __init__(self):
137
+ self.tools: list = []
138
+ self._last_content: Optional[str] = None
139
+ self._last_type: Optional[str] = None # "python" | "sql" | "full_analysis"
140
+ self._last_description: Optional[str] = None
141
+ # Track consumed ToolMessage IDs to prevent re-extraction
142
+ self._consumed_msg_ids: set = set()
143
+
144
+ def _consume(self) -> None:
145
+ """Mark current content as consumed and clear state."""
146
+ _mid = getattr(self, "_pending_msg_id", None)
147
+ _dbg(
148
+ f"CI._consume: pending_id={_mid} content_len={len(self._last_content) if self._last_content else 0}"
149
+ )
150
+ if _mid:
151
+ self._consumed_msg_ids.add(_mid)
152
+ self._last_content = None
153
+ self._last_type = None
154
+ self._last_description = None
155
+ self._pending_msg_id = None
156
+
157
+ def before_model(self, state, runtime):
158
+ """Scan recent ToolMessages for [CODE]/[SQL] blocks before LLM call."""
159
+ messages = state.get("messages", [])
160
+ # 현재 상태 로깅
161
+ _dbg(
162
+ f"CI.before_model: msg_count={len(messages)} _last_content="
163
+ f"{'%dchars' % len(self._last_content) if self._last_content else 'None'} "
164
+ f"_last_type={self._last_type} consumed_ids={self._consumed_msg_ids} "
165
+ f"pending_id={getattr(self, '_pending_msg_id', None)}"
166
+ )
167
+ # 최근 메시지 내용 디버그 로깅 (AI + Tool 모두)
168
+ msgs_found = 0
169
+ for _m in reversed(messages):
170
+ _mtype = getattr(_m, "type", "unknown")
171
+ if _mtype == "tool":
172
+ _c = getattr(_m, "content", "")
173
+ _mid = getattr(_m, "tool_call_id", None)
174
+ _consumed = _mid and _mid in self._consumed_msg_ids
175
+ _dbg(
176
+ f"CI.before_model: [{len(messages) - msgs_found - 1}] ToolMessage "
177
+ f"id={_mid} consumed={_consumed} "
178
+ f"has_CODE={'[CODE]' in _c if isinstance(_c, str) else 'N/A'} "
179
+ f"has_SQL={'[SQL]' in _c if isinstance(_c, str) else 'N/A'} "
180
+ f"len={len(_c) if isinstance(_c, str) else 0} "
181
+ f"preview={repr(_c[:200]) if isinstance(_c, str) else repr(type(_c))}"
182
+ )
183
+ elif _mtype == "ai":
184
+ _tc = getattr(_m, "tool_calls", [])
185
+ _tc_names = [tc.get("name") for tc in _tc] if _tc else []
186
+ _dbg(
187
+ f"CI.before_model: [{len(messages) - msgs_found - 1}] AIMessage "
188
+ f"tool_calls={_tc_names}"
189
+ )
190
+ msgs_found += 1
191
+ if msgs_found >= 6:
192
+ break
193
+
194
+ for msg in reversed(messages):
195
+ if not (hasattr(msg, "type") and msg.type == "tool"):
196
+ continue
197
+ content = getattr(msg, "content", "")
198
+ if not isinstance(content, str):
199
+ continue
200
+
201
+ # Skip already-consumed ToolMessages
202
+ msg_id = getattr(msg, "tool_call_id", None) or getattr(msg, "id", None)
203
+ if msg_id and msg_id in self._consumed_msg_ids:
204
+ continue
205
+
206
+ if "[CODE]" in content:
207
+ code = _extract_code_block(content, lang="python")
208
+ if code:
209
+ self._last_content = code
210
+ self._last_type = "python"
211
+ self._last_description = _extract_description(content)
212
+ self._pending_msg_id = msg_id
213
+ _dbg(
214
+ f"CI.EXTRACT: python code {len(code)}chars from ToolMessage id={msg_id}"
215
+ )
216
+ break
217
+ elif "[ANALYSIS]" in content and "[RECOMMENDATION]" in content:
218
+ # Airflow expert analysis response — keep entire content
219
+ self._last_content = content
220
+ self._last_type = "full_analysis"
221
+ self._last_description = None
222
+ self._pending_msg_id = msg_id
223
+ _dbg(
224
+ f"CI.EXTRACT: full_analysis (airflow) {len(content)}chars from ToolMessage id={msg_id}"
225
+ )
226
+ break
227
+ elif "[SQL]" in content and (
228
+ "[ANALYSIS]" in content
229
+ or "[MERMAID]" in content
230
+ or "[PERFORMANCE]" in content
231
+ or "[SYNTAX]" in content
232
+ or "[PLAN]" in content
233
+ or "## " in content
234
+ ):
235
+ # Full analysis response (bq_agent Format 4 etc.) — keep entire content
236
+ self._last_content = content
237
+ self._last_type = "full_analysis"
238
+ self._last_description = None # description is already in content
239
+ self._pending_msg_id = msg_id
240
+ _dbg(
241
+ f"CI.EXTRACT: full_analysis {len(content)}chars from ToolMessage id={msg_id}"
242
+ )
243
+ break
244
+ elif "[SQL]" in content:
245
+ sql = _extract_code_block(content, lang="sql")
246
+ if sql:
247
+ self._last_content = sql
248
+ self._last_type = "sql"
249
+ self._last_description = _extract_description(content)
250
+ self._pending_msg_id = msg_id
251
+ _dbg(
252
+ f"CI.EXTRACT: SQL {len(sql)}chars from ToolMessage id={msg_id}"
253
+ )
254
+ break
255
+ return None # no state modification
256
+
257
+ def after_model(self, state, runtime):
258
+ """Inject stored content into LLM output's tool_calls args (in-place)."""
259
+ messages = state.get("messages", [])
260
+ last_msg = messages[-1] if messages else None
261
+ _tc = getattr(last_msg, "tool_calls", []) if last_msg else []
262
+ _tc_info = [
263
+ {
264
+ "name": tc.get("name"),
265
+ "has_code": bool(tc.get("args", {}).get("code")),
266
+ "has_content": bool(tc.get("args", {}).get("content")),
267
+ }
268
+ for tc in _tc
269
+ ]
270
+ _dbg(
271
+ f"CI.after_model: _last_content="
272
+ f"{'%dchars' % len(self._last_content) if self._last_content else 'None'} "
273
+ f"_last_type={self._last_type} tool_calls={_tc_info}"
274
+ )
275
+ if not self._last_content:
276
+ return None
277
+
278
+ messages = state.get("messages", [])
279
+ if not messages:
280
+ return None
281
+
282
+ last_msg = messages[-1]
283
+ tool_calls = getattr(last_msg, "tool_calls", [])
284
+ if not tool_calls:
285
+ return None
286
+
287
+ for tc in tool_calls:
288
+ if self._last_type == "full_analysis":
289
+ if tc["name"] == "jupyter_markdown" and not tc["args"].get("content"):
290
+ tc["args"]["content"] = self._last_content
291
+ _dbg(
292
+ f"CI.INJECT: full_analysis {len(self._last_content)}chars → jupyter_markdown"
293
+ )
294
+ self._consume()
295
+ break
296
+ elif self._last_type == "python":
297
+ if tc["name"] == "jupyter_cell" and not tc["args"].get("code"):
298
+ tc["args"]["code"] = self._last_content
299
+ if self._last_description:
300
+ tc["args"]["description"] = self._last_description
301
+ _dbg(
302
+ f"CI.INJECT: python {len(self._last_content)}chars → jupyter_cell"
303
+ )
304
+ self._consume()
305
+ break
306
+ elif tc["name"] == "file_write" and not tc["args"].get("content"):
307
+ tc["args"]["content"] = self._last_content
308
+ _dbg(
309
+ f"CI.INJECT: python {len(self._last_content)}chars → file_write"
310
+ )
311
+ self._consume()
312
+ break
313
+ elif self._last_type == "sql":
314
+ if tc["name"] == "jupyter_markdown" and not tc["args"].get("content"):
315
+ sql_md = f"```sql\n{self._last_content}\n```"
316
+ if self._last_description:
317
+ sql_md = f"{self._last_description}\n\n{sql_md}"
318
+ tc["args"]["content"] = sql_md
319
+ _dbg(
320
+ f"CI.INJECT: SQL {len(self._last_content)}chars → jupyter_markdown"
321
+ )
322
+ self._consume()
323
+ break
324
+
325
+ return None # tool_calls are modified in-place
326
+
327
+ async def awrap_tool_call(self, request, handler):
328
+ """Fallback: inject stored content during tool execution.
329
+
330
+ This handles cases where after_model didn't inject (e.g. content was
331
+ already injected by ds_tool_description, or HITL skipped after_model).
332
+ Also consumes state when the target tool already has content
333
+ (injected by _inject_content_from_state in ds_tool_description).
334
+ """
335
+ tool_name = request.tool_call["name"]
336
+ args = request.tool_call.get("args", {})
337
+ _dbg(
338
+ f"CI.awrap_tool_call: tool={tool_name} _last_content="
339
+ f"{'%dchars' % len(self._last_content) if self._last_content else 'None'} "
340
+ f"has_code={bool(args.get('code'))} has_content={bool(args.get('content'))}"
341
+ )
342
+
343
+ if self._last_content:
344
+ new_args = None
345
+
346
+ if self._last_type == "full_analysis":
347
+ if tool_name == "jupyter_markdown":
348
+ if not args.get("content"):
349
+ new_args = {**args, "content": self._last_content}
350
+ self._consume()
351
+ elif self._last_type == "python":
352
+ if tool_name == "jupyter_cell":
353
+ if not args.get("code"):
354
+ new_args = {**args, "code": self._last_content}
355
+ if self._last_description and not args.get("description"):
356
+ new_args["description"] = self._last_description
357
+ # Consume regardless - code was either injected here or by _inject_content_from_state
358
+ self._consume()
359
+ elif tool_name == "file_write":
360
+ if not args.get("content"):
361
+ new_args = {**args, "content": self._last_content}
362
+ self._consume()
363
+ elif self._last_type == "sql":
364
+ if tool_name == "jupyter_markdown":
365
+ if not args.get("content"):
366
+ sql_md = f"```sql\n{self._last_content}\n```"
367
+ if self._last_description:
368
+ sql_md = f"{self._last_description}\n\n{sql_md}"
369
+ new_args = {**args, "content": sql_md}
370
+ self._consume()
371
+
372
+ if new_args is not None:
373
+ request = request.override(
374
+ tool_call={**request.tool_call, "args": new_args}
375
+ )
376
+
377
+ return await handler(request)