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:
parent
f9297fc456
commit
da726cb5bf
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue