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,548 @@
|
|
|
1
|
+
"""ContinuationControl + LimitToolCalls + TodoActive 미들웨어.
|
|
2
|
+
|
|
3
|
+
ContinuationControlMiddleware: 도구 완료 후 LLM에게 다음 행동 안내.
|
|
4
|
+
LimitToolCallsMiddleware: LLM 응답의 tool_calls를 1개로 제한 + additional_kwargs 정리.
|
|
5
|
+
TodoActiveMiddleware: wrap_tool_call로 todo_active state flag 관리 (Command 반환).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from collections.abc import Awaitable, Callable
|
|
14
|
+
from typing import Annotated, Any, NotRequired
|
|
15
|
+
|
|
16
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
17
|
+
from langgraph.types import Command
|
|
18
|
+
|
|
19
|
+
from langchain.agents.middleware.types import (
|
|
20
|
+
AgentMiddleware,
|
|
21
|
+
AgentState,
|
|
22
|
+
ModelCallResult,
|
|
23
|
+
ModelRequest,
|
|
24
|
+
ModelResponse,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from ..log_paths import get_debug_log_path
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Direct file-based debug logging (bypasses Python logging config)
|
|
32
|
+
|
|
33
|
+
_DEBUG_LOG = get_debug_log_path()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _dbg(msg: str) -> None:
|
|
37
|
+
"""Write debug message directly to file."""
|
|
38
|
+
try:
|
|
39
|
+
ts = datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
40
|
+
with open(_DEBUG_LOG, "a") as f:
|
|
41
|
+
f.write(f"[{ts}] CONTINUATION: {msg}\n")
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# write_todos 후에는 "다시 write_todos 호출 금지" 별도 guidance 사용
|
|
47
|
+
_WRITE_TODOS_TOOLS = {"write_todos"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _get_tool_name(msg: Any) -> str:
|
|
51
|
+
"""ToolMessage에서 도구 이름 추출."""
|
|
52
|
+
if hasattr(msg, "name"):
|
|
53
|
+
return msg.name or ""
|
|
54
|
+
if hasattr(msg, "additional_kwargs"):
|
|
55
|
+
return msg.additional_kwargs.get("name", "")
|
|
56
|
+
return ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _extract_ai_message(response: Any) -> AIMessage | None:
|
|
60
|
+
"""ModelResponse/ModelCallResult에서 AIMessage 추출."""
|
|
61
|
+
if isinstance(response, AIMessage):
|
|
62
|
+
return response
|
|
63
|
+
if hasattr(response, "message") and isinstance(response.message, AIMessage):
|
|
64
|
+
return response.message
|
|
65
|
+
if hasattr(response, "messages"):
|
|
66
|
+
for msg in reversed(response.messages):
|
|
67
|
+
if isinstance(msg, AIMessage):
|
|
68
|
+
return msg
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_todos_from_state(request: ModelRequest) -> list | None:
|
|
73
|
+
"""LangGraph state에서 todos 직접 읽기. 없으면 None."""
|
|
74
|
+
state = getattr(request, "state", None)
|
|
75
|
+
if state and isinstance(state, dict):
|
|
76
|
+
todos = state.get("todos")
|
|
77
|
+
if isinstance(todos, list):
|
|
78
|
+
return todos
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _get_todos_from_messages(messages: list) -> list | None:
|
|
83
|
+
"""메시지에서 write_todos 결과를 찾아 todos 추출. 없으면 None."""
|
|
84
|
+
for msg in reversed(messages):
|
|
85
|
+
if not hasattr(msg, "type") or msg.type != "tool":
|
|
86
|
+
continue
|
|
87
|
+
if _get_tool_name(msg) != "write_todos":
|
|
88
|
+
continue
|
|
89
|
+
content = getattr(msg, "content", "")
|
|
90
|
+
if not isinstance(content, str):
|
|
91
|
+
continue
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(content)
|
|
94
|
+
todos = data.get("todos", [])
|
|
95
|
+
if todos:
|
|
96
|
+
return todos
|
|
97
|
+
except (json.JSONDecodeError, AttributeError):
|
|
98
|
+
continue
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _classify_todos(todos: list | None) -> str:
|
|
103
|
+
"""todos 상태 분류. 'none' | 'pending' | 'all_completed'."""
|
|
104
|
+
if not todos:
|
|
105
|
+
return "none"
|
|
106
|
+
pending = [
|
|
107
|
+
t
|
|
108
|
+
for t in todos
|
|
109
|
+
if isinstance(t, dict) and t.get("status") in ("pending", "in_progress")
|
|
110
|
+
]
|
|
111
|
+
# dict가 아닌 경우 (Todo 객체): attribute 접근
|
|
112
|
+
if not pending:
|
|
113
|
+
pending = [
|
|
114
|
+
t
|
|
115
|
+
for t in todos
|
|
116
|
+
if hasattr(t, "status") and t.status in ("pending", "in_progress")
|
|
117
|
+
]
|
|
118
|
+
return "pending" if pending else "all_completed"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _has_summary_in_messages(messages: list) -> bool:
|
|
122
|
+
"""메시지에서 final_summary 도구 호출 또는 요약 텍스트 존재 여부 확인."""
|
|
123
|
+
for msg in reversed(messages[-10:]):
|
|
124
|
+
msg_type = getattr(msg, "type", "")
|
|
125
|
+
name = _get_tool_name(msg) if msg_type == "tool" else ""
|
|
126
|
+
|
|
127
|
+
# final_summary 도구 결과
|
|
128
|
+
if name in ("final_summary_tool", "final_summary"):
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
# AI 메시지의 summary JSON 패턴
|
|
132
|
+
content = getattr(msg, "content", "")
|
|
133
|
+
if isinstance(content, str):
|
|
134
|
+
if ('"summary"' in content and '"next_items"' in content) or (
|
|
135
|
+
"'summary'" in content and "'next_items'" in content
|
|
136
|
+
):
|
|
137
|
+
return True
|
|
138
|
+
# 한국어 요약 패턴
|
|
139
|
+
if any(
|
|
140
|
+
kw in content
|
|
141
|
+
for kw in ("모든 작업이 완료", "작업 요약", "다음 단계 제안")
|
|
142
|
+
):
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# TodoActiveState — state schema for todo_active flag
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _take_last(existing: str, new: str) -> str:
|
|
153
|
+
"""Reducer: 마지막 값만 유지. 병렬 도구 호출 시 충돌 방지."""
|
|
154
|
+
return new
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _take_last_bool(existing: bool, new: bool) -> bool:
|
|
158
|
+
"""Reducer: 마지막 bool 값만 유지."""
|
|
159
|
+
return new
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TodoActiveState(AgentState):
|
|
163
|
+
"""todo_active flag를 포함하는 state schema.
|
|
164
|
+
|
|
165
|
+
- todo_active=True: write_todos가 호출된 후 (작업 진행 중)
|
|
166
|
+
- todo_active=False: final_summary_tool 호출 후 또는 초기 상태
|
|
167
|
+
- last_tool_name: 가장 최근 실행된 도구 이름 (messages[-1] 의존 제거)
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
todo_active: NotRequired[Annotated[bool, _take_last_bool]]
|
|
171
|
+
last_tool_name: NotRequired[Annotated[str, _take_last]]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# TodoActiveMiddleware — wrap_tool_call로 state flag 관리
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TodoActiveMiddleware(AgentMiddleware):
|
|
180
|
+
"""wrap_tool_call로 todo_active, last_tool_name state flag를 관리.
|
|
181
|
+
|
|
182
|
+
- write_todos → todo_active=True, last_tool_name="write_todos"
|
|
183
|
+
- final_summary_tool → todo_active=False, last_tool_name="final_summary_tool"
|
|
184
|
+
- task → last_tool_name="task"
|
|
185
|
+
- 기타 도구 → last_tool_name=<tool_name>
|
|
186
|
+
|
|
187
|
+
Command(update={...})로 LangGraph state를 확정적으로 업데이트.
|
|
188
|
+
messages[-1] 타입에 의존하지 않아 system 메시지 끼어들기 문제 해결.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
state_schema = TodoActiveState
|
|
192
|
+
|
|
193
|
+
async def awrap_tool_call(self, request, handler):
|
|
194
|
+
"""도구 실행 후 state flag 업데이트."""
|
|
195
|
+
result = await handler(request)
|
|
196
|
+
tool_name = request.tool_call.get("name", "")
|
|
197
|
+
|
|
198
|
+
if tool_name == "write_todos":
|
|
199
|
+
_dbg("TodoActive: write_todos → todo_active=True")
|
|
200
|
+
return self._wrap_with_state(
|
|
201
|
+
result, todo_active=True, last_tool_name=tool_name
|
|
202
|
+
)
|
|
203
|
+
elif tool_name in ("final_summary_tool", "final_summary"):
|
|
204
|
+
_dbg(f"TodoActive: {tool_name} → todo_active=False")
|
|
205
|
+
return self._wrap_with_state(
|
|
206
|
+
result, todo_active=False, last_tool_name=tool_name
|
|
207
|
+
)
|
|
208
|
+
elif tool_name:
|
|
209
|
+
extra = ""
|
|
210
|
+
if tool_name == "task":
|
|
211
|
+
args = request.tool_call.get("args", {})
|
|
212
|
+
if isinstance(args, str):
|
|
213
|
+
try:
|
|
214
|
+
args = json.loads(args)
|
|
215
|
+
except Exception:
|
|
216
|
+
args = {}
|
|
217
|
+
agent_type = args.get("subagent_type", args.get("agent_name", ""))
|
|
218
|
+
if agent_type:
|
|
219
|
+
extra = f":{agent_type}"
|
|
220
|
+
_dbg(f"TodoActive: {tool_name}{extra} → last_tool_name update")
|
|
221
|
+
return self._wrap_with_state(result, last_tool_name=f"{tool_name}{extra}")
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
def _wrap_with_state(
|
|
226
|
+
self,
|
|
227
|
+
result: Any,
|
|
228
|
+
*,
|
|
229
|
+
todo_active: bool | None = None,
|
|
230
|
+
last_tool_name: str | None = None,
|
|
231
|
+
) -> Command:
|
|
232
|
+
"""도구 결과를 Command로 감싸서 state 업데이트."""
|
|
233
|
+
update: dict[str, Any] = {}
|
|
234
|
+
if todo_active is not None:
|
|
235
|
+
update["todo_active"] = todo_active
|
|
236
|
+
if last_tool_name is not None:
|
|
237
|
+
update["last_tool_name"] = last_tool_name
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
if isinstance(result, Command):
|
|
241
|
+
# 기존 Command에 merge
|
|
242
|
+
existing = (
|
|
243
|
+
result.update if hasattr(result, "update") and result.update else {}
|
|
244
|
+
)
|
|
245
|
+
merged = {**existing, **update}
|
|
246
|
+
return Command(update=merged)
|
|
247
|
+
elif isinstance(result, ToolMessage):
|
|
248
|
+
update["messages"] = [result]
|
|
249
|
+
return Command(update=update)
|
|
250
|
+
else:
|
|
251
|
+
# Unknown type → ToolMessage로 변환
|
|
252
|
+
content = str(result) if result else ""
|
|
253
|
+
tool_msg = ToolMessage(content=content, tool_call_id="")
|
|
254
|
+
update["messages"] = [tool_msg]
|
|
255
|
+
return Command(update=update)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
_dbg(f"TodoActive: wrap failed: {e}")
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# ContinuationControlMiddleware — state flag 기반 continuation 제어
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class ContinuationControlMiddleware(AgentMiddleware):
|
|
267
|
+
"""state flag 기반으로 도구 완료 후 LLM에게 다음 행동을 안내.
|
|
268
|
+
|
|
269
|
+
TodoActiveMiddleware가 설정한 state flag를 읽어서 판단:
|
|
270
|
+
- todo_active=False + final_summary 완료 → 즉시 중단
|
|
271
|
+
- last_tool_name="task" → 코드 실행 유도 guidance 주입
|
|
272
|
+
- last_tool_name="write_todos" → 다음 작업 진행 유도
|
|
273
|
+
- pending todos → 계속 진행 유도
|
|
274
|
+
- 모든 todos 완료 → final_summary 호출 유도
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def awrap_model_call(
|
|
278
|
+
self,
|
|
279
|
+
request: ModelRequest,
|
|
280
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
281
|
+
) -> Awaitable[ModelCallResult]:
|
|
282
|
+
return self._impl(request, handler)
|
|
283
|
+
|
|
284
|
+
async def _impl(
|
|
285
|
+
self,
|
|
286
|
+
request: ModelRequest,
|
|
287
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
288
|
+
) -> ModelCallResult:
|
|
289
|
+
messages = request.messages
|
|
290
|
+
state = getattr(request, "state", {}) or {}
|
|
291
|
+
todo_active = state.get("todo_active", False)
|
|
292
|
+
last_tool = state.get("last_tool_name", "")
|
|
293
|
+
|
|
294
|
+
_dbg(
|
|
295
|
+
f"_impl called, msg_count={len(messages) if messages else 0}, "
|
|
296
|
+
f"todo_active={todo_active}, last_tool={last_tool}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# ── Guard 1: final_summary 완료 후 즉시 중단 ──
|
|
300
|
+
# todo_active=False이고 메시지에 summary가 있으면 agent 중단
|
|
301
|
+
if not todo_active and last_tool in ("final_summary_tool", "final_summary"):
|
|
302
|
+
_dbg("final_summary tool + todo_active=False → stopping agent")
|
|
303
|
+
return AIMessage(content="", tool_calls=[])
|
|
304
|
+
|
|
305
|
+
# todo_active=False이면서 summary가 messages에 있으면 중단
|
|
306
|
+
if not todo_active and _has_summary_in_messages(messages):
|
|
307
|
+
_dbg("todo_active=False + summary in messages → stopping agent")
|
|
308
|
+
return AIMessage(content="", tool_calls=[])
|
|
309
|
+
|
|
310
|
+
# ── Guard 2: task 완료 후 결과 처리 유도 ──
|
|
311
|
+
if last_tool.startswith("task"):
|
|
312
|
+
subagent = last_tool.split(":", 1)[1] if ":" in last_tool else ""
|
|
313
|
+
if subagent in ("bq_agent", "athena_query", "airflow_expert"):
|
|
314
|
+
guidance = (
|
|
315
|
+
"[SYSTEM] Sub-agent returned analysis/SQL. "
|
|
316
|
+
"You MUST display it with jupyter_markdown() BEFORE calling write_todos. "
|
|
317
|
+
"Do NOT skip display. Do NOT call jupyter_cell for analysis results."
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
guidance = (
|
|
321
|
+
"[SYSTEM] Sub-agent returned code. "
|
|
322
|
+
"You MUST execute it with jupyter_cell() BEFORE calling write_todos. "
|
|
323
|
+
"Do NOT skip execution. Do NOT mark the todo as completed without running the code."
|
|
324
|
+
)
|
|
325
|
+
new_messages = list(messages) + [HumanMessage(content=guidance)]
|
|
326
|
+
_dbg(f"injected post-task guidance for subagent={subagent} (state-based)")
|
|
327
|
+
request = request.override(messages=new_messages)
|
|
328
|
+
response = await handler(request)
|
|
329
|
+
return self._strip_write_todos_if_summary(response)
|
|
330
|
+
|
|
331
|
+
# ── Guard 3: todo_active=False → 간단한 작업, guidance 불필요 ──
|
|
332
|
+
if not todo_active:
|
|
333
|
+
_dbg(f"todo_active=False, last_tool={last_tool} → skip guidance")
|
|
334
|
+
response = await handler(request)
|
|
335
|
+
return self._strip_write_todos_if_summary(response)
|
|
336
|
+
|
|
337
|
+
# ── todo_active=True: continuation guidance 주입 ──
|
|
338
|
+
if last_tool:
|
|
339
|
+
is_write_todos = last_tool in _WRITE_TODOS_TOOLS
|
|
340
|
+
modified = self._inject_continuation(
|
|
341
|
+
request,
|
|
342
|
+
last_tool,
|
|
343
|
+
after_write_todos=is_write_todos,
|
|
344
|
+
)
|
|
345
|
+
if modified is None:
|
|
346
|
+
_dbg("summary exists → stopping agent")
|
|
347
|
+
return AIMessage(content="", tool_calls=[])
|
|
348
|
+
request = modified
|
|
349
|
+
|
|
350
|
+
response = await handler(request)
|
|
351
|
+
return self._strip_write_todos_if_summary(response)
|
|
352
|
+
|
|
353
|
+
def _get_last_tool_result(self, messages: list) -> dict | None:
|
|
354
|
+
"""마지막 ToolMessage에서 결과를 dict로 추출. 없으면 None."""
|
|
355
|
+
for msg in reversed(messages):
|
|
356
|
+
if not isinstance(msg, ToolMessage):
|
|
357
|
+
continue
|
|
358
|
+
content = getattr(msg, "content", "")
|
|
359
|
+
if not isinstance(content, str):
|
|
360
|
+
continue
|
|
361
|
+
try:
|
|
362
|
+
return json.loads(content)
|
|
363
|
+
except (json.JSONDecodeError, TypeError):
|
|
364
|
+
# Plain text error message → wrap in dict
|
|
365
|
+
if content.strip():
|
|
366
|
+
return {"error": content, "success": False}
|
|
367
|
+
break
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
def _inject_continuation(
|
|
371
|
+
self,
|
|
372
|
+
request: ModelRequest,
|
|
373
|
+
tool_name: str,
|
|
374
|
+
*,
|
|
375
|
+
after_write_todos: bool = False,
|
|
376
|
+
) -> ModelRequest | None:
|
|
377
|
+
"""continuation 메시지 주입.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
수정된 request, 또는 None (에이전트 중단 신호)
|
|
381
|
+
"""
|
|
382
|
+
messages = request.messages
|
|
383
|
+
|
|
384
|
+
# ── Error recovery: 마지막 도구 실패 시 복구 guidance 주입 ──
|
|
385
|
+
last_tool_result = self._get_last_tool_result(messages)
|
|
386
|
+
if last_tool_result and not last_tool_result.get("success", True):
|
|
387
|
+
error_msg = last_tool_result.get("error", "")
|
|
388
|
+
if "command not found" in error_msg or "not found" in error_msg:
|
|
389
|
+
guidance = (
|
|
390
|
+
f"[SYSTEM] Tool '{tool_name}' failed: {error_msg[:100]}. "
|
|
391
|
+
"Replan: if 'pip' not found, use jupyter_cell(code='!pip install ...') instead. "
|
|
392
|
+
"Do NOT retry the same command."
|
|
393
|
+
)
|
|
394
|
+
new_messages = list(messages) + [HumanMessage(content=guidance)]
|
|
395
|
+
_dbg(f"injected error recovery guidance for {tool_name}")
|
|
396
|
+
return request.override(messages=new_messages)
|
|
397
|
+
|
|
398
|
+
# todos 가져오기: state 우선, 없으면 메시지에서 추출
|
|
399
|
+
todos = _get_todos_from_state(request) or _get_todos_from_messages(messages)
|
|
400
|
+
todos_status = _classify_todos(todos)
|
|
401
|
+
_dbg(
|
|
402
|
+
f"_inject tool={tool_name} todos_status={todos_status} after_wt={after_write_todos} todos={todos!r:.200s}"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# todos 없음 → 간단한 작업, guidance 불필요
|
|
406
|
+
if todos_status == "none":
|
|
407
|
+
_dbg("no todos → skip guidance")
|
|
408
|
+
return request
|
|
409
|
+
|
|
410
|
+
# pending todos 있음 → 계속 진행
|
|
411
|
+
if todos_status == "pending":
|
|
412
|
+
pending_preview = self._build_pending_preview(todos)
|
|
413
|
+
|
|
414
|
+
if after_write_todos:
|
|
415
|
+
guidance = (
|
|
416
|
+
f"[SYSTEM] Todo list updated.{pending_preview} "
|
|
417
|
+
"Now continue working on the next pending todo item. "
|
|
418
|
+
"Use 'task' for code generation. "
|
|
419
|
+
"Package installs → jupyter_cell(code='!pip install ...'). "
|
|
420
|
+
"Exploration only (file check, head, pip list) → execute. "
|
|
421
|
+
"Do NOT call write_todos again until the next task is done. "
|
|
422
|
+
"Do NOT repeat tools that already succeeded."
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
guidance = (
|
|
426
|
+
f"[SYSTEM] Tool '{tool_name}' completed successfully.{pending_preview} "
|
|
427
|
+
"Update the current todo status with write_todos, then continue "
|
|
428
|
+
"with the next pending todo item. "
|
|
429
|
+
"Use 'task' for code generation. "
|
|
430
|
+
"Package installs → jupyter_cell(code='!pip install ...'). "
|
|
431
|
+
"Exploration only (file check, head, pip list) → execute. "
|
|
432
|
+
"Do NOT repeat tools that already succeeded."
|
|
433
|
+
)
|
|
434
|
+
else:
|
|
435
|
+
# 모든 todos 완료 → summary 이미 있으면 중단, 없으면 호출 유도
|
|
436
|
+
if _has_summary_in_messages(messages):
|
|
437
|
+
_dbg("all_completed + summary exists → stopping agent")
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
guidance = (
|
|
441
|
+
"[SYSTEM] 모든 작업이 완료되었습니다. "
|
|
442
|
+
"반드시 final_summary_tool을 호출하여 작업 요약과 다음 단계를 제시하세요. "
|
|
443
|
+
"final_summary_tool(summary='완료된 작업 요약', "
|
|
444
|
+
"next_items=[{'subject': '제목', 'description': '설명'}, ...]) "
|
|
445
|
+
"(next_items 3개 이상 필수). "
|
|
446
|
+
"텍스트로 JSON을 출력하지 말고, 반드시 도구 호출로 실행하세요."
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
new_messages = list(messages) + [HumanMessage(content=guidance)]
|
|
450
|
+
_dbg(f"injected guidance for tool={tool_name} status={todos_status}")
|
|
451
|
+
_dbg(f"guidance: {guidance[:200]}")
|
|
452
|
+
return request.override(messages=new_messages)
|
|
453
|
+
|
|
454
|
+
def _build_pending_preview(self, todos: list | None) -> str:
|
|
455
|
+
"""pending 항목 미리보기 문자열 생성."""
|
|
456
|
+
if not todos:
|
|
457
|
+
return ""
|
|
458
|
+
pending_items = [
|
|
459
|
+
t
|
|
460
|
+
for t in todos
|
|
461
|
+
if (isinstance(t, dict) and t.get("status") in ("pending", "in_progress"))
|
|
462
|
+
or (hasattr(t, "status") and t.status in ("pending", "in_progress"))
|
|
463
|
+
]
|
|
464
|
+
if not pending_items:
|
|
465
|
+
return ""
|
|
466
|
+
previews = []
|
|
467
|
+
for t in pending_items[:3]:
|
|
468
|
+
c = (
|
|
469
|
+
t.get("content", "")
|
|
470
|
+
if isinstance(t, dict)
|
|
471
|
+
else getattr(t, "content", "")
|
|
472
|
+
)
|
|
473
|
+
previews.append(c[:40])
|
|
474
|
+
return f" Pending: {', '.join(previews)}."
|
|
475
|
+
|
|
476
|
+
def _strip_write_todos_if_summary(self, response: Any) -> Any:
|
|
477
|
+
"""summary JSON이 있는 응답에서 write_todos 호출 제거."""
|
|
478
|
+
ai_msg = _extract_ai_message(response)
|
|
479
|
+
if not ai_msg or not hasattr(ai_msg, "tool_calls") or not ai_msg.tool_calls:
|
|
480
|
+
return response
|
|
481
|
+
|
|
482
|
+
content = getattr(ai_msg, "content", "")
|
|
483
|
+
has_summary = isinstance(content, str) and (
|
|
484
|
+
('"summary"' in content and '"next_items"' in content)
|
|
485
|
+
or ("'summary'" in content and "'next_items'" in content)
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if not has_summary:
|
|
489
|
+
return response
|
|
490
|
+
|
|
491
|
+
non_todo = [tc for tc in ai_msg.tool_calls if tc.get("name") != "write_todos"]
|
|
492
|
+
if len(non_todo) < len(ai_msg.tool_calls):
|
|
493
|
+
ai_msg.tool_calls = non_todo
|
|
494
|
+
logger.debug("CONTINUATION: stripped write_todos from summary response")
|
|
495
|
+
|
|
496
|
+
return response
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class LimitToolCallsMiddleware(AgentMiddleware):
|
|
500
|
+
"""LLM 응답의 tool_calls를 1개로 제한 + additional_kwargs 정리.
|
|
501
|
+
|
|
502
|
+
HITL decision 수 불일치 방지:
|
|
503
|
+
- LLM이 2개 이상 tool_calls를 반환하면 첫 번째만 유지
|
|
504
|
+
- additional_kwargs['tool_calls'] 중복 제거
|
|
505
|
+
- tool_calls가 있으면 content(thinking text) 비우기
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
def awrap_model_call(
|
|
509
|
+
self,
|
|
510
|
+
request: ModelRequest,
|
|
511
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
512
|
+
) -> Awaitable[ModelCallResult]:
|
|
513
|
+
return self._impl(request, handler)
|
|
514
|
+
|
|
515
|
+
async def _impl(
|
|
516
|
+
self,
|
|
517
|
+
request: ModelRequest,
|
|
518
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
519
|
+
) -> ModelCallResult:
|
|
520
|
+
response = await handler(request)
|
|
521
|
+
|
|
522
|
+
ai_msg = _extract_ai_message(response)
|
|
523
|
+
if not ai_msg:
|
|
524
|
+
return response
|
|
525
|
+
|
|
526
|
+
tool_calls = getattr(ai_msg, "tool_calls", None)
|
|
527
|
+
if not tool_calls:
|
|
528
|
+
return response
|
|
529
|
+
|
|
530
|
+
# tool_calls가 2개 이상이면 첫 번째만 유지
|
|
531
|
+
if len(tool_calls) > 1:
|
|
532
|
+
logger.debug(
|
|
533
|
+
"LIMIT_TOOL_CALLS: truncating %d -> 1 (kept: %s)",
|
|
534
|
+
len(tool_calls),
|
|
535
|
+
tool_calls[0].get("name", "unknown"),
|
|
536
|
+
)
|
|
537
|
+
ai_msg.tool_calls = [tool_calls[0]]
|
|
538
|
+
|
|
539
|
+
# additional_kwargs['tool_calls'] 중복 제거
|
|
540
|
+
additional_kwargs = getattr(ai_msg, "additional_kwargs", {})
|
|
541
|
+
if additional_kwargs.get("tool_calls"):
|
|
542
|
+
del additional_kwargs["tool_calls"]
|
|
543
|
+
|
|
544
|
+
# tool_calls 있으면 content 비우기 (thinking text 제거)
|
|
545
|
+
if ai_msg.tool_calls and ai_msg.content:
|
|
546
|
+
ai_msg.content = ""
|
|
547
|
+
|
|
548
|
+
return response
|