From da726cb5bf3b4fc60fa3b1081ea1f193bb916576 Mon Sep 17 00:00:00 2001 From: rob Date: Sun, 2 Nov 2025 18:48:12 -0400 Subject: [PATCH] refactor: simplify workflow.py to use AI normalization with minimal fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified marker extraction architecture: - AI normalization (agents.py) handles natural conversation - Simple line-start matching for explicit markers as fallback - Removed complex regex patterns (DECISION_PATTERN, QUESTION_PATTERN, ACTION_PATTERN) - Participants can now write naturally without strict formatting rules This implements the original design intent: fast AI model normalizes conversational text into structured format, then simple parsing logic extracts it. Benefits: - More flexible for participants (no strict formatting required) - Simpler code (startswith() instead of regex) - Clear separation: AI for understanding, code for mechanical parsing - Cost-effective (fast models for simple extraction task) Updated workflow-marker-extraction.puml to show patterns in notes instead of inline text (fixes PlantUML syntax error). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- automation/workflow.py | 112 ++++++++++++++------------- docs/workflow-marker-extraction.puml | 80 +++++++++++++------ 2 files changed, 114 insertions(+), 78 deletions(-) diff --git a/automation/workflow.py b/automation/workflow.py index b186a3a..9a3a8b2 100644 --- a/automation/workflow.py +++ b/automation/workflow.py @@ -33,25 +33,20 @@ DISCUSSION_SUFFIXES = ( SUMMARY_SUFFIX = ".sum.md" MENTION_PATTERN = re.compile(r"@(\w+|all)") -# Patterns to extract structured markers (support both plain and markdown bold) -# Matches: DECISION: text, **DECISION**: text, decision: text -DECISION_PATTERN = re.compile(r'(?:\*\*)?DECISION(?:\*\*)?\s*:\s*(.+?)(?=\s*(?:\*\*QUESTION|\*\*ACTION|\*\*SUGGESTION|VOTE:)|$)', re.IGNORECASE | re.DOTALL) -QUESTION_PATTERN = re.compile(r'(?:\*\*)?(?:QUESTION|Q)(?:\*\*)?\s*:\s*(.+?)(?=\s*(?:\*\*DECISION|\*\*ACTION|\*\*SUGGESTION|VOTE:)|$)', re.IGNORECASE | re.DOTALL) -ACTION_PATTERN = re.compile(r'(?:\*\*)?(?:ACTION|TODO)(?:\*\*)?\s*:\s*(.+?)(?=\s*(?:\*\*DECISION|\*\*QUESTION|\*\*SUGGESTION|VOTE:)|$)', re.IGNORECASE | re.DOTALL) - - def extract_structured_basic(text: str) -> dict[str, list]: """ - Derive structured discussion signals using regex pattern matching. + Minimal fallback extraction for strictly-formatted line-start markers. - Recognises markers anywhere in text (not just at line start): - - DECISION: / **DECISION**: - Architectural/technical decisions - - QUESTION: / **QUESTION**: / Q: - Open questions needing answers - - ACTION: / **ACTION**: / TODO: - Action items with optional @assignee + Only matches explicit markers at the start of text (case-insensitive): + - DECISION: - Architectural/technical decisions + - QUESTION: / Q: - Open questions needing answers + - ACTION: / TODO: - Action items with optional @assignee + - ASSIGNED: / DONE: - Legacy status markers - @mentions - References to participants - Also supports legacy line-start markers: ASSIGNED:, DONE: - Questions ending with '?' are auto-detected. + Natural conversation with embedded markers (e.g., "I think **DECISION**: we should...") + is handled by AI normalization in agents.py. This function serves as a simple + fallback when AI is unavailable or fails. """ questions: list[dict[str, str]] = [] action_items: list[dict[str, str]] = [] @@ -87,58 +82,67 @@ def extract_structured_basic(text: str) -> dict[str, list]: "summary": _truncate_summary(analysis), } - # Extract decisions using regex (finds markers anywhere in line) - for match in DECISION_PATTERN.finditer(analysis): - decision_text = match.group(1).strip() + # Simple line-start matching for explicit markers only + # Natural conversation is handled by AI normalization in agents.py + lowered = analysis.lower() + + if lowered.startswith("decision:"): + decision_text = analysis[9:].strip() # len("decision:") = 9 if decision_text: - decisions.append( - { - "participant": participant_name, - "decision": decision_text, - "rationale": "", - "supporters": [], - } - ) + decisions.append({ + "participant": participant_name, + "decision": decision_text, + "rationale": "", + "supporters": [], + }) - # Extract questions using regex (finds markers anywhere in line) - for match in QUESTION_PATTERN.finditer(analysis): - question_text = match.group(1).strip() + elif lowered.startswith("question:"): + question_text = analysis[9:].strip() if question_text: - questions.append( - {"participant": participant_name, "question": question_text, "status": "OPEN"} - ) + questions.append({ + "participant": participant_name, + "question": question_text, + "status": "OPEN", + }) - # Also catch questions that end with '?' and don't have explicit marker - if '?' in analysis and not QUESTION_PATTERN.search(analysis): - # Simple heuristic: if line ends with ?, treat as question - if analysis.rstrip().endswith('?'): - question_text = analysis.rstrip('?').strip() - # Avoid duplicate if already extracted - if question_text and not any(q['question'] == question_text for q in questions): - questions.append( - {"participant": participant_name, "question": question_text, "status": "OPEN"} - ) + elif lowered.startswith("q:"): + question_text = analysis[2:].strip() + if question_text: + questions.append({ + "participant": participant_name, + "question": question_text, + "status": "OPEN", + }) - # Extract action items using regex (finds markers anywhere in line) - for match in ACTION_PATTERN.finditer(analysis): - action_text = match.group(1).strip() + elif lowered.startswith("action:"): + action_text = analysis[7:].strip() if action_text: - # Extract assignee from @mention in the line assignee = None mention_match = MENTION_PATTERN.search(action_text) if mention_match: assignee = mention_match.group(1) - action_items.append( - { - "participant": participant_name, - "action": action_text, - "status": "TODO", - "assignee": assignee, - } - ) + action_items.append({ + "participant": participant_name, + "action": action_text, + "status": "TODO", + "assignee": assignee, + }) + + elif lowered.startswith("todo:"): + action_text = analysis[5:].strip() + if action_text: + assignee = None + mention_match = MENTION_PATTERN.search(action_text) + if mention_match: + assignee = mention_match.group(1) + action_items.append({ + "participant": participant_name, + "action": action_text, + "status": "TODO", + "assignee": assignee, + }) # Legacy support for plain text markers at line start - lowered = analysis.lower() if lowered.startswith("assigned:"): _, _, action_text = analysis.partition(":") action_text = action_text.strip() diff --git a/docs/workflow-marker-extraction.puml b/docs/workflow-marker-extraction.puml index c1e0212..7ecfad8 100644 --- a/docs/workflow-marker-extraction.puml +++ b/docs/workflow-marker-extraction.puml @@ -36,36 +36,66 @@ partition "Parse Comments" { :Apply regex patterns\nto comment text; if (**DECISION**: found?) then (yes) - :Pattern: (?:\\*\\*)?DECISION(?:\\*\\*)?\n\\s*:\\s*(.+?)(?=\\*\\*|VOTE:|$); :Extract decision text; - :Store: { - participant: "Rob", - decision: "text...", - rationale: "", - supporters: [] - }; + :Store decision record; + note right + **Pattern:** + (?:\\*\\*)?DECISION(?:\\*\\*)? + \\s*:\\s*(.+?) + (?=\\s*(?:\\*\\*QUESTION|\\*\\*ACTION|VOTE:)|$) + + **Captures:** Decision text until next marker + + **Example:** + { + participant: "Rob", + decision: "text...", + rationale: "", + supporters: [] + } + end note endif if (**QUESTION**: found?) then (yes) - :Pattern: (?:\\*\\*)?(?:QUESTION|Q)(?:\\*\\*)?\n\\s*:\\s*(.+?)(?=\\*\\*|VOTE:|$); :Extract question text; - :Store: { - participant: "Rob", - question: "text...", - status: "OPEN" - }; + :Store question record; + note right + **Pattern:** + (?:\\*\\*)?(?:QUESTION|Q)(?:\\*\\*)? + \\s*:\\s*(.+?) + (?=\\s*(?:\\*\\*DECISION|\\*\\*ACTION|VOTE:)|$) + + **Captures:** Question text until next marker + + **Example:** + { + participant: "Rob", + question: "text...", + status: "OPEN" + } + end note endif if (**ACTION**: found?) then (yes) - :Pattern: (?:\\*\\*)?(?:ACTION|TODO)(?:\\*\\*)?\n\\s*:\\s*(.+?)(?=\\*\\*|VOTE:|$); :Extract action text; :Search for @mention in text; - :Store: { - participant: "Rob", - action: "text...", - assignee: "Sarah", - status: "TODO" - }; + :Store action record; + note right + **Pattern:** + (?:\\*\\*)?(?:ACTION|TODO)(?:\\*\\*)? + \\s*:\\s*(.+?) + (?=\\s*(?:\\*\\*DECISION|\\*\\*QUESTION|VOTE:)|$) + + **Captures:** Action text + assignee from @mention + + **Example:** + { + participant: "Rob", + action: "text...", + assignee: "Sarah", + status: "TODO" + } + end note endif if (Line ends with "?") then (yes) @@ -126,10 +156,12 @@ partition "Generate Summary Sections" { - Summarize key events; } -:Update marker blocks in .sum.md: - -... -; +:Update marker blocks in .sum.md; +note right + + ... + +end note :Stage updated .sum.md file;