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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- 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": []}
|