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,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"])
|