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,837 @@
1
+ """Analytics Aggregation Script — JSONL → CSV 리포트 생성.
2
+
3
+ analytics JSONL 파일들을 읽어 C레벨 보고용 CSV 리포트를 생성합니다.
4
+ - A1_active_users.csv: Jupyter vs Agent DAU/MAU
5
+ - A2_agent_frequency.csv: 사용자별 사용 빈도
6
+ - A3_feature_preference.csv: 기능별 사용량
7
+ - A4_errors.csv: 실패 유형 집계
8
+ - A5_cell_attribution.csv: 일별/유저별 agent 셀 기여율
9
+ - A6_codebuff_users.csv: Jupyter-only vs Code Buff 사용자 (MAU/DAU)
10
+ - A7_codebuff_feature_mix.csv: Code Buff 기능별 비중 (softmax + 건수)
11
+ - A8_codebuff_utilization.csv: Code Buff 활용률 (세션 단위 binary)
12
+
13
+ Usage:
14
+ python scripts/analytics_aggregate.py \
15
+ --input extensions/logs/ \
16
+ --output /tmp/reports/ \
17
+ --date-from 2026-03-01 \
18
+ --date-to 2026-03-31
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+
32
+ CODEBUFF_FEATURES = frozenset(
33
+ {
34
+ "code_insert_from_agent",
35
+ "code_copy_from_agent",
36
+ "code_paste_from_agent",
37
+ }
38
+ )
39
+
40
+
41
+ def _read_jsonl_files(input_dir: Path, prefix: str) -> list[dict]:
42
+ """Read all JSONL files matching prefix and return list of parsed dicts."""
43
+ records: list[dict] = []
44
+ for p in sorted(input_dir.glob(f"{prefix}*.jsonl")):
45
+ with open(p) as f:
46
+ for line in f:
47
+ line = line.strip()
48
+ if not line:
49
+ continue
50
+ try:
51
+ records.append(json.loads(line))
52
+ except json.JSONDecodeError:
53
+ continue
54
+ return records
55
+
56
+
57
+ def _filter_by_date(
58
+ df: pd.DataFrame,
59
+ date_col: str,
60
+ date_from: Optional[str],
61
+ date_to: Optional[str],
62
+ ) -> pd.DataFrame:
63
+ """Filter DataFrame by date range (inclusive)."""
64
+ if df.empty:
65
+ return df
66
+ if date_from:
67
+ df = df[df[date_col] >= date_from]
68
+ if date_to:
69
+ df = df[df[date_col] <= date_to]
70
+ return df
71
+
72
+
73
+ def aggregate_active_users(
74
+ impressions: list[dict],
75
+ clicks: list[dict],
76
+ date_from: Optional[str] = None,
77
+ date_to: Optional[str] = None,
78
+ ) -> pd.DataFrame:
79
+ """A1: Jupyter vs Agent DAU/MAU."""
80
+ # Build per-user-per-date sets
81
+ imp_df = pd.DataFrame(impressions)
82
+ click_df = pd.DataFrame(clicks)
83
+
84
+ if imp_df.empty and click_df.empty:
85
+ return pd.DataFrame(
86
+ columns=[
87
+ "date",
88
+ "jupyter_dau",
89
+ "agent_dau",
90
+ "non_agent_dau",
91
+ "agent_adoption_rate",
92
+ "jupyter_mau",
93
+ "agent_mau",
94
+ "agent_mau_rate",
95
+ ]
96
+ )
97
+
98
+ # Extract date from datetime field
99
+ if not imp_df.empty:
100
+ imp_df["date"] = imp_df["datetime"].str[:10]
101
+ imp_df = _filter_by_date(imp_df, "date", date_from, date_to)
102
+
103
+ if not click_df.empty:
104
+ click_df["date"] = click_df["datetime"].str[:10]
105
+ click_df = _filter_by_date(click_df, "date", date_from, date_to)
106
+
107
+ # Jupyter DAU: users with session_start impression
108
+ if not imp_df.empty:
109
+ session_starts = imp_df[imp_df["feature"] == "session_start"]
110
+ if not session_starts.empty:
111
+ jupyter_daily_df = (
112
+ session_starts.groupby("date")["user_id"].apply(set).reset_index()
113
+ )
114
+ jupyter_daily_df.columns = ["date", "jupyter_users"]
115
+ else:
116
+ jupyter_daily_df = pd.DataFrame(columns=["date", "jupyter_users"])
117
+ else:
118
+ jupyter_daily_df = pd.DataFrame(columns=["date", "jupyter_users"])
119
+
120
+ # Agent DAU: users with at least 1 click event
121
+ if not click_df.empty:
122
+ agent_daily_df = click_df.groupby("date")["user_id"].apply(set).reset_index()
123
+ agent_daily_df.columns = ["date", "agent_users"]
124
+ else:
125
+ agent_daily_df = pd.DataFrame(columns=["date", "agent_users"])
126
+
127
+ # Merge daily
128
+ all_dates = set()
129
+ if not jupyter_daily_df.empty:
130
+ all_dates.update(jupyter_daily_df["date"].tolist())
131
+ if not agent_daily_df.empty:
132
+ all_dates.update(agent_daily_df["date"].tolist())
133
+
134
+ rows = []
135
+ jupyter_daily_map = {}
136
+ agent_daily_map = {}
137
+ if not jupyter_daily_df.empty:
138
+ jupyter_daily_map = dict(
139
+ zip(jupyter_daily_df["date"], jupyter_daily_df["jupyter_users"])
140
+ )
141
+ if not agent_daily_df.empty:
142
+ agent_daily_map = dict(
143
+ zip(agent_daily_df["date"], agent_daily_df["agent_users"])
144
+ )
145
+
146
+ for date in sorted(all_dates):
147
+ j_users = jupyter_daily_map.get(date, set())
148
+ a_users = agent_daily_map.get(date, set())
149
+ jupyter_dau = len(j_users)
150
+ agent_dau = len(a_users)
151
+ non_agent_dau = len(j_users - a_users)
152
+ adoption = round(agent_dau / jupyter_dau, 4) if jupyter_dau > 0 else 0.0
153
+ rows.append(
154
+ {
155
+ "date": date,
156
+ "jupyter_dau": jupyter_dau,
157
+ "agent_dau": agent_dau,
158
+ "non_agent_dau": non_agent_dau,
159
+ "agent_adoption_rate": adoption,
160
+ }
161
+ )
162
+
163
+ daily_df = pd.DataFrame(rows)
164
+
165
+ # MAU: same logic at month level
166
+ month_jupyter: dict[str, set] = {}
167
+ month_agent: dict[str, set] = {}
168
+
169
+ if not imp_df.empty:
170
+ session_starts = imp_df[imp_df["feature"] == "session_start"]
171
+ for _, row in session_starts.iterrows():
172
+ m = row["date"][:7]
173
+ month_jupyter.setdefault(m, set()).add(row["user_id"])
174
+
175
+ if not click_df.empty:
176
+ for _, row in click_df.iterrows():
177
+ m = row["date"][:7]
178
+ month_agent.setdefault(m, set()).add(row["user_id"])
179
+
180
+ all_months = sorted(set(list(month_jupyter.keys()) + list(month_agent.keys())))
181
+ mau_map: dict[str, dict] = {}
182
+ for m in all_months:
183
+ j_mau = len(month_jupyter.get(m, set()))
184
+ a_mau = len(month_agent.get(m, set()))
185
+ mau_rate = round(a_mau / j_mau, 4) if j_mau > 0 else 0.0
186
+ mau_map[m] = {
187
+ "jupyter_mau": j_mau,
188
+ "agent_mau": a_mau,
189
+ "agent_mau_rate": mau_rate,
190
+ }
191
+
192
+ # Attach MAU to daily rows by month
193
+ if not daily_df.empty:
194
+ daily_df["month"] = daily_df["date"].str[:7]
195
+ daily_df["jupyter_mau"] = daily_df["month"].map(
196
+ lambda m: mau_map.get(m, {}).get("jupyter_mau", 0)
197
+ )
198
+ daily_df["agent_mau"] = daily_df["month"].map(
199
+ lambda m: mau_map.get(m, {}).get("agent_mau", 0)
200
+ )
201
+ daily_df["agent_mau_rate"] = daily_df["month"].map(
202
+ lambda m: mau_map.get(m, {}).get("agent_mau_rate", 0.0)
203
+ )
204
+ daily_df = daily_df.drop(columns=["month"])
205
+
206
+ return daily_df
207
+
208
+
209
+ def aggregate_agent_frequency(
210
+ clicks: list[dict],
211
+ date_from: Optional[str] = None,
212
+ date_to: Optional[str] = None,
213
+ ) -> pd.DataFrame:
214
+ """A2: 사용자별 사용 빈도."""
215
+ if not clicks:
216
+ return pd.DataFrame(
217
+ columns=["team", "project", "user_id", "date", "click_count"]
218
+ )
219
+
220
+ df = pd.DataFrame(clicks)
221
+ df["date"] = df["datetime"].str[:10]
222
+ df = _filter_by_date(df, "date", date_from, date_to)
223
+
224
+ if df.empty:
225
+ return pd.DataFrame(
226
+ columns=["team", "project", "user_id", "date", "click_count"]
227
+ )
228
+
229
+ # Fill missing identity fields
230
+ for col in ("team", "project", "user_id"):
231
+ if col not in df.columns:
232
+ df[col] = ""
233
+ df[col] = df[col].fillna("")
234
+
235
+ result = (
236
+ df.groupby(["team", "project", "user_id", "date"])
237
+ .size()
238
+ .reset_index(name="click_count")
239
+ .sort_values(["date", "team", "project", "user_id"])
240
+ )
241
+ return result
242
+
243
+
244
+ def aggregate_feature_preference(
245
+ clicks: list[dict],
246
+ date_from: Optional[str] = None,
247
+ date_to: Optional[str] = None,
248
+ ) -> pd.DataFrame:
249
+ """A3: 기능별 사용량."""
250
+ if not clicks:
251
+ return pd.DataFrame(columns=["feature", "count", "percentage"])
252
+
253
+ df = pd.DataFrame(clicks)
254
+ df["date"] = df["datetime"].str[:10]
255
+ df = _filter_by_date(df, "date", date_from, date_to)
256
+
257
+ if df.empty:
258
+ return pd.DataFrame(columns=["feature", "count", "percentage"])
259
+
260
+ result = (
261
+ df.groupby("feature")
262
+ .size()
263
+ .reset_index(name="count")
264
+ .sort_values("count", ascending=False)
265
+ )
266
+ total = result["count"].sum()
267
+ result["percentage"] = (result["count"] / total * 100).round(2)
268
+ return result
269
+
270
+
271
+ def aggregate_errors(
272
+ errors: list[dict],
273
+ structured_logs: list[dict],
274
+ date_from: Optional[str] = None,
275
+ date_to: Optional[str] = None,
276
+ ) -> pd.DataFrame:
277
+ """A4: 실패 유형 집계 (프론트엔드 + 백엔드)."""
278
+ rows = []
279
+
280
+ # Frontend errors from analytics_error JSONL
281
+ if errors:
282
+ err_df = pd.DataFrame(errors)
283
+ err_df["date"] = err_df["datetime"].str[:10]
284
+ err_df = _filter_by_date(err_df, "date", date_from, date_to)
285
+ if not err_df.empty:
286
+ fe_counts = err_df.groupby("feature").size().reset_index(name="count")
287
+ for _, r in fe_counts.iterrows():
288
+ rows.append(
289
+ {
290
+ "source": "frontend",
291
+ "feature_or_error_type": r["feature"],
292
+ "count": r["count"],
293
+ }
294
+ )
295
+
296
+ # Backend errors from agent_structured.jsonl (status starts with "error:")
297
+ if structured_logs:
298
+ for rec in structured_logs:
299
+ status = rec.get("status", "")
300
+ if isinstance(status, str) and status.startswith("error:"):
301
+ ts = rec.get("ts", "")
302
+ date = ts[:10] if ts else ""
303
+ if date_from and date < date_from:
304
+ continue
305
+ if date_to and date > date_to:
306
+ continue
307
+ error_type = status.replace("error:", "", 1)
308
+ rows.append(
309
+ {
310
+ "source": "backend",
311
+ "feature_or_error_type": error_type,
312
+ "count": 1,
313
+ }
314
+ )
315
+
316
+ if not rows:
317
+ return pd.DataFrame(columns=["source", "feature_or_error_type", "count"])
318
+
319
+ result = (
320
+ pd.DataFrame(rows)
321
+ .groupby(["source", "feature_or_error_type"])["count"]
322
+ .sum()
323
+ .reset_index()
324
+ .sort_values(["source", "count"], ascending=[True, False])
325
+ )
326
+ return result
327
+
328
+
329
+ def aggregate_cell_attribution(
330
+ impressions: list[dict],
331
+ clicks: list[dict],
332
+ date_from: Optional[str] = None,
333
+ date_to: Optional[str] = None,
334
+ ) -> pd.DataFrame:
335
+ """A5: 노트북 저장 시점의 agent 셀 기여 비율 + 코드 복사 기여."""
336
+ cols = [
337
+ "team",
338
+ "project",
339
+ "user_id",
340
+ "date",
341
+ "total_code_cells",
342
+ "agent_created_code_cells",
343
+ "agent_modified_code_cells",
344
+ "agent_pasted_code_cells",
345
+ "agent_code_ratio_pct",
346
+ "total_markdown_cells",
347
+ "agent_markdown_cells",
348
+ "code_copy_count",
349
+ "code_copy_lines",
350
+ ]
351
+ if not impressions and not clicks:
352
+ return pd.DataFrame(columns=cols)
353
+
354
+ # --- Cell attribution from impressions ---
355
+ attr_rows = []
356
+ if impressions:
357
+ imp_df = pd.DataFrame(impressions)
358
+ attr = imp_df[imp_df["feature"] == "cell_attribution"].copy()
359
+ if not attr.empty:
360
+ attr["date"] = attr["datetime"].str[:10]
361
+ attr = _filter_by_date(attr, "date", date_from, date_to)
362
+ if not attr.empty:
363
+ for col in ("team", "project", "user_id"):
364
+ if col not in attr.columns:
365
+ attr[col] = ""
366
+ attr[col] = attr[col].fillna("")
367
+
368
+ numeric_fields = cols[4:11] # cell count fields
369
+ for field in numeric_fields:
370
+ attr[field] = attr["context"].apply(
371
+ lambda c, f=field: c.get(f, 0) if isinstance(c, dict) else 0
372
+ )
373
+
374
+ # 유저별 일별 마지막 저장 스냅샷 기준
375
+ attr_rows_df = (
376
+ attr.groupby(["team", "project", "user_id", "date"])
377
+ .agg({f: "last" for f in numeric_fields})
378
+ .reset_index()
379
+ )
380
+ attr_rows = attr_rows_df.to_dict("records")
381
+
382
+ # --- Code copy from clicks ---
383
+ copy_map: dict[tuple, dict] = {} # (team, project, user_id, date) -> counts
384
+ if clicks:
385
+ click_df = pd.DataFrame(clicks)
386
+ copies = click_df[click_df["feature"] == "code_copy_from_agent"].copy()
387
+ if not copies.empty:
388
+ copies["date"] = copies["datetime"].str[:10]
389
+ copies = _filter_by_date(copies, "date", date_from, date_to)
390
+ if not copies.empty:
391
+ for col in ("team", "project", "user_id"):
392
+ if col not in copies.columns:
393
+ copies[col] = ""
394
+ copies[col] = copies[col].fillna("")
395
+
396
+ copies["_lines"] = copies["context"].apply(
397
+ lambda c: c.get("code_lines", 0) if isinstance(c, dict) else 0
398
+ )
399
+ grouped = (
400
+ copies.groupby(["team", "project", "user_id", "date"])
401
+ .agg(
402
+ code_copy_count=("_lines", "count"),
403
+ code_copy_lines=("_lines", "sum"),
404
+ )
405
+ .reset_index()
406
+ )
407
+ for _, r in grouped.iterrows():
408
+ key = (r["team"], r["project"], r["user_id"], r["date"])
409
+ copy_map[key] = {
410
+ "code_copy_count": int(r["code_copy_count"]),
411
+ "code_copy_lines": int(r["code_copy_lines"]),
412
+ }
413
+
414
+ # --- Merge ---
415
+ if not attr_rows and not copy_map:
416
+ return pd.DataFrame(columns=cols)
417
+
418
+ # Start from attribution rows, then merge copy data
419
+ result_map: dict[tuple, dict] = {}
420
+ for row in attr_rows:
421
+ key = (row["team"], row["project"], row["user_id"], row["date"])
422
+ result_map[key] = row
423
+
424
+ # Add copy data (may create new rows if user only copied, no cell attribution)
425
+ for key, cdata in copy_map.items():
426
+ if key in result_map:
427
+ result_map[key].update(cdata)
428
+ else:
429
+ result_map[key] = {
430
+ "team": key[0],
431
+ "project": key[1],
432
+ "user_id": key[2],
433
+ "date": key[3],
434
+ "total_code_cells": 0,
435
+ "agent_created_code_cells": 0,
436
+ "agent_modified_code_cells": 0,
437
+ "agent_pasted_code_cells": 0,
438
+ "agent_code_ratio_pct": 0,
439
+ "total_markdown_cells": 0,
440
+ "agent_markdown_cells": 0,
441
+ **cdata,
442
+ }
443
+
444
+ # Fill missing copy fields
445
+ for row in result_map.values():
446
+ row.setdefault("code_copy_count", 0)
447
+ row.setdefault("code_copy_lines", 0)
448
+
449
+ result = (
450
+ pd.DataFrame(list(result_map.values()), columns=cols)
451
+ .sort_values(["date", "team", "project", "user_id"])
452
+ .reset_index(drop=True)
453
+ )
454
+ return result
455
+
456
+
457
+ def aggregate_codebuff_users(
458
+ impressions: list[dict],
459
+ clicks: list[dict],
460
+ date_from: Optional[str] = None,
461
+ date_to: Optional[str] = None,
462
+ ) -> pd.DataFrame:
463
+ """A6: Jupyter-only vs Code Buff DAU/MAU."""
464
+ empty_cols = [
465
+ "date",
466
+ "jupyter_dau",
467
+ "codebuff_dau",
468
+ "jupyter_only_dau",
469
+ "codebuff_adoption_rate",
470
+ "jupyter_mau",
471
+ "codebuff_mau",
472
+ "jupyter_only_mau",
473
+ "codebuff_mau_rate",
474
+ ]
475
+
476
+ imp_df = pd.DataFrame(impressions)
477
+ click_df = pd.DataFrame(clicks)
478
+
479
+ if imp_df.empty and click_df.empty:
480
+ return pd.DataFrame(columns=empty_cols)
481
+
482
+ if not imp_df.empty:
483
+ imp_df["date"] = imp_df["datetime"].str[:10]
484
+ imp_df = _filter_by_date(imp_df, "date", date_from, date_to)
485
+
486
+ if not click_df.empty:
487
+ click_df["date"] = click_df["datetime"].str[:10]
488
+ click_df = _filter_by_date(click_df, "date", date_from, date_to)
489
+
490
+ # Jupyter DAU: users with session_start impression
491
+ if not imp_df.empty:
492
+ session_starts = imp_df[imp_df["feature"] == "session_start"]
493
+ if not session_starts.empty:
494
+ jupyter_daily_df = (
495
+ session_starts.groupby("date")["user_id"].apply(set).reset_index()
496
+ )
497
+ jupyter_daily_df.columns = ["date", "jupyter_users"]
498
+ else:
499
+ jupyter_daily_df = pd.DataFrame(columns=["date", "jupyter_users"])
500
+ else:
501
+ jupyter_daily_df = pd.DataFrame(columns=["date", "jupyter_users"])
502
+
503
+ # Code Buff DAU: users with code buff click features only
504
+ if not click_df.empty:
505
+ cb_clicks = click_df[click_df["feature"].isin(CODEBUFF_FEATURES)]
506
+ if not cb_clicks.empty:
507
+ cb_daily_df = cb_clicks.groupby("date")["user_id"].apply(set).reset_index()
508
+ cb_daily_df.columns = ["date", "codebuff_users"]
509
+ else:
510
+ cb_daily_df = pd.DataFrame(columns=["date", "codebuff_users"])
511
+ else:
512
+ cb_daily_df = pd.DataFrame(columns=["date", "codebuff_users"])
513
+
514
+ # Merge daily
515
+ all_dates: set[str] = set()
516
+ if not jupyter_daily_df.empty:
517
+ all_dates.update(jupyter_daily_df["date"].tolist())
518
+ if not cb_daily_df.empty:
519
+ all_dates.update(cb_daily_df["date"].tolist())
520
+
521
+ jupyter_daily_map: dict[str, set] = {}
522
+ cb_daily_map: dict[str, set] = {}
523
+ if not jupyter_daily_df.empty:
524
+ jupyter_daily_map = dict(
525
+ zip(jupyter_daily_df["date"], jupyter_daily_df["jupyter_users"])
526
+ )
527
+ if not cb_daily_df.empty:
528
+ cb_daily_map = dict(zip(cb_daily_df["date"], cb_daily_df["codebuff_users"]))
529
+
530
+ rows = []
531
+ for date in sorted(all_dates):
532
+ j_users = jupyter_daily_map.get(date, set())
533
+ cb_users = cb_daily_map.get(date, set())
534
+ jupyter_dau = len(j_users)
535
+ codebuff_dau = len(cb_users)
536
+ jupyter_only_dau = len(j_users - cb_users)
537
+ adoption = round(codebuff_dau / jupyter_dau, 4) if jupyter_dau > 0 else 0.0
538
+ rows.append(
539
+ {
540
+ "date": date,
541
+ "jupyter_dau": jupyter_dau,
542
+ "codebuff_dau": codebuff_dau,
543
+ "jupyter_only_dau": jupyter_only_dau,
544
+ "codebuff_adoption_rate": adoption,
545
+ }
546
+ )
547
+
548
+ daily_df = pd.DataFrame(rows)
549
+
550
+ # MAU
551
+ month_jupyter: dict[str, set] = {}
552
+ month_cb: dict[str, set] = {}
553
+
554
+ if not imp_df.empty:
555
+ session_starts = imp_df[imp_df["feature"] == "session_start"]
556
+ for _, row in session_starts.iterrows():
557
+ m = row["date"][:7]
558
+ month_jupyter.setdefault(m, set()).add(row["user_id"])
559
+
560
+ if not click_df.empty:
561
+ cb_clicks = click_df[click_df["feature"].isin(CODEBUFF_FEATURES)]
562
+ for _, row in cb_clicks.iterrows():
563
+ m = row["date"][:7]
564
+ month_cb.setdefault(m, set()).add(row["user_id"])
565
+
566
+ all_months = sorted(set(list(month_jupyter.keys()) + list(month_cb.keys())))
567
+ mau_map: dict[str, dict] = {}
568
+ for m in all_months:
569
+ j_set = month_jupyter.get(m, set())
570
+ cb_set = month_cb.get(m, set())
571
+ j_mau = len(j_set)
572
+ cb_mau = len(cb_set)
573
+ mau_rate = round(cb_mau / j_mau, 4) if j_mau > 0 else 0.0
574
+ mau_map[m] = {
575
+ "jupyter_mau": j_mau,
576
+ "codebuff_mau": cb_mau,
577
+ "jupyter_only_mau": len(j_set - cb_set),
578
+ "codebuff_mau_rate": mau_rate,
579
+ }
580
+
581
+ if not daily_df.empty:
582
+ daily_df["month"] = daily_df["date"].str[:7]
583
+ daily_df["jupyter_mau"] = daily_df["month"].map(
584
+ lambda m: mau_map.get(m, {}).get("jupyter_mau", 0)
585
+ )
586
+ daily_df["codebuff_mau"] = daily_df["month"].map(
587
+ lambda m: mau_map.get(m, {}).get("codebuff_mau", 0)
588
+ )
589
+ daily_df["jupyter_only_mau"] = daily_df["month"].map(
590
+ lambda m: mau_map.get(m, {}).get("jupyter_only_mau", 0)
591
+ )
592
+ daily_df["codebuff_mau_rate"] = daily_df["month"].map(
593
+ lambda m: mau_map.get(m, {}).get("codebuff_mau_rate", 0.0)
594
+ )
595
+ daily_df = daily_df.drop(columns=["month"])
596
+
597
+ return daily_df
598
+
599
+
600
+ def aggregate_codebuff_feature_mix(
601
+ clicks: list[dict],
602
+ date_from: Optional[str] = None,
603
+ date_to: Optional[str] = None,
604
+ ) -> pd.DataFrame:
605
+ """A7: Code Buff 기능별 softmax 비중 + 건수."""
606
+ empty_cols = ["period", "period_type", "feature", "count", "softmax_weight"]
607
+ if not clicks:
608
+ return pd.DataFrame(columns=empty_cols)
609
+
610
+ df = pd.DataFrame(clicks)
611
+ df["date"] = df["datetime"].str[:10]
612
+ df = _filter_by_date(df, "date", date_from, date_to)
613
+ df = df[df["feature"].isin(CODEBUFF_FEATURES)]
614
+
615
+ if df.empty:
616
+ return pd.DataFrame(columns=empty_cols)
617
+
618
+ df["month"] = df["date"].str[:7]
619
+
620
+ rows = []
621
+
622
+ # Daily
623
+ daily_counts = df.groupby(["date", "feature"]).size().reset_index(name="count")
624
+ for date, grp in daily_counts.groupby("date"):
625
+ counts = grp["count"].values.astype(float)
626
+ shifted = counts - counts.max()
627
+ exp_vals = np.exp(shifted)
628
+ softmax = exp_vals / exp_vals.sum()
629
+ for i, (_, r) in enumerate(grp.iterrows()):
630
+ rows.append(
631
+ {
632
+ "period": date,
633
+ "period_type": "daily",
634
+ "feature": r["feature"],
635
+ "count": int(r["count"]),
636
+ "softmax_weight": round(float(softmax[i]), 6),
637
+ }
638
+ )
639
+
640
+ # Monthly
641
+ monthly_counts = df.groupby(["month", "feature"]).size().reset_index(name="count")
642
+ for month, grp in monthly_counts.groupby("month"):
643
+ counts = grp["count"].values.astype(float)
644
+ shifted = counts - counts.max()
645
+ exp_vals = np.exp(shifted)
646
+ softmax = exp_vals / exp_vals.sum()
647
+ for i, (_, r) in enumerate(grp.iterrows()):
648
+ rows.append(
649
+ {
650
+ "period": month,
651
+ "period_type": "monthly",
652
+ "feature": r["feature"],
653
+ "count": int(r["count"]),
654
+ "softmax_weight": round(float(softmax[i]), 6),
655
+ }
656
+ )
657
+
658
+ return pd.DataFrame(rows, columns=empty_cols)
659
+
660
+
661
+ def aggregate_codebuff_utilization(
662
+ impressions: list[dict],
663
+ clicks: list[dict],
664
+ date_from: Optional[str] = None,
665
+ date_to: Optional[str] = None,
666
+ ) -> pd.DataFrame:
667
+ """A8: Code Buff 활용률 (세션 단위 binary)."""
668
+ empty_cols = [
669
+ "period",
670
+ "period_type",
671
+ "code_shown_sessions",
672
+ "code_action_sessions",
673
+ "utilization_rate",
674
+ ]
675
+
676
+ imp_df = pd.DataFrame(impressions)
677
+ click_df = pd.DataFrame(clicks)
678
+
679
+ if imp_df.empty and click_df.empty:
680
+ return pd.DataFrame(columns=empty_cols)
681
+
682
+ # Impression: code_block_shown → (session_id, date)
683
+ shown_pairs: set[tuple[str, str]] = set()
684
+ if not imp_df.empty:
685
+ imp_df["date"] = imp_df["datetime"].str[:10]
686
+ imp_df = _filter_by_date(imp_df, "date", date_from, date_to)
687
+ shown = imp_df[imp_df["feature"] == "code_block_shown"]
688
+ if not shown.empty:
689
+ for _, r in shown.iterrows():
690
+ shown_pairs.add((r["session_id"], r["date"]))
691
+
692
+ # Click: CODEBUFF_FEATURES → (session_id, date)
693
+ action_pairs: set[tuple[str, str]] = set()
694
+ if not click_df.empty:
695
+ click_df["date"] = click_df["datetime"].str[:10]
696
+ click_df = _filter_by_date(click_df, "date", date_from, date_to)
697
+ cb_clicks = click_df[click_df["feature"].isin(CODEBUFF_FEATURES)]
698
+ if not cb_clicks.empty:
699
+ for _, r in cb_clicks.iterrows():
700
+ action_pairs.add((r["session_id"], r["date"]))
701
+
702
+ if not shown_pairs and not action_pairs:
703
+ return pd.DataFrame(columns=empty_cols)
704
+
705
+ rows = []
706
+
707
+ # Daily
708
+ all_dates = sorted({d for _, d in shown_pairs} | {d for _, d in action_pairs})
709
+ for date in all_dates:
710
+ shown_sessions = {sid for sid, d in shown_pairs if d == date}
711
+ action_sessions = {sid for sid, d in action_pairs if d == date}
712
+ n_shown = len(shown_sessions)
713
+ n_action = len(action_sessions)
714
+ rate = round(n_action / n_shown, 4) if n_shown > 0 else 0.0
715
+ rows.append(
716
+ {
717
+ "period": date,
718
+ "period_type": "daily",
719
+ "code_shown_sessions": n_shown,
720
+ "code_action_sessions": n_action,
721
+ "utilization_rate": rate,
722
+ }
723
+ )
724
+
725
+ # Monthly
726
+ shown_monthly: dict[str, set] = {}
727
+ for sid, d in shown_pairs:
728
+ m = d[:7]
729
+ shown_monthly.setdefault(m, set()).add(sid)
730
+ action_monthly: dict[str, set] = {}
731
+ for sid, d in action_pairs:
732
+ m = d[:7]
733
+ action_monthly.setdefault(m, set()).add(sid)
734
+
735
+ all_months = sorted(set(shown_monthly.keys()) | set(action_monthly.keys()))
736
+ for month in all_months:
737
+ n_shown = len(shown_monthly.get(month, set()))
738
+ n_action = len(action_monthly.get(month, set()))
739
+ rate = round(n_action / n_shown, 4) if n_shown > 0 else 0.0
740
+ rows.append(
741
+ {
742
+ "period": month,
743
+ "period_type": "monthly",
744
+ "code_shown_sessions": n_shown,
745
+ "code_action_sessions": n_action,
746
+ "utilization_rate": rate,
747
+ }
748
+ )
749
+
750
+ return pd.DataFrame(rows, columns=empty_cols)
751
+
752
+
753
+ def main() -> None:
754
+ parser = argparse.ArgumentParser(
755
+ description="Analytics JSONL → CSV 집계 리포트 생성"
756
+ )
757
+ parser.add_argument(
758
+ "--input",
759
+ required=True,
760
+ help="JSONL 로그 디렉토리 (예: extensions/logs/)",
761
+ )
762
+ parser.add_argument(
763
+ "--output",
764
+ required=True,
765
+ help="CSV 출력 디렉토리 (예: /tmp/reports/)",
766
+ )
767
+ parser.add_argument("--date-from", default=None, help="시작 날짜 (YYYY-MM-DD)")
768
+ parser.add_argument("--date-to", default=None, help="종료 날짜 (YYYY-MM-DD)")
769
+ args = parser.parse_args()
770
+
771
+ input_dir = Path(args.input)
772
+ output_dir = Path(args.output)
773
+
774
+ if not input_dir.is_dir():
775
+ print(f"Error: input directory not found: {input_dir}", file=sys.stderr)
776
+ sys.exit(1)
777
+
778
+ output_dir.mkdir(parents=True, exist_ok=True)
779
+
780
+ # Read JSONL data
781
+ impressions = _read_jsonl_files(input_dir, "analytics_impression_")
782
+ clicks = _read_jsonl_files(input_dir, "analytics_click_")
783
+ errors = _read_jsonl_files(input_dir, "analytics_error_")
784
+ structured_logs = _read_jsonl_files(input_dir, "agent_structured")
785
+
786
+ print(
787
+ f"Read: {len(impressions)} impressions, {len(clicks)} clicks, "
788
+ f"{len(errors)} errors, {len(structured_logs)} structured logs"
789
+ )
790
+
791
+ # A1: Active Users
792
+ a1 = aggregate_active_users(impressions, clicks, args.date_from, args.date_to)
793
+ a1.to_csv(output_dir / "A1_active_users.csv", index=False)
794
+ print(f"A1_active_users.csv: {len(a1)} rows")
795
+
796
+ # A2: Agent Frequency
797
+ a2 = aggregate_agent_frequency(clicks, args.date_from, args.date_to)
798
+ a2.to_csv(output_dir / "A2_agent_frequency.csv", index=False)
799
+ print(f"A2_agent_frequency.csv: {len(a2)} rows")
800
+
801
+ # A3: Feature Preference
802
+ a3 = aggregate_feature_preference(clicks, args.date_from, args.date_to)
803
+ a3.to_csv(output_dir / "A3_feature_preference.csv", index=False)
804
+ print(f"A3_feature_preference.csv: {len(a3)} rows")
805
+
806
+ # A4: Errors
807
+ a4 = aggregate_errors(errors, structured_logs, args.date_from, args.date_to)
808
+ a4.to_csv(output_dir / "A4_errors.csv", index=False)
809
+ print(f"A4_errors.csv: {len(a4)} rows")
810
+
811
+ # A5: Cell Attribution
812
+ a5 = aggregate_cell_attribution(impressions, clicks, args.date_from, args.date_to)
813
+ a5.to_csv(output_dir / "A5_cell_attribution.csv", index=False)
814
+ print(f"A5_cell_attribution.csv : {len(a5)} rows")
815
+
816
+ # A6: Code Buff Users
817
+ a6 = aggregate_codebuff_users(impressions, clicks, args.date_from, args.date_to)
818
+ a6.to_csv(output_dir / "A6_codebuff_users.csv", index=False)
819
+ print(f"A6_codebuff_users.csv : {len(a6)} rows")
820
+
821
+ # A7: Code Buff Feature Mix
822
+ a7 = aggregate_codebuff_feature_mix(clicks, args.date_from, args.date_to)
823
+ a7.to_csv(output_dir / "A7_codebuff_feature_mix.csv", index=False)
824
+ print(f"A7_codebuff_feature_mix.csv: {len(a7)} rows")
825
+
826
+ # A8: Code Buff Utilization
827
+ a8 = aggregate_codebuff_utilization(
828
+ impressions, clicks, args.date_from, args.date_to
829
+ )
830
+ a8.to_csv(output_dir / "A8_codebuff_utilization.csv", index=False)
831
+ print(f"A8_codebuff_utilization.csv: {len(a8)} rows")
832
+
833
+ print(f"\nDone. Reports saved to {output_dir}/")
834
+
835
+
836
+ if __name__ == "__main__":
837
+ main()