跳到主要内容

项目方向与思路复盘

收藏0

回看当前讨论的目标、假设、遗漏与思维偏差,给出有依据的继续、调整或暂停建议。

让 AI 安装

github最新版本 0.1.1更新于 2026-10-21下载 2 次2 个版本

scripts/conversation_depth_analyzer.py

7.2 KB
1#!/usr/bin/env python3
2"""conversation_depth_analyzer.py — Detect implicit reflect-trigger signals.
3
4Stdlib-only. Analyzes a conversation transcript and reports:
5
6 - turn count (User: + Assistant: pairs)
7 - detail-mode turns (turns dominated by implementation specifics)
8 - frustration markers (signs of user stuck-ness)
9 - dead-end signals (pivots within short span)
10 - implicit-trigger verdict: whether the conversation matches reflect-skill auto-invocation criteria
11
12The skill OFFERS reflection when implicit signals fire; it does NOT auto-invoke.
13
14NO LLM CALLS. Pure regex + counting.
15
16Usage:
17 python conversation_depth_analyzer.py --conversation /tmp/transcript.txt
18 python conversation_depth_analyzer.py --conversation /tmp/transcript.txt --output json
19 python conversation_depth_analyzer.py --sample
20"""
21
22import argparse
23import json
24import re
25import sys
26from pathlib import Path
27from typing import Any, Dict, List
28
29
30TURN_RE = re.compile(r"^\s*(User|Assistant):\s*", re.MULTILINE)
31
32DETAIL_MARKERS = [
33 r"\bimplementation\b",
34 r"\bcode\b",
35 r"\bfunction\b",
36 r"\bclass\b",
37 r"\bvariable\b",
38 r"\b(syntax|method|parameter|argument)\b",
39 r"\bdebug\b",
40 r"\berror\b",
41 r"`[^`]+`", # backtick-quoted code references
42]
43
44FRUSTRATION_MARKERS = [
45 r"\b(ugh|argh|frustrated|stuck)\b",
46 r"\bnot working\b",
47 r"\bdoesn'?t work\b",
48 r"\bgoing in circles\b",
49 r"\bstill (broken|failing|wrong)\b",
50 r"\bwhy isn'?t\b",
51 r"\bthis is (weird|strange|odd|confusing)\b",
52]
53
54DEAD_END_MARKERS = [
55 r"\bnope\b",
56 r"\bthat didn'?t work\b",
57 r"\b(let's|let me) try (something|a) (else|different)\b",
58 r"\bback to\b",
59 r"\bnever mind\b",
60 r"\bscratch that\b",
61]
62
63
64def count_turns(text: str) -> Dict[str, int]:
65 matches = TURN_RE.findall(text)
66 user_turns = sum(1 for m in matches if m == "User")
67 assistant_turns = sum(1 for m in matches if m == "Assistant")
68 return {
69 "total_turns": len(matches),
70 "user_turns": user_turns,
71 "assistant_turns": assistant_turns,
72 }
73
74
75def count_pattern_hits(text: str, patterns: List[str]) -> int:
76 return sum(len(re.findall(p, text, re.IGNORECASE)) for p in patterns)
77
78
79def detect_detail_mode_run(text: str) -> int:
80 """Count consecutive turns that have detail markers but no strategic check-in."""
81 blocks = TURN_RE.split(text)
82 consecutive_detail = 0
83 max_consecutive = 0
84 for block in blocks:
85 if not block.strip():
86 continue
87 if any(re.search(p, block, re.IGNORECASE) for p in DETAIL_MARKERS):
88 consecutive_detail += 1
89 max_consecutive = max(max_consecutive, consecutive_detail)
90 else:
91 consecutive_detail = 0
92 return max_consecutive
93
94
95def analyze(text: str) -> Dict[str, Any]:
96 turns = count_turns(text)
97 detail_mode_max = detect_detail_mode_run(text)
98 frustration_count = count_pattern_hits(text, FRUSTRATION_MARKERS)
99 dead_end_count = count_pattern_hits(text, DEAD_END_MARKERS)
100
101 signals: List[str] = []
102 if detail_mode_max >= 5:
103 signals.append(f"Detail-mode run of {detail_mode_max} consecutive turns")
104 if turns["total_turns"] >= 20:
105 signals.append(f"Long conversation: {turns['total_turns']} turns total")
106 if frustration_count >= 2:
107 signals.append(f"Frustration markers: {frustration_count} occurrences")
108 if dead_end_count >= 2:
109 signals.append(f"Dead-end / pivot markers: {dead_end_count} occurrences")
110
111 should_offer_reflect = len(signals) >= 2 or detail_mode_max >= 10
112
113 return {
114 "turns": turns,
115 "max_consecutive_detail_turns": detail_mode_max,
116 "frustration_hits": frustration_count,
117 "dead_end_hits": dead_end_count,
118 "trigger_signals": signals,
119 "implicit_trigger_verdict": (
120 "OFFER_REFLECT" if should_offer_reflect else "NO_TRIGGER"
121 ),
122 "note": (
123 "Skill should OFFER reflection (ask user if they want to step back), not auto-invoke"
124 if should_offer_reflect
125 else "No implicit trigger signal — only explicit phrases should invoke"
126 ),
127 }
128
129
130SAMPLE_CONVERSATION = """User: Help me implement a sorting function.
131Assistant: Sure. What language and what type of data?
132User: Python, sorting a list of dicts by 'name' key.
133Assistant: Here's the basic syntax: `sorted(items, key=lambda x: x['name'])`
134User: That doesn't work — getting a TypeError.
135Assistant: Can you share the exact error? Maybe some items don't have 'name'.
136User: Ugh, still broken. Same error.
137Assistant: Try adding a default: `key=lambda x: x.get('name', '')`.
138User: Nope, that didn't work either. Different error now: KeyError on 'name'.
139Assistant: That's strange. Let me look at your input data.
140User: Frustrated. Let's try something else.
141Assistant: OK let me reconsider. Are you sure the data is a list of dicts?
142User: I think so. It's coming from an API.
143Assistant: Let me check the API response shape. Add `print(type(items[0]))` first.
144User: It's a string, not a dict.
145Assistant: That's the issue. The API returns JSON strings; you need to parse them first.
146User: This is weird, why isn't this working from the start?
147Assistant: Let's go back to the data shape question first.
148User: Going in circles. I'm stuck.
149"""
150
151
152def render_human(result: Dict[str, Any]) -> str:
153 out: List[str] = []
154 t = result["turns"]
155 out.append(f"Conversation depth analysis:")
156 out.append(f" Total turns: {t['total_turns']} (user: {t['user_turns']}, assistant: {t['assistant_turns']})")
157 out.append(f" Max consecutive detail turns: {result['max_consecutive_detail_turns']}")
158 out.append(f" Frustration markers: {result['frustration_hits']}")
159 out.append(f" Dead-end / pivot markers: {result['dead_end_hits']}")
160 out.append("")
161 out.append(f"Implicit-trigger verdict: {result['implicit_trigger_verdict']}")
162 if result["trigger_signals"]:
163 out.append("Signals detected:")
164 for s in result["trigger_signals"]:
165 out.append(f" - {s}")
166 out.append("")
167 out.append(result["note"])
168 return "\n".join(out)
169
170
171def main(argv: List[str]) -> int:
172 parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
173 parser.add_argument("--conversation", help="Path to conversation transcript text file")
174 parser.add_argument("--sample", action="store_true", help="Run on embedded sample (stuck-debugging scenario)")
175 parser.add_argument("--output", choices=["human", "json"], default="human")
176 args = parser.parse_args(argv)
177
178 if args.sample:
179 text = SAMPLE_CONVERSATION
180 elif args.conversation:
181 p = Path(args.conversation)
182 if not p.exists():
183 print(f"error: {args.conversation} not found", file=sys.stderr)
184 return 2
185 text = p.read_text(encoding="utf-8")
186 else:
187 parser.print_help()
188 return 0
189
190 result = analyze(text)
191 if args.output == "json":
192 print(json.dumps(result, indent=2))
193 else:
194 print(render_human(result))
195 return 0
196
197
198if __name__ == "__main__":
199 sys.exit(main(sys.argv[1:]))

文件内容是平台检查时读出的文本,只供查看。平台不会运行包里的任何脚本或程序。