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