codebuff-agent 2.1.74__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,334 @@
1
+ """ObservabilityMiddleware — 구조화된 JSONL 이벤트 로깅.
2
+
3
+ 모든 model/tool call을 래핑하여 타이밍 + 메타데이터를
4
+ extensions/logs/agent_structured.jsonl에 기록.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import datetime
10
+ import json
11
+ import os
12
+ import time
13
+ from collections.abc import Awaitable, Callable
14
+ from contextvars import ContextVar
15
+ from typing import Any, Optional
16
+
17
+ from codebuff_core.core.exceptions import AgentError
18
+
19
+ from langchain_core.messages import AIMessage
20
+ from langgraph.errors import GraphBubbleUp
21
+
22
+ from langchain.agents.middleware.types import (
23
+ AgentMiddleware,
24
+ ModelCallResult,
25
+ ModelRequest,
26
+ ModelResponse,
27
+ )
28
+
29
+ from ..analytics_ingest import enqueue_agent_call
30
+ from ..team_access import resolve_team
31
+
32
+ # 로그 디렉토리: log_paths 공통 모듈 사용
33
+ from ..log_paths import get_structured_log_path
34
+
35
+ _STRUCTURED_LOG = get_structured_log_path()
36
+
37
+
38
+ def _write_structured_event(event: dict) -> None:
39
+ """JSON 라인을 agent_structured.jsonl에 기록. ts 자동 추가.
40
+
41
+ 파일 기록 뒤 RDS 로도 한 벌 보낸다 (이슈 #30). 이 한 곳이 model_call /
42
+ tool_call / error_classification 세 종류를 모두 덮는다.
43
+
44
+ `agent_backend` 는 codex 경로(`codex_bridge._record`)가 이미 박아 보내므로
45
+ 여기서는 비어 있을 때만 `client` 로 채운다. 파일 기록 **전에** 찍어야 로컬
46
+ jsonl 과 RDS 행의 값이 같다 (이슈 #92).
47
+ """
48
+ try:
49
+ event["ts"] = datetime.datetime.now().isoformat(timespec="seconds")
50
+ event.setdefault("agent_backend", "client")
51
+ with open(_STRUCTURED_LOG, "a") as f:
52
+ f.write(json.dumps(event, ensure_ascii=False) + "\n")
53
+ except OSError:
54
+ pass
55
+ enqueue_agent_call(event)
56
+
57
+
58
+ def default_user_identity() -> dict[str, str]:
59
+ """구조화 이벤트에 싣는 사용자 식별 — 미들웨어·턴 계측·codex 가 같은 값을 쓴다."""
60
+ return {
61
+ "team": resolve_team(),
62
+ "project": os.environ.get("HDSP_PRJ_ID", ""),
63
+ "user_id": os.environ.get("JUPYTERHUB_USER", "")
64
+ or os.environ.get("HDSP_BDP_ID", ""),
65
+ }
66
+
67
+
68
+ def _add(total: Optional[int], value: Optional[int]) -> Optional[int]:
69
+ """토큰 합. 한 번도 안 잰 것은 None 으로 남긴다 — 0 은 "잰 값이 0" 이다."""
70
+ if value is None:
71
+ return total
72
+ return (total or 0) + value
73
+
74
+
75
+ def _usage_of(ai_msg: Optional[AIMessage]) -> dict[str, Any]:
76
+ """AIMessage 의 토큰·모델명·종료 사유. 제공자가 안 주면 None (0 이 아니다)."""
77
+ out: dict[str, Any] = {
78
+ "input_tokens": None,
79
+ "output_tokens": None,
80
+ "cached_tokens": None,
81
+ "model_name": None,
82
+ "finish_reason": None,
83
+ }
84
+ if ai_msg is None:
85
+ return out
86
+ usage = getattr(ai_msg, "usage_metadata", None) or {}
87
+ if usage:
88
+ out["input_tokens"] = usage.get("input_tokens")
89
+ out["output_tokens"] = usage.get("output_tokens")
90
+ out["cached_tokens"] = (usage.get("input_token_details") or {}).get(
91
+ "cache_read"
92
+ )
93
+ meta = getattr(ai_msg, "response_metadata", None) or {}
94
+ out["model_name"] = meta.get("model_name") or meta.get("model") or None
95
+ out["finish_reason"] = meta.get("finish_reason") or meta.get("stop_reason") or None
96
+ return out
97
+
98
+
99
+ class TurnMeter:
100
+ """프롬프트 1건(= 턴)의 계측. HITL 대기를 포함해 턴이 끝날 때 `turn` 행 1개를 낸다.
101
+
102
+ 그래프는 캐시돼 여러 thread 가 공유하므로 미들웨어 생성자 값으로는 턴을 못 가른다.
103
+ 요청마다 만들어 ContextVar 로 묶고(`bind_turn`), HITL 로 멈추면 runner 에 보관했다가
104
+ 재개 요청에서 다시 묶는다 (이슈 #98).
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ session_id: str,
110
+ ux_session_id: Optional[str] = None,
111
+ user_identity: Optional[dict[str, str]] = None,
112
+ ):
113
+ self.session_id = session_id
114
+ self.ux_session_id = ux_session_id
115
+ self.user_identity = (
116
+ user_identity if user_identity is not None else default_user_identity()
117
+ )
118
+ self.model_calls = 0
119
+ self.tool_calls = 0
120
+ self.input_tokens: Optional[int] = None
121
+ self.output_tokens: Optional[int] = None
122
+ self.cached_tokens: Optional[int] = None
123
+ self.model_name: Optional[str] = None
124
+ self.finish_reason: Optional[str] = None
125
+ self._t0 = time.monotonic()
126
+ self._emitted = False
127
+
128
+ def add_model_call(self, usage: dict[str, Any], tool_calls: int) -> None:
129
+ self.model_calls += 1
130
+ self.input_tokens = _add(self.input_tokens, usage.get("input_tokens"))
131
+ self.output_tokens = _add(self.output_tokens, usage.get("output_tokens"))
132
+ self.cached_tokens = _add(self.cached_tokens, usage.get("cached_tokens"))
133
+ self.model_name = usage.get("model_name") or self.model_name
134
+ self.finish_reason = usage.get("finish_reason") or self.finish_reason
135
+
136
+ def emit(self, status: str) -> None:
137
+ """turn 행을 낸다. 종료 지점이 여럿이라 두 번째부터는 무시한다."""
138
+ if self._emitted:
139
+ return
140
+ self._emitted = True
141
+ _write_structured_event(
142
+ {
143
+ "event": "turn",
144
+ "session_id": self.session_id,
145
+ "ux_session_id": self.ux_session_id,
146
+ **self.user_identity,
147
+ "call_num": self.model_calls,
148
+ "output_tool_calls": self.tool_calls,
149
+ "elapsed_ms": round((time.monotonic() - self._t0) * 1000, 1),
150
+ "status": status,
151
+ "model_name": self.model_name,
152
+ "input_tokens": self.input_tokens,
153
+ "output_tokens": self.output_tokens,
154
+ "cached_tokens": self.cached_tokens,
155
+ "finish_reason": self.finish_reason,
156
+ }
157
+ )
158
+
159
+
160
+ _TURN: ContextVar[Optional[TurnMeter]] = ContextVar("codebuff_turn", default=None)
161
+
162
+
163
+ def bind_turn(meter: Optional[TurnMeter]) -> None:
164
+ """이 컨텍스트(와 여기서 만드는 태스크)의 턴을 정한다.
165
+
166
+ `astream_events` 는 첫 iteration 에 태스크를 만들며 컨텍스트를 복사하므로 **그 전에**
167
+ 불러야 한다.
168
+ """
169
+ _TURN.set(meter)
170
+
171
+
172
+ def emit_parked_turn(runner: Any, status: str) -> None:
173
+ """HITL 대기로 runner 에 보관된 턴을 닫는다 — 취소·리셋이 runner 를 버릴 때."""
174
+ meter = getattr(runner, "turn_meter", None)
175
+ if meter is not None:
176
+ meter.emit(status)
177
+
178
+
179
+ def _extract_ai_message(response: Any) -> AIMessage | None:
180
+ """ModelResponse/ModelCallResult에서 AIMessage 추출."""
181
+ if isinstance(response, AIMessage):
182
+ return response
183
+ if isinstance(response, ModelResponse):
184
+ for msg in reversed(response.result):
185
+ if isinstance(msg, AIMessage):
186
+ return msg
187
+ if hasattr(response, "message") and isinstance(response.message, AIMessage):
188
+ return response.message
189
+ if hasattr(response, "messages"):
190
+ for msg in reversed(response.messages):
191
+ if isinstance(msg, AIMessage):
192
+ return msg
193
+ return None
194
+
195
+
196
+ class ObservabilityMiddleware(AgentMiddleware):
197
+ """model/tool call 타이밍 + 메타데이터를 JSONL로 기록.
198
+
199
+ `agent_name` 은 행이 어느 에이전트의 호출인지 가른다 — 오케스트레이터는 `main`,
200
+ multi 모드 서브에이전트는 자기 이름이다 (이슈 #98).
201
+ """
202
+
203
+ def __init__(
204
+ self,
205
+ session_id: str = "",
206
+ user_identity: Optional[dict[str, str]] = None,
207
+ agent_name: str = "main",
208
+ ):
209
+ self._session_id = session_id
210
+ self._user_identity = user_identity or {}
211
+ self._agent_name = agent_name
212
+ self._model_call_count = 0
213
+ self._tool_call_count = 0
214
+
215
+ def awrap_model_call(
216
+ self,
217
+ request: ModelRequest,
218
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
219
+ ) -> Awaitable[ModelCallResult]:
220
+ return self._wrap_model(request, handler)
221
+
222
+ async def _wrap_model(
223
+ self,
224
+ request: ModelRequest,
225
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
226
+ ) -> ModelCallResult:
227
+ self._model_call_count += 1
228
+ call_num = self._model_call_count
229
+ input_msg_count = len(request.messages) if request.messages else 0
230
+
231
+ meter = _TURN.get()
232
+ t0 = time.monotonic()
233
+ status = "ok"
234
+ output_tool_calls = 0
235
+ usage = _usage_of(None)
236
+ try:
237
+ response = await handler(request)
238
+ ai_msg = _extract_ai_message(response)
239
+ if ai_msg and hasattr(ai_msg, "tool_calls") and ai_msg.tool_calls:
240
+ output_tool_calls = len(ai_msg.tool_calls)
241
+ usage = _usage_of(ai_msg)
242
+ return response
243
+ except Exception as exc:
244
+ status = (
245
+ f"error:{exc.error_type}"
246
+ if isinstance(exc, AgentError)
247
+ else f"error:{type(exc).__name__}"
248
+ )
249
+ raise
250
+ finally:
251
+ elapsed_ms = (time.monotonic() - t0) * 1000
252
+ if meter is not None:
253
+ meter.add_model_call(usage, output_tool_calls)
254
+ _write_structured_event(
255
+ {
256
+ "event": "model_call",
257
+ **self._ids(meter),
258
+ "agent_name": self._agent_name,
259
+ **self._user_identity,
260
+ "call_num": call_num,
261
+ "input_msg_count": input_msg_count,
262
+ "output_tool_calls": output_tool_calls,
263
+ "elapsed_ms": round(elapsed_ms, 1),
264
+ "status": status,
265
+ **usage,
266
+ }
267
+ )
268
+
269
+ def _ids(self, meter: Optional[TurnMeter]) -> dict[str, str]:
270
+ """턴이 묶여 있으면 그 thread 를 쓴다 — 생성자 값은 캐시된 그래프의 첫 thread 다."""
271
+ if meter is None:
272
+ return {"session_id": self._session_id}
273
+ return {"session_id": meter.session_id, "ux_session_id": meter.ux_session_id}
274
+
275
+ async def awrap_tool_call(self, request, handler):
276
+ meter = _TURN.get()
277
+ tool_name = request.tool_call.get("name", "unknown")
278
+
279
+ t0 = time.monotonic()
280
+ status = "ok"
281
+ error_msg = None
282
+ try:
283
+ result = await handler(request)
284
+ return result
285
+ except GraphBubbleUp:
286
+ # 서브에이전트 HITL 이 task 도구 안에서 올라온 것 — 오류도 실행도 아니다.
287
+ # 재개하면 도구가 다시 돌고 그때 행이 나간다 (이슈 #98).
288
+ status = None
289
+ raise
290
+ except Exception as exc:
291
+ status = (
292
+ f"error:{exc.error_type}"
293
+ if isinstance(exc, AgentError)
294
+ else f"error:{type(exc).__name__}"
295
+ )
296
+ error_msg = str(exc)[:200]
297
+ raise
298
+ finally:
299
+ if status is not None:
300
+ self._write_tool_call(meter, tool_name, t0, status, error_msg)
301
+
302
+ def _write_tool_call(
303
+ self,
304
+ meter: Optional[TurnMeter],
305
+ tool_name: str,
306
+ t0: float,
307
+ status: str,
308
+ error_msg: Optional[str],
309
+ ) -> None:
310
+ if meter is not None:
311
+ meter.tool_calls += 1
312
+ self._tool_call_count += 1
313
+ call_num = self._tool_call_count
314
+ elapsed_ms = (time.monotonic() - t0) * 1000
315
+ ev: dict[str, Any] = {
316
+ "event": "tool_call",
317
+ **self._ids(meter),
318
+ "agent_name": self._agent_name,
319
+ **self._user_identity,
320
+ "tool_name": tool_name,
321
+ "call_num": call_num,
322
+ "elapsed_ms": round(elapsed_ms, 1),
323
+ "status": status,
324
+ }
325
+ if error_msg:
326
+ ev["error"] = error_msg
327
+ _write_structured_event(ev)
328
+
329
+ def get_stats(self) -> dict:
330
+ """현재까지의 model_calls, tool_calls 카운트 반환."""
331
+ return {
332
+ "model_calls": self._model_call_count,
333
+ "tool_calls": self._tool_call_count,
334
+ }
@@ -0,0 +1,57 @@
1
+ """Middleware to patch dangling tool calls in the messages history."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from langchain.agents.middleware import AgentMiddleware, AgentState
8
+ from langchain_core.messages import RemoveMessage, ToolMessage
9
+ from langgraph.graph.message import REMOVE_ALL_MESSAGES
10
+ from langgraph.runtime import Runtime
11
+
12
+
13
+ class PatchToolCallsMiddleware(AgentMiddleware):
14
+ """Middleware to patch dangling tool calls in the messages history.
15
+
16
+ Before the agent runs, scans AI messages for tool calls that were
17
+ never fulfilled and creates synthetic ToolMessages to prevent agent
18
+ state corruption when tool calls are incomplete.
19
+ """
20
+
21
+ def before_agent(
22
+ self, state: AgentState, runtime: Runtime[Any]
23
+ ) -> dict[str, Any] | None: # noqa: ARG002
24
+ """Handle dangling tool calls from any AIMessage."""
25
+ messages = state["messages"]
26
+ if not messages or len(messages) == 0:
27
+ return None
28
+
29
+ patched_messages = []
30
+ for i, msg in enumerate(messages):
31
+ patched_messages.append(msg)
32
+ if msg.type == "ai" and msg.tool_calls:
33
+ for tool_call in msg.tool_calls:
34
+ corresponding_tool_msg = next(
35
+ (
36
+ msg
37
+ for msg in messages[i:]
38
+ if msg.type == "tool"
39
+ and msg.tool_call_id == tool_call["id"]
40
+ ),
41
+ None,
42
+ )
43
+ if corresponding_tool_msg is None:
44
+ tool_msg = (
45
+ f"Tool call {tool_call['name']} with id "
46
+ f"{tool_call['id']} was cancelled - another "
47
+ "message came in before it could be completed."
48
+ )
49
+ patched_messages.append(
50
+ ToolMessage(
51
+ content=tool_msg,
52
+ name=tool_call["name"],
53
+ tool_call_id=tool_call["id"],
54
+ )
55
+ )
56
+
57
+ return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *patched_messages]}
@@ -0,0 +1,112 @@
1
+ """System prompts for the agent.
2
+
3
+ multi 모드 planner 는 여기 없다 — `prompts.planner` 키다 (이슈 #218).
4
+ """
5
+
6
+ SYSTEM_PROMPTS = {
7
+ "default": """You are Codebuff, an AI assistant that helps users with data science and programming tasks in Jupyter notebooks.
8
+
9
+ You have access to various tools to help accomplish tasks:
10
+ - jupyter_cell: Create, read, update, delete, or execute Jupyter notebook cells
11
+ - jupyter_markdown: Create or update markdown cells for documentation
12
+ - file_read: Read file contents
13
+ - file_write: Write content to files
14
+ - file_list: List directory contents
15
+ - search_code: Search for patterns in code files
16
+ - web_search: Search the web for information
17
+
18
+ Guidelines:
19
+ 1. Always explain what you're going to do before taking action
20
+ 2. Use tools step by step - don't try to do everything at once
21
+ 3. After executing code, check the output and handle any errors
22
+ 4. Be concise but informative in your responses
23
+ 5. If you're unsure about something, ask the user for clarification
24
+
25
+ When working with notebooks:
26
+ - Create new cells for new code
27
+ - Update existing cells when fixing or modifying code
28
+ - Add markdown cells to explain your work
29
+ - Execute cells to verify your code works""",
30
+ "data_science": """You are Codebuff, a specialized AI assistant for data science tasks in Jupyter notebooks.
31
+
32
+ You excel at:
33
+ - Data loading and preprocessing
34
+ - Exploratory data analysis (EDA)
35
+ - Statistical analysis
36
+ - Data visualization with matplotlib, seaborn, plotly
37
+ - Machine learning with scikit-learn, pandas
38
+ - Deep learning with PyTorch, TensorFlow
39
+
40
+ Available tools:
41
+ - jupyter_cell: Create, read, update, delete, or execute Jupyter notebook cells
42
+ - jupyter_markdown: Create or update markdown cells for documentation
43
+ - file_read: Read data files and scripts
44
+ - file_write: Save processed data or results
45
+ - file_list: Explore data directories
46
+ - search_code: Find code patterns
47
+ - web_search: Look up documentation or examples
48
+
49
+ Best practices:
50
+ 1. Start by understanding the data structure
51
+ 2. Handle missing values and outliers appropriately
52
+ 3. Visualize data before and after transformations
53
+ 4. Document your analysis steps with markdown cells
54
+ 5. Use meaningful variable names
55
+ 6. Test code incrementally""",
56
+ "code_review": """You are Codebuff, a hybrid code reviewer that combines automated tool scanning with LLM deep analysis.
57
+
58
+ ## 3-Phase Hybrid Code Review Workflow
59
+
60
+ ### Phase 1: 도구 스캔 (MANDATORY - 반드시 먼저 실행)
61
+ 코드 리뷰를 시작하기 전에 반드시 security_scan 도구를 호출하세요:
62
+ - `security_scan(scan_type="all")` 을 실행하여 자동화된 스캐너 결과를 먼저 확보
63
+ - 이 단계를 건너뛰지 마세요. 도구 스캔 결과가 Phase 2의 입력이 됩니다.
64
+ - 스캔 대상: SAST(bandit, semgrep), 시크릿 탐지(detect-secrets), 우회 패턴(evasion), 개인정보(PII)
65
+
66
+ ### Phase 2: LLM 심층 분석
67
+ 도구 스캔 결과를 바탕으로 다음을 추가 분석하세요:
68
+ - **중복 제거**: 도구가 동일 이슈를 다른 이름으로 보고한 경우 통합
69
+ - **비즈니스 로직 취약점**: 도구가 탐지 못하는 논리적 결함, 레이스 컨디션, 권한 상승 등
70
+ - **한국 금융 컴플라이언스**: 개인정보보호법, 신용정보법, 전자금융거래법 관점의 준수 여부
71
+ - **컨텍스트 분석**: 코드의 실제 의도와 사용 맥락을 고려한 위양성(false positive) 필터링
72
+
73
+ ### Phase 3: 통합 리포트 (5-관점 포맷)
74
+ 아래 5개 섹션으로 결과를 정리하세요. 각 발견사항에 출처 태그를 명시:
75
+
76
+ 1. **시큐어코딩** - 입력 검증, 인젝션, 민감 정보 노출, 크리덴셜 관리
77
+ - 각 항목에 [도구발견] 또는 [LLM분석] 태그 표기
78
+ - 심각도 표기: 🔴 Critical, 🟠 High, 🟡 Medium, 🔵 Low
79
+ - 하나의 셀에 여러 취약점이 있으면 각각 별도 항목으로 분리
80
+ - 은닉 패턴(변수 조합, 역순, ROT13, base64, 함수 기본 인자)은 패턴별로 개별 지적
81
+ - DataFrame PII는 모든 PII 컬럼을 나열하고 각각 마스킹 방안 제시
82
+ 2. **개인정보보호(PII)** - 한국 금융권 개인정보 패턴, 마스킹, 암호화, 로깅 안전성
83
+ - 관련 법률 명시 (개인정보보호법/신용정보법/전자금융거래법)
84
+ 3. **성능** - OOM 방지, 전처리·학습 속도 개선, 컴퓨팅 리소스 최적 활용. 불필요한 DataFrame 복사, 비효율적 dtypes, 중복 연산 등 DS 실무 병목 식별 (프롬프트에 실행 환경 사양이 포함된 경우 해당 환경에 맞춘 구체적 조언 제공)
85
+ 4. **코드 품질** - 구조, 일관성, 에러 처리, 타입 힌트
86
+ 5. **요약** - 종합 평가, 도구 스캔 통계, 개선 우선순위 Top 3, 개선 필요 셀의 전체 교체 코드
87
+
88
+ Available tools:
89
+ - jupyter_cell: Examine and modify code cells
90
+ - jupyter_markdown: Add review comments and documentation
91
+ - file_read: Read source files
92
+ - search_code: Find related code patterns
93
+ - web_search: Look up best practices
94
+ - security_scan: 자동화된 보안 스캔 실행 (bandit, semgrep, detect-secrets, evasion, pii)
95
+
96
+ Important formatting rules:
97
+ - 개선 코드는 반드시 마크다운 코드 블록(```python)으로 제시하세요.
98
+ - 절대 표(table) 형식 안에 코드를 넣지 마세요.""",
99
+ }
100
+
101
+
102
+ def get_system_prompt(mode: str = "default") -> str:
103
+ """
104
+ Get system prompt for the specified mode.
105
+
106
+ Args:
107
+ mode: Agent mode (default, data_science, code_review)
108
+
109
+ Returns:
110
+ System prompt string
111
+ """
112
+ return SYSTEM_PROMPTS.get(mode, SYSTEM_PROMPTS["default"])