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,497 @@
1
+ """WebSocket handler for Agent communication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ from typing import Any, Optional
9
+
10
+ from jupyter_server.base.handlers import JupyterHandler
11
+ from tornado.websocket import WebSocketHandler
12
+
13
+ # Agent-mode dependencies (langgraph/langchain require Python >=3.10)
14
+ try:
15
+ from codebuff_core.core.exceptions import AgentError
16
+ from langgraph.types import Command
17
+
18
+ from ..agent import AgentFactory, AgentRunner, config_from_dict
19
+ from ..agent.agent_factory import AgentEvent
20
+ from ..agent.middleware import MemoryStore
21
+ from ..tools import NotebookContextManager
22
+
23
+ _AGENT_AVAILABLE = True
24
+ except ImportError:
25
+ _AGENT_AVAILABLE = False
26
+
27
+ from ..log_paths import get_log_dir
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ _LOGS_DIR = get_log_dir()
32
+ _MEMORY_FILE = _LOGS_DIR / "memory" / "memories.json"
33
+
34
+
35
+ class AgentWebSocketHandler(WebSocketHandler, JupyterHandler):
36
+ """
37
+ WebSocket handler for real-time agent communication.
38
+
39
+ Uses the LangChain Agent-based AgentFactory to create and run agents.
40
+ HITL은 HumanInTheLoopMiddleware의 그래프 인터럽트 방식으로 처리.
41
+
42
+ Handles JSON-RPC style messages for:
43
+ - Starting agent runs
44
+ - Canceling agent runs
45
+ - Responding to HITL interrupts (approve/edit/reject)
46
+ """
47
+
48
+ def initialize(self) -> None:
49
+ """Initialize handler state."""
50
+ self._agent_runner: Optional[AgentRunner] = None
51
+ self._agent_task: Optional[asyncio.Task] = None
52
+ self._hitl_future: Optional[asyncio.Future] = None
53
+ self._is_closed = False
54
+ self._notebook_context: Optional[NotebookContextManager] = None
55
+ self._memory_store = MemoryStore(storage_path=_MEMORY_FILE)
56
+
57
+ def check_origin(self, origin: str) -> bool:
58
+ """Allow connections from same origin."""
59
+ return True
60
+
61
+ async def open(self) -> None:
62
+ """Handle WebSocket connection opened."""
63
+ logger.info("Agent WebSocket connected")
64
+ await self._send_event("connected", {"status": "ready"})
65
+
66
+ def on_close(self) -> None:
67
+ """Handle WebSocket connection closed."""
68
+ logger.info("Agent WebSocket disconnected")
69
+ self._is_closed = True
70
+ self._memory_store.flush_now() # 보류 중인 메모리 즉시 저장
71
+
72
+ # Cancel any running agent task
73
+ if self._agent_task and not self._agent_task.done():
74
+ self._agent_task.cancel()
75
+
76
+ # Cancel any running agent
77
+ if self._agent_runner:
78
+ self._agent_runner.cancel()
79
+
80
+ # Cancel any pending HITL request
81
+ if self._hitl_future and not self._hitl_future.done():
82
+ self._hitl_future.cancel()
83
+
84
+ async def on_message(self, message: str) -> None:
85
+ """
86
+ Handle incoming WebSocket message.
87
+
88
+ Args:
89
+ message: JSON-RPC formatted message string
90
+ """
91
+ try:
92
+ data = json.loads(message)
93
+ except json.JSONDecodeError:
94
+ await self._send_error(None, -32700, "Parse error")
95
+ return
96
+
97
+ method = data.get("method", "")
98
+ params = data.get("params", {})
99
+ msg_id = data.get("id")
100
+
101
+ logger.debug(f"Received message: method={method}, id={msg_id}")
102
+
103
+ # Route to appropriate handler
104
+ if method == "agent/run":
105
+ await self._handle_run(msg_id, params)
106
+ elif method == "agent/cancel":
107
+ await self._handle_cancel(msg_id, params)
108
+ elif method == "agent/hitl_response":
109
+ await self._handle_hitl_response(msg_id, params)
110
+ else:
111
+ await self._send_error(msg_id, -32601, f"Method not found: {method}")
112
+
113
+ async def _handle_run(self, msg_id: Optional[str], params: dict[str, Any]) -> None:
114
+ """
115
+ Handle agent/run request.
116
+
117
+ Args:
118
+ msg_id: Request ID
119
+ params: Request parameters
120
+ """
121
+ # Check if agent is already running
122
+ if self._agent_task and not self._agent_task.done():
123
+ await self._send_error(msg_id, -32000, "Agent is already running")
124
+ return
125
+
126
+ prompt = params.get("prompt", "")
127
+ mode = params.get("mode", "default")
128
+ max_iterations = params.get("maxIterations", 20)
129
+ notebook_context = params.get("notebookContext")
130
+
131
+ if not prompt:
132
+ await self._send_error(msg_id, -32602, "Prompt is required")
133
+ return
134
+
135
+ # Create notebook context manager
136
+ self._notebook_context = NotebookContextManager(
137
+ kernel_manager=getattr(self, "kernel_manager", None),
138
+ contents_manager=getattr(self, "contents_manager", None),
139
+ )
140
+
141
+ # Set notebook context if provided
142
+ if notebook_context:
143
+ self._notebook_context.set_notebook(
144
+ notebook_context.get("path", ""),
145
+ notebook_context,
146
+ )
147
+
148
+ # Create agent factory (HITL은 미들웨어가 처리)
149
+ factory = AgentFactory(
150
+ notebook_context=self._notebook_context,
151
+ on_event=self._on_agent_event,
152
+ memory_store=self._memory_store,
153
+ )
154
+
155
+ llm_config = _turn_llm_config(params.get("llmConfig") or {})
156
+ llm_config["mode"] = mode
157
+ llm_config["maxIterations"] = max_iterations
158
+
159
+ # Create agent config from frontend format
160
+ config = config_from_dict(llm_config)
161
+
162
+ # Create agent runner
163
+ try:
164
+ self._agent_runner = factory.create_agent(config)
165
+ except Exception as e:
166
+ logger.error(f"Failed to create agent: {e}")
167
+ await self._send_error(msg_id, -32000, f"Failed to create agent: {e}")
168
+ return
169
+
170
+ # Run agent in background
171
+ self._agent_task = asyncio.create_task(self._run_agent(msg_id, prompt))
172
+
173
+ async def _run_agent(self, msg_id: Optional[str], prompt: str) -> None:
174
+ """
175
+ Run the agent and handle completion.
176
+
177
+ HITL 인터럽트 발생 시:
178
+ 1. 에이전트가 GraphInterrupt 발생
179
+ 2. 인터럽트 데이터를 프론트엔드에 hitl_request 이벤트로 전송
180
+ 3. 사용자 응답을 기다린 후 Command(resume=...) 로 그래프 재개
181
+
182
+ Args:
183
+ msg_id: Request ID
184
+ prompt: User prompt
185
+ """
186
+ try:
187
+ result = await self._agent_runner.run(prompt)
188
+
189
+ # Send success response if we have a message ID
190
+ if msg_id:
191
+ await self._send_response(
192
+ msg_id,
193
+ {
194
+ "status": "complete",
195
+ "response": result,
196
+ "iterations": self._agent_runner.iteration,
197
+ },
198
+ )
199
+
200
+ except asyncio.CancelledError:
201
+ logger.info("Agent task cancelled")
202
+ if msg_id:
203
+ await self._send_error(msg_id, -32000, "Agent cancelled")
204
+
205
+ except Exception as e:
206
+ # GraphInterrupt는 HITL 인터럽트를 의미
207
+ if _is_graph_interrupt(e):
208
+ await self._handle_graph_interrupt(msg_id, e)
209
+ elif isinstance(e, AgentError):
210
+ logger.error(f"Agent error [{e.error_type}]: {e}", exc_info=e.cause)
211
+ if msg_id:
212
+ await self._send_error(msg_id, -32000, str(e))
213
+ else:
214
+ logger.error(f"Unexpected agent error: {e}", exc_info=True)
215
+ if msg_id:
216
+ await self._send_error(msg_id, -32000, str(e))
217
+
218
+ async def _handle_graph_interrupt(
219
+ self, msg_id: Optional[str], exc: Exception
220
+ ) -> None:
221
+ """
222
+ HITL 그래프 인터럽트 처리.
223
+
224
+ 인터럽트 데이터를 파싱하여 프론트엔드에 전송하고,
225
+ 사용자 응답을 기다림.
226
+
227
+ Args:
228
+ msg_id: Original request ID
229
+ exc: GraphInterrupt exception
230
+ """
231
+ # 인터럽트 데이터 추출
232
+ interrupt_data = _extract_interrupt_data(exc)
233
+
234
+ # 프론트엔드에 HITL 요청 전송
235
+ await self._send_event(
236
+ "hitl_request",
237
+ {
238
+ "action_requests": interrupt_data.get("action_requests", []),
239
+ "review_configs": interrupt_data.get("review_configs", []),
240
+ },
241
+ )
242
+
243
+ # 사용자 응답 대기 (Future 생성)
244
+ self._hitl_future = asyncio.Future()
245
+
246
+ try:
247
+ hitl_response = await asyncio.wait_for(
248
+ self._hitl_future, timeout=300
249
+ ) # 5 min timeout
250
+ except asyncio.TimeoutError:
251
+ logger.warning("HITL request timed out")
252
+ return
253
+ except asyncio.CancelledError:
254
+ return
255
+ finally:
256
+ self._hitl_future = None
257
+
258
+ # 사용자 응답으로 그래프 재개
259
+ try:
260
+ config = {"configurable": {"thread_id": self._agent_runner.thread_id}}
261
+
262
+ result = ""
263
+ async for event in self._agent_runner.agent.astream_events(
264
+ Command(resume=hitl_response),
265
+ config=config,
266
+ version="v2",
267
+ ):
268
+ if self._is_closed:
269
+ return
270
+
271
+ event_kind = event.get("event", "")
272
+
273
+ if event_kind == "on_tool_start":
274
+ tool_name = event.get("name", "unknown")
275
+ tool_input = event.get("data", {}).get("input", {})
276
+ await self._send_event(
277
+ "tool_call",
278
+ {
279
+ "id": event.get("run_id", ""),
280
+ "name": tool_name,
281
+ "arguments": tool_input,
282
+ },
283
+ )
284
+
285
+ elif event_kind == "on_tool_end":
286
+ tool_name = event.get("name", "unknown")
287
+ tool_output = event.get("data", {}).get("output", "")
288
+ await self._send_event(
289
+ "tool_result",
290
+ {
291
+ "id": event.get("run_id", ""),
292
+ "name": tool_name,
293
+ "result": {"success": True, "data": tool_output},
294
+ },
295
+ )
296
+
297
+ elif event_kind == "on_chat_model_stream":
298
+ chunk = event.get("data", {}).get("chunk", None)
299
+ if chunk and hasattr(chunk, "content") and chunk.content:
300
+ result += chunk.content
301
+
302
+ if msg_id:
303
+ await self._send_response(
304
+ msg_id,
305
+ {
306
+ "status": "complete",
307
+ "response": result,
308
+ "iterations": self._agent_runner.iteration,
309
+ },
310
+ )
311
+
312
+ except Exception as resume_exc:
313
+ if _is_graph_interrupt(resume_exc):
314
+ # 재귀적으로 또 다른 인터럽트 처리
315
+ await self._handle_graph_interrupt(msg_id, resume_exc)
316
+ else:
317
+ logger.error(f"Agent resume error: {resume_exc}")
318
+ if msg_id:
319
+ await self._send_error(msg_id, -32000, str(resume_exc))
320
+
321
+ async def _handle_cancel(
322
+ self, msg_id: Optional[str], params: dict[str, Any]
323
+ ) -> None:
324
+ """
325
+ Handle agent/cancel request.
326
+
327
+ Args:
328
+ msg_id: Request ID
329
+ params: Request parameters
330
+ """
331
+ if self._agent_runner:
332
+ self._agent_runner.cancel()
333
+
334
+ if self._agent_task and not self._agent_task.done():
335
+ self._agent_task.cancel()
336
+ await self._send_response(msg_id, {"status": "cancelled"})
337
+ else:
338
+ await self._send_response(msg_id, {"status": "not_running"})
339
+
340
+ async def _handle_hitl_response(
341
+ self,
342
+ msg_id: Optional[str],
343
+ params: dict[str, Any],
344
+ ) -> None:
345
+ """
346
+ Handle agent/hitl_response request.
347
+
348
+ 프론트엔드에서 HITL 승인/거부 응답을 수신.
349
+ decisions 형식: [{"type": "approve"} | {"type": "reject", "message": "..."} | ...]
350
+
351
+ Args:
352
+ msg_id: Request ID
353
+ params: Request parameters containing decisions
354
+ """
355
+ decisions = params.get("decisions", [])
356
+
357
+ if self._hitl_future and not self._hitl_future.done():
358
+ # HITLResponse 형식으로 전달
359
+ self._hitl_future.set_result({"decisions": decisions})
360
+ await self._send_response(msg_id, {"status": "received"})
361
+ else:
362
+ await self._send_error(msg_id, -32000, "No pending HITL request")
363
+
364
+ async def _on_agent_event(self, event: AgentEvent) -> None:
365
+ """
366
+ Callback for agent events.
367
+
368
+ Args:
369
+ event: Agent event
370
+ """
371
+ if self._is_closed:
372
+ return
373
+ await self._send_event(event.type, event.data)
374
+
375
+ async def _send_event(self, event_type: str, data: dict[str, Any]) -> None:
376
+ """
377
+ Send a JSON-RPC notification (no id).
378
+
379
+ Args:
380
+ event_type: Event type
381
+ data: Event data
382
+ """
383
+ if self._is_closed:
384
+ return
385
+
386
+ message = {
387
+ "jsonrpc": "2.0",
388
+ "method": f"agent/{event_type}",
389
+ "params": data,
390
+ }
391
+
392
+ try:
393
+ await self.write_message(json.dumps(message))
394
+ except Exception as e:
395
+ logger.error(f"Error sending event: {e}")
396
+
397
+ async def _send_response(
398
+ self,
399
+ msg_id: Optional[str],
400
+ result: Any,
401
+ ) -> None:
402
+ """
403
+ Send a JSON-RPC response.
404
+
405
+ Args:
406
+ msg_id: Request ID
407
+ result: Result data
408
+ """
409
+ if self._is_closed:
410
+ return
411
+
412
+ message = {
413
+ "jsonrpc": "2.0",
414
+ "id": msg_id,
415
+ "result": result,
416
+ }
417
+
418
+ try:
419
+ await self.write_message(json.dumps(message))
420
+ except Exception as e:
421
+ logger.error(f"Error sending response: {e}")
422
+
423
+ async def _send_error(
424
+ self,
425
+ msg_id: Optional[str],
426
+ code: int,
427
+ message: str,
428
+ ) -> None:
429
+ """
430
+ Send a JSON-RPC error response.
431
+
432
+ Args:
433
+ msg_id: Request ID
434
+ code: Error code
435
+ message: Error message
436
+ """
437
+ if self._is_closed:
438
+ return
439
+
440
+ error_msg = {
441
+ "jsonrpc": "2.0",
442
+ "id": msg_id,
443
+ "error": {
444
+ "code": code,
445
+ "message": message,
446
+ },
447
+ }
448
+
449
+ try:
450
+ await self.write_message(json.dumps(error_msg))
451
+ except Exception as e:
452
+ logger.error(f"Error sending error: {e}")
453
+
454
+
455
+ # ---------------------------------------------------------------------------
456
+ # GraphInterrupt helpers
457
+ # ---------------------------------------------------------------------------
458
+
459
+
460
+ # 브라우저가 보낸 llmConfig 에서 받는 키 — 개인설정뿐이다 (이슈 #113).
461
+ # temperature 는 0.0 고정이라 받지 않는다 (이슈 #171).
462
+ _CLIENT_LLM_PREFS = ("autoApprove",)
463
+
464
+
465
+ def _turn_llm_config(client_prefs: dict) -> dict:
466
+ """한 턴의 LLM 설정. 접속·조직 값은 서버가 해석하고 브라우저 값은 개인설정만 얹는다.
467
+
468
+ 접속 설정은 centri > 시크릿 env > pod 파일이다 (이슈 #113). 브라우저가 endpoint·apiKey·
469
+ prompts 를 보내도 반영하지 않는다.
470
+ """
471
+ from codebuff_core.managers.config_manager import get_config_manager
472
+
473
+ llm_config = get_config_manager().get_config()
474
+ llm_config.update({k: v for k, v in client_prefs.items() if k in _CLIENT_LLM_PREFS})
475
+ return llm_config
476
+
477
+
478
+ def _is_graph_interrupt(exc: Exception) -> bool:
479
+ """GraphInterrupt 예외인지 확인."""
480
+ # langgraph의 GraphInterrupt 또는 NodeInterrupt 확인
481
+ exc_type = type(exc).__name__
482
+ return exc_type in ("GraphInterrupt", "NodeInterrupt")
483
+
484
+
485
+ def _extract_interrupt_data(exc: Exception) -> dict[str, Any]:
486
+ """GraphInterrupt에서 HITL 데이터 추출."""
487
+ # GraphInterrupt.value에 HITLRequest 데이터가 있음
488
+ if hasattr(exc, "value"):
489
+ value = exc.value
490
+ if isinstance(value, dict):
491
+ return value
492
+ # interrupt() 결과가 리스트인 경우
493
+ if isinstance(value, (list, tuple)) and value:
494
+ first = value[0]
495
+ if isinstance(first, dict):
496
+ return first
497
+ return {"action_requests": [], "review_configs": []}