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"
|
SUMMARY_SUFFIX = ".sum.md"
|
||||||
MENTION_PATTERN = re.compile(r"@(\w+|all)")
|
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]:
|
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):
|
Only matches explicit markers at the start of text (case-insensitive):
|
||||||
- DECISION: / **DECISION**: - Architectural/technical decisions
|
- DECISION: - Architectural/technical decisions
|
||||||
- QUESTION: / **QUESTION**: / Q: - Open questions needing answers
|
- QUESTION: / Q: - Open questions needing answers
|
||||||
- ACTION: / **ACTION**: / TODO: - Action items with optional @assignee
|
- ACTION: / TODO: - Action items with optional @assignee
|
||||||
|
- ASSIGNED: / DONE: - Legacy status markers
|
||||||
- @mentions - References to participants
|
- @mentions - References to participants
|
||||||
|
|
||||||
Also supports legacy line-start markers: ASSIGNED:, DONE:
|
Natural conversation with embedded markers (e.g., "I think **DECISION**: we should...")
|
||||||
Questions ending with '?' are auto-detected.
|
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]] = []
|
questions: list[dict[str, str]] = []
|
||||||
action_items: 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),
|
"summary": _truncate_summary(analysis),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract decisions using regex (finds markers anywhere in line)
|
# Simple line-start matching for explicit markers only
|
||||||
for match in DECISION_PATTERN.finditer(analysis):
|
# Natural conversation is handled by AI normalization in agents.py
|
||||||
decision_text = match.group(1).strip()
|
lowered = analysis.lower()
|
||||||
|
|
||||||
|
if lowered.startswith("decision:"):
|
||||||
|
decision_text = analysis[9:].strip() # len("decision:") = 9
|
||||||
if decision_text:
|
if decision_text:
|
||||||
decisions.append(
|
decisions.append({
|
||||||
{
|
|
||||||
"participant": participant_name,
|
"participant": participant_name,
|
||||||
"decision": decision_text,
|
"decision": decision_text,
|
||||||
"rationale": "",
|
"rationale": "",
|
||||||
"supporters": [],
|
"supporters": [],
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
# Extract questions using regex (finds markers anywhere in line)
|
elif lowered.startswith("question:"):
|
||||||
for match in QUESTION_PATTERN.finditer(analysis):
|
question_text = analysis[9:].strip()
|
||||||
question_text = match.group(1).strip()
|
|
||||||
if question_text:
|
if question_text:
|
||||||
questions.append(
|
questions.append({
|
||||||
{"participant": participant_name, "question": question_text, "status": "OPEN"}
|
"participant": participant_name,
|
||||||
)
|
"question": question_text,
|
||||||
|
"status": "OPEN",
|
||||||
|
})
|
||||||
|
|
||||||
# Also catch questions that end with '?' and don't have explicit marker
|
elif lowered.startswith("q:"):
|
||||||
if '?' in analysis and not QUESTION_PATTERN.search(analysis):
|
question_text = analysis[2:].strip()
|
||||||
# Simple heuristic: if line ends with ?, treat as question
|
if question_text:
|
||||||
if analysis.rstrip().endswith('?'):
|
questions.append({
|
||||||
question_text = analysis.rstrip('?').strip()
|
"participant": participant_name,
|
||||||
# Avoid duplicate if already extracted
|
"question": question_text,
|
||||||
if question_text and not any(q['question'] == question_text for q in questions):
|
"status": "OPEN",
|
||||||
questions.append(
|
})
|
||||||
{"participant": participant_name, "question": question_text, "status": "OPEN"}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract action items using regex (finds markers anywhere in line)
|
elif lowered.startswith("action:"):
|
||||||
for match in ACTION_PATTERN.finditer(analysis):
|
action_text = analysis[7:].strip()
|
||||||
action_text = match.group(1).strip()
|
|
||||||
if action_text:
|
if action_text:
|
||||||
# Extract assignee from @mention in the line
|
|
||||||
assignee = None
|
assignee = None
|
||||||
mention_match = MENTION_PATTERN.search(action_text)
|
mention_match = MENTION_PATTERN.search(action_text)
|
||||||
if mention_match:
|
if mention_match:
|
||||||
assignee = mention_match.group(1)
|
assignee = mention_match.group(1)
|
||||||
action_items.append(
|
action_items.append({
|
||||||
{
|
|
||||||
"participant": participant_name,
|
"participant": participant_name,
|
||||||
"action": action_text,
|
"action": action_text,
|
||||||
"status": "TODO",
|
"status": "TODO",
|
||||||
"assignee": assignee,
|
"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
|
# Legacy support for plain text markers at line start
|
||||||
lowered = analysis.lower()
|
|
||||||
if lowered.startswith("assigned:"):
|
if lowered.startswith("assigned:"):
|
||||||
_, _, action_text = analysis.partition(":")
|
_, _, action_text = analysis.partition(":")
|
||||||
action_text = action_text.strip()
|
action_text = action_text.strip()
|
||||||
|
|
|
||||||
|
|
@ -36,36 +36,66 @@ partition "Parse Comments" {
|
||||||
:Apply regex patterns\nto comment text;
|
:Apply regex patterns\nto comment text;
|
||||||
|
|
||||||
if (**DECISION**: found?) then (yes)
|
if (**DECISION**: found?) then (yes)
|
||||||
:Pattern: (?:\\*\\*)?DECISION(?:\\*\\*)?\n\\s*:\\s*(.+?)(?=\\*\\*|VOTE:|$);
|
|
||||||
:Extract decision text;
|
: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",
|
participant: "Rob",
|
||||||
decision: "text...",
|
decision: "text...",
|
||||||
rationale: "",
|
rationale: "",
|
||||||
supporters: []
|
supporters: []
|
||||||
};
|
}
|
||||||
|
end note
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if (**QUESTION**: found?) then (yes)
|
if (**QUESTION**: found?) then (yes)
|
||||||
:Pattern: (?:\\*\\*)?(?:QUESTION|Q)(?:\\*\\*)?\n\\s*:\\s*(.+?)(?=\\*\\*|VOTE:|$);
|
|
||||||
:Extract question text;
|
: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",
|
participant: "Rob",
|
||||||
question: "text...",
|
question: "text...",
|
||||||
status: "OPEN"
|
status: "OPEN"
|
||||||
};
|
}
|
||||||
|
end note
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if (**ACTION**: found?) then (yes)
|
if (**ACTION**: found?) then (yes)
|
||||||
:Pattern: (?:\\*\\*)?(?:ACTION|TODO)(?:\\*\\*)?\n\\s*:\\s*(.+?)(?=\\*\\*|VOTE:|$);
|
|
||||||
:Extract action text;
|
:Extract action text;
|
||||||
:Search for @mention in 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",
|
participant: "Rob",
|
||||||
action: "text...",
|
action: "text...",
|
||||||
assignee: "Sarah",
|
assignee: "Sarah",
|
||||||
status: "TODO"
|
status: "TODO"
|
||||||
};
|
}
|
||||||
|
end note
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if (Line ends with "?") then (yes)
|
if (Line ends with "?") then (yes)
|
||||||
|
|
@ -126,10 +156,12 @@ partition "Generate Summary Sections" {
|
||||||
- Summarize key events;
|
- Summarize key events;
|
||||||
}
|
}
|
||||||
|
|
||||||
:Update marker blocks in .sum.md:
|
:Update marker blocks in .sum.md;
|
||||||
<!-- SUMMARY:DECISIONS START -->
|
note right
|
||||||
...
|
<!-- SUMMARY:DECISIONS START -->
|
||||||
<!-- SUMMARY:DECISIONS END -->;
|
...
|
||||||
|
<!-- SUMMARY:DECISIONS END -->
|
||||||
|
end note
|
||||||
|
|
||||||
:Stage updated .sum.md file;
|
:Stage updated .sum.md file;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue