refactor: simplify workflow.py to use AI normalization with minimal fallback

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 <noreply@anthropic.com>
This commit is contained in:
rob 2025-11-02 18:48:12 -04:00
parent f9297fc456
commit da726cb5bf
2 changed files with 114 additions and 78 deletions

View File

@ -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(
{
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(
{
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()

View File

@ -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: {
: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: {
: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: {
: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:
<!-- SUMMARY:DECISIONS START -->
...
<!-- SUMMARY:DECISIONS END -->;
:Update marker blocks in .sum.md;
note right
<!-- SUMMARY:DECISIONS START -->
...
<!-- SUMMARY:DECISIONS END -->
end note
:Stage updated .sum.md file;