Skip to content

Commit 691ecd5

Browse files
committed
feat(plugin): add user prompt pattern analysis for personalized shortcuts
Implement PromptPatternAnalyzer module that classifies user prompts into categories (test, debug, refactor, build, deploy, etc.) and suggests matching shortcuts/skills based on frequency. Privacy-first: stores only category counts locally, never raw prompts. Opt-in via config. Closes #1003
1 parent 415606f commit 691ecd5

2 files changed

Lines changed: 679 additions & 0 deletions

File tree

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
"""User prompt pattern analysis for personalized shortcut suggestions.
2+
3+
Analyzes prompt patterns locally to suggest matching shortcuts/skills.
4+
Privacy-first: stores only category counts, never raw prompts.
5+
Opt-in via ``promptPatternAnalysis.enabled`` in codingbuddy.config.json.
6+
"""
7+
8+
import json
9+
import logging
10+
import os
11+
from typing import Any, Dict, List, Optional
12+
13+
logger = logging.getLogger(__name__)
14+
15+
PATTERNS_FILE = "patterns.json"
16+
17+
# Keyword-to-category mapping for prompt classification.
18+
# Order matters: first match wins.
19+
CATEGORY_KEYWORDS: Dict[str, List[str]] = {
20+
"test": [
21+
"test",
22+
"테스트",
23+
"pytest",
24+
"jest",
25+
"spec",
26+
"coverage",
27+
"assertion",
28+
],
29+
"debug": [
30+
"fix",
31+
"bug",
32+
"error",
33+
"debug",
34+
"디버그",
35+
"수정",
36+
"오류",
37+
"issue",
38+
"broken",
39+
"crash",
40+
"fail",
41+
],
42+
"refactor": [
43+
"refactor",
44+
"리팩토링",
45+
"리팩터",
46+
"clean",
47+
"restructure",
48+
"simplify",
49+
"extract",
50+
],
51+
"build": [
52+
"build",
53+
"compile",
54+
"빌드",
55+
"bundle",
56+
"webpack",
57+
"vite",
58+
"esbuild",
59+
],
60+
"deploy": [
61+
"deploy",
62+
"배포",
63+
"release",
64+
"ship",
65+
"publish",
66+
"production",
67+
],
68+
"review": [
69+
"review",
70+
"리뷰",
71+
"pr",
72+
"pull request",
73+
"코드 리뷰",
74+
"check",
75+
],
76+
"docs": [
77+
"doc",
78+
"문서",
79+
"readme",
80+
"comment",
81+
"documentation",
82+
"jsdoc",
83+
],
84+
"create": [
85+
"create",
86+
"생성",
87+
"new",
88+
"add",
89+
"만들",
90+
"component",
91+
"feature",
92+
"implement",
93+
],
94+
"git": [
95+
"commit",
96+
"push",
97+
"merge",
98+
"branch",
99+
"rebase",
100+
"cherry-pick",
101+
"stash",
102+
],
103+
"security": [
104+
"security",
105+
"보안",
106+
"vulnerab",
107+
"auth",
108+
"permission",
109+
"xss",
110+
"injection",
111+
"csrf",
112+
],
113+
"performance": [
114+
"performance",
115+
"성능",
116+
"optimi",
117+
"slow",
118+
"fast",
119+
"latency",
120+
"memory",
121+
"cache",
122+
],
123+
}
124+
125+
# Shortcut/skill suggestions per category.
126+
_SHORTCUT_MAP: Dict[str, Dict[str, str]] = {
127+
"test": {
128+
"shortcut": "/tdd",
129+
"skill": "superpowers:test-driven-development",
130+
"description": "Run TDD workflow for test-first development",
131+
},
132+
"debug": {
133+
"shortcut": "/debug",
134+
"skill": "superpowers:systematic-debugging",
135+
"description": "Systematic debugging with root cause analysis",
136+
},
137+
"refactor": {
138+
"shortcut": "/simplify",
139+
"skill": "simplify",
140+
"description": "Review and simplify code for quality",
141+
},
142+
"build": {
143+
"shortcut": "/build-fix",
144+
"skill": "oh-my-claudecode:build-fix",
145+
"description": "Fix build and compilation errors",
146+
},
147+
"deploy": {
148+
"shortcut": "/ship",
149+
"skill": "ship",
150+
"description": "Run CI checks and ship changes",
151+
},
152+
"review": {
153+
"shortcut": "/code-review",
154+
"skill": "oh-my-claudecode:code-review",
155+
"description": "Run comprehensive code review",
156+
},
157+
"docs": {
158+
"shortcut": "/plan",
159+
"skill": "oh-my-claudecode:plan",
160+
"description": "Plan documentation structure",
161+
},
162+
"create": {
163+
"shortcut": "/brainstorm",
164+
"skill": "superpowers:brainstorming",
165+
"description": "Brainstorm before creating new features",
166+
},
167+
"git": {
168+
"shortcut": "/git-master",
169+
"skill": "oh-my-claudecode:git-master",
170+
"description": "Git expert for commits and history management",
171+
},
172+
"security": {
173+
"shortcut": "/security-review",
174+
"skill": "oh-my-claudecode:security-review",
175+
"description": "Run comprehensive security review",
176+
},
177+
"performance": {
178+
"shortcut": "/analyze",
179+
"skill": "oh-my-claudecode:analyze",
180+
"description": "Deep analysis and investigation",
181+
},
182+
}
183+
184+
185+
def classify_prompt(prompt: str) -> str:
186+
"""Classify a prompt into a category using keyword matching.
187+
188+
Privacy: only returns a category string, never stores the prompt.
189+
190+
Args:
191+
prompt: Raw user prompt text.
192+
193+
Returns:
194+
Category string (e.g. "test", "debug") or "other".
195+
"""
196+
if not prompt:
197+
return "other"
198+
lower = prompt.lower()
199+
for category, keywords in CATEGORY_KEYWORDS.items():
200+
for kw in keywords:
201+
if kw in lower:
202+
return category
203+
return "other"
204+
205+
206+
class PromptPatternAnalyzer:
207+
"""Analyzes user prompt patterns and suggests personalized shortcuts.
208+
209+
Stores only category frequency counts locally. Never stores raw prompts.
210+
Must be explicitly enabled via config (opt-in).
211+
"""
212+
213+
def __init__(self, data_dir: str, enabled: bool = False) -> None:
214+
self._data_dir = data_dir
215+
self._enabled = enabled
216+
self._categories: Dict[str, int] = {}
217+
self._total: int = 0
218+
219+
if self._enabled:
220+
os.makedirs(self._data_dir, exist_ok=True)
221+
self._load()
222+
223+
@property
224+
def enabled(self) -> bool:
225+
return self._enabled
226+
227+
@classmethod
228+
def from_config(
229+
cls,
230+
config: Dict[str, Any],
231+
data_dir: str,
232+
) -> "PromptPatternAnalyzer":
233+
"""Create an analyzer from codingbuddy config.
234+
235+
Args:
236+
config: Parsed codingbuddy.config.json dict.
237+
data_dir: Default data directory path.
238+
239+
Returns:
240+
Configured PromptPatternAnalyzer instance.
241+
"""
242+
ppa_config = config.get("promptPatternAnalysis", {})
243+
enabled = ppa_config.get("enabled", False)
244+
custom_dir = ppa_config.get("dataDir", None)
245+
return cls(
246+
data_dir=custom_dir or data_dir,
247+
enabled=bool(enabled),
248+
)
249+
250+
def record_prompt(self, prompt: str) -> None:
251+
"""Record a prompt by classifying and incrementing the category count.
252+
253+
Args:
254+
prompt: Raw user prompt text (not stored).
255+
"""
256+
if not self._enabled:
257+
return
258+
259+
category = classify_prompt(prompt)
260+
self._categories[category] = self._categories.get(category, 0) + 1
261+
self._total += 1
262+
self._save()
263+
264+
def analyze_patterns(self, top_n: int = 10) -> List[Dict[str, Any]]:
265+
"""Return top N most frequent prompt categories.
266+
267+
Args:
268+
top_n: Maximum number of patterns to return.
269+
270+
Returns:
271+
List of dicts with keys: category, count, percentage.
272+
Sorted by count descending.
273+
"""
274+
if not self._enabled or not self._categories:
275+
return []
276+
277+
sorted_cats = sorted(
278+
self._categories.items(), key=lambda x: x[1], reverse=True
279+
)[:top_n]
280+
281+
total = self._total if self._total > 0 else 1
282+
return [
283+
{
284+
"category": cat,
285+
"count": count,
286+
"percentage": round((count / total) * 100, 1),
287+
}
288+
for cat, count in sorted_cats
289+
]
290+
291+
def suggest_shortcuts(
292+
self, min_count: int = 3
293+
) -> List[Dict[str, Any]]:
294+
"""Suggest shortcuts/skills based on frequent patterns.
295+
296+
Args:
297+
min_count: Minimum category count to trigger a suggestion.
298+
299+
Returns:
300+
List of suggestion dicts with keys:
301+
category, count, shortcut, skill, description.
302+
"""
303+
if not self._enabled:
304+
return []
305+
306+
suggestions = []
307+
for cat, count in sorted(
308+
self._categories.items(), key=lambda x: x[1], reverse=True
309+
):
310+
if count < min_count:
311+
continue
312+
mapping = _SHORTCUT_MAP.get(cat)
313+
if mapping is None:
314+
continue
315+
suggestions.append(
316+
{
317+
"category": cat,
318+
"count": count,
319+
**mapping,
320+
}
321+
)
322+
return suggestions
323+
324+
def _load(self) -> None:
325+
"""Load pattern data from disk."""
326+
path = os.path.join(self._data_dir, PATTERNS_FILE)
327+
if not os.path.isfile(path):
328+
return
329+
try:
330+
with open(path, "r", encoding="utf-8") as f:
331+
data = json.load(f)
332+
self._categories = data.get("categories", {})
333+
self._total = data.get("total", 0)
334+
except (json.JSONDecodeError, OSError) as e:
335+
logger.warning("Failed to load pattern data: %s", e)
336+
337+
def _save(self) -> None:
338+
"""Persist pattern data to disk."""
339+
path = os.path.join(self._data_dir, PATTERNS_FILE)
340+
try:
341+
with open(path, "w", encoding="utf-8") as f:
342+
json.dump(
343+
{"categories": self._categories, "total": self._total},
344+
f,
345+
indent=2,
346+
)
347+
except OSError as e:
348+
logger.warning("Failed to save pattern data: %s", e)

0 commit comments

Comments
 (0)