implement 0004_plan_how_to_edit_question_table_from_check_result
This commit is contained in:
567
app/review_filler.py
Normal file
567
app/review_filler.py
Normal file
@@ -0,0 +1,567 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from docx import Document
|
||||
from docx.oxml.ns import qn
|
||||
from docx.table import Table as DocxTable
|
||||
|
||||
|
||||
ReviewResult = Literal["通过", "未通过", "不适用"]
|
||||
EvidencePolarity = Literal["positive", "negative", "manual", "suggestion"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReviewCriterion:
|
||||
table_heading: str
|
||||
table_index: int
|
||||
row_index: int
|
||||
sequence: str
|
||||
category: str
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReviewTable:
|
||||
heading: str
|
||||
table_index: int
|
||||
header_row_index: int
|
||||
sequence_col: int
|
||||
category_col: int
|
||||
content_col: int
|
||||
pass_col: int
|
||||
fail_col: int
|
||||
na_col: int
|
||||
criteria: list[ReviewCriterion]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AnalysisEvidence:
|
||||
section: str
|
||||
topic: str
|
||||
text: str
|
||||
polarity: EvidencePolarity
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParsedAnalysis:
|
||||
path: str
|
||||
source_filename: str
|
||||
matched_skills: list[str]
|
||||
full_text: str
|
||||
evidences: list[AnalysisEvidence]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReviewDecision:
|
||||
criterion: ReviewCriterion
|
||||
result: ReviewResult
|
||||
confidence: float
|
||||
reason: str
|
||||
evidence: list[AnalysisEvidence]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReviewFillResult:
|
||||
analysis_markdown: str
|
||||
review_docx: str
|
||||
output_docx: str
|
||||
target_heading: str
|
||||
decisions: list[ReviewDecision]
|
||||
|
||||
|
||||
SECTION_POLARITY: dict[str, EvidencePolarity] = {
|
||||
"符合项": "positive",
|
||||
"不符合项": "negative",
|
||||
"缺失章节或缺失证据": "negative",
|
||||
"整改建议": "suggestion",
|
||||
"需人工复核事项": "manual",
|
||||
}
|
||||
|
||||
SECTION_ALIASES = [
|
||||
("不符合项", "不符合项"),
|
||||
("符合项", "符合项"),
|
||||
("缺失章节或缺失证据", "缺失章节或缺失证据"),
|
||||
("缺失章节", "缺失章节或缺失证据"),
|
||||
("需人工复核事项", "需人工复核事项"),
|
||||
("整改建议", "整改建议"),
|
||||
]
|
||||
|
||||
KEYWORD_GROUPS = {
|
||||
"标识": {"完整标识", "标识号", "缩略名", "版本号", "发布号"},
|
||||
"系统概述": {"系统概述", "用途", "一般特性", "安全性", "可靠性", "实时性", "技术风险"},
|
||||
"文档概述": {"文档概述", "保密性", "安全保密"},
|
||||
"引用文档": {"引用文档", "引用文件", "编号", "标题", "修订版", "日期"},
|
||||
"状态方式": {"状态", "方式", "正常模式", "减载模式", "降级", "紧急"},
|
||||
"能力需求": {"能力需求", "CSCI能力", "软件任务"},
|
||||
"外部接口": {"外部接口", "接口图", "1553B", "CAN", "RS422"},
|
||||
"内部接口": {"内部接口", "内部接口需求"},
|
||||
"内部数据": {"内部数据", "数据结构", "全局变量", "数据字典"},
|
||||
"适应性": {"适应性", "运行环境", "适配"},
|
||||
"保密性": {"保密性", "保密"},
|
||||
"安全性": {"安全性", "安全"},
|
||||
"环境适应性": {"环境适应性", "环境"},
|
||||
"计算机资源": {"计算机资源", "处理时间", "内存", "存储", "CPU", "资源预算"},
|
||||
"人员训练": {"人员", "训练"},
|
||||
"优先级": {"优先级", "关键性", "必须", "应当", "可选"},
|
||||
"合格性规定": {"合格性规定", "检验方法", "测试", "演示", "推断"},
|
||||
"需求可追踪性": {"需求可追踪性", "可追踪性", "追踪矩阵", "双向追踪", "追溯"},
|
||||
"可验证": {"可验证", "可测试", "验证", "判据", "粒度"},
|
||||
"准确清晰": {"准确", "清晰", "歧义", "明确", "二义性", "术语"},
|
||||
"图表": {"图", "图形", "流程图", "时序图", "图文"},
|
||||
"一致性": {"一致", "冲突", "矛盾"},
|
||||
}
|
||||
|
||||
|
||||
def parse_analysis_markdown(path: Path | str) -> ParsedAnalysis:
|
||||
markdown_path = Path(path)
|
||||
text = markdown_path.read_text(encoding="utf-8")
|
||||
source_filename = _extract_front_matter_value(text, "源文件")
|
||||
matched = _extract_front_matter_value(text, "命中技能")
|
||||
matched_skills = [item.strip() for item in matched.split(",") if item.strip() and item.strip() != "无"]
|
||||
model_text = text.split("## 模型分析原文", 1)[-1]
|
||||
evidences = _extract_evidences(model_text)
|
||||
return ParsedAnalysis(
|
||||
path=str(markdown_path),
|
||||
source_filename=source_filename,
|
||||
matched_skills=matched_skills,
|
||||
full_text=text,
|
||||
evidences=evidences,
|
||||
)
|
||||
|
||||
|
||||
def extract_review_tables(docx_path: Path | str) -> list[ReviewTable]:
|
||||
document = Document(docx_path)
|
||||
table_objects = iter(document.tables)
|
||||
review_tables: list[ReviewTable] = []
|
||||
current_heading = ""
|
||||
table_index = 0
|
||||
|
||||
for child in document._element.body.iterchildren():
|
||||
if child.tag == qn("w:p"):
|
||||
text = _paragraph_text(child)
|
||||
if text:
|
||||
current_heading = text
|
||||
continue
|
||||
if child.tag != qn("w:tbl"):
|
||||
continue
|
||||
|
||||
table = next(table_objects)
|
||||
parsed = _parse_review_table(table, current_heading, table_index)
|
||||
if parsed is not None:
|
||||
review_tables.append(parsed)
|
||||
table_index += 1
|
||||
|
||||
return review_tables
|
||||
|
||||
|
||||
def select_review_table(analysis: ParsedAnalysis, tables: list[ReviewTable]) -> ReviewTable:
|
||||
candidates = [table for table in tables if "A.1" not in table.heading]
|
||||
if not candidates:
|
||||
raise ValueError("No review tables found after skipping A.1")
|
||||
|
||||
text = f"{analysis.source_filename}\n{' '.join(analysis.matched_skills)}\n{analysis.full_text}"
|
||||
scores: list[tuple[int, ReviewTable]] = []
|
||||
for table in candidates:
|
||||
heading = table.heading
|
||||
score = 0
|
||||
if "A.2" in heading:
|
||||
score += _term_score(text, ["需求规格说明", "SRS", "需求章节", "需求可追踪性", "合格性规定"])
|
||||
score += 6 if any("requirement" in skill for skill in analysis.matched_skills) else 0
|
||||
if "A.3" in heading:
|
||||
score += _term_score(text, ["设计说明", "详细设计", "软件单元", "CSCI级设计决策"])
|
||||
if "A.4" in heading:
|
||||
score += _term_score(text, ["用户手册", "安装", "操作", "用户功能", "快速参考"])
|
||||
scores.append((score, table))
|
||||
|
||||
scores.sort(key=lambda item: (-item[0], item[1].table_index))
|
||||
best_score, best_table = scores[0]
|
||||
if best_score <= 0:
|
||||
raise ValueError("Unable to select a review table from the analysis report")
|
||||
return best_table
|
||||
|
||||
|
||||
def build_review_decisions(analysis: ParsedAnalysis, table: ReviewTable) -> list[ReviewDecision]:
|
||||
decisions: list[ReviewDecision] = []
|
||||
for criterion in table.criteria:
|
||||
matched = _match_evidence(criterion, analysis.evidences)
|
||||
decisions.append(_decide_review_result(criterion, matched))
|
||||
return decisions
|
||||
|
||||
|
||||
def build_review_decisions_for_tables(
|
||||
analysis: ParsedAnalysis,
|
||||
tables: list[ReviewTable],
|
||||
) -> list[ReviewDecision]:
|
||||
decisions: list[ReviewDecision] = []
|
||||
for table in tables:
|
||||
decisions.extend(build_review_decisions(analysis, table))
|
||||
return decisions
|
||||
|
||||
|
||||
def apply_review_decisions(
|
||||
review_docx_path: Path | str,
|
||||
decisions: list[ReviewDecision],
|
||||
output_docx_path: Path | str,
|
||||
mark: str = "✔",
|
||||
) -> Path:
|
||||
if not decisions:
|
||||
raise ValueError("No review decisions to apply")
|
||||
|
||||
document = Document(review_docx_path)
|
||||
tables_by_index = {index: table for index, table in enumerate(document.tables)}
|
||||
grouped: dict[int, list[ReviewDecision]] = {}
|
||||
for decision in decisions:
|
||||
grouped.setdefault(decision.criterion.table_index, []).append(decision)
|
||||
|
||||
for table_index, table_decisions in grouped.items():
|
||||
table = tables_by_index[table_index]
|
||||
review_table = _parse_review_table(table, table_decisions[0].criterion.table_heading, table_index)
|
||||
if review_table is None:
|
||||
raise ValueError(f"Unable to parse review table {table_index}")
|
||||
by_sequence = {decision.criterion.sequence: decision for decision in table_decisions}
|
||||
result_cols = {
|
||||
"通过": review_table.pass_col,
|
||||
"未通过": review_table.fail_col,
|
||||
"不适用": review_table.na_col,
|
||||
}
|
||||
for criterion in review_table.criteria:
|
||||
decision = by_sequence.get(criterion.sequence)
|
||||
if decision is None:
|
||||
continue
|
||||
row = table.rows[criterion.row_index]
|
||||
selected_col = result_cols[decision.result]
|
||||
for col in (review_table.pass_col, review_table.fail_col, review_table.na_col):
|
||||
row.cells[col].text = mark if col == selected_col else ""
|
||||
|
||||
output_path = Path(output_docx_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
document.save(output_path)
|
||||
return output_path
|
||||
|
||||
|
||||
def validate_review_results(docx_path: Path | str, target_heading: str, mark: str = "✔") -> list[str]:
|
||||
errors: list[str] = []
|
||||
target = _find_table_by_heading(extract_review_tables(docx_path), target_heading)
|
||||
if target is None:
|
||||
return [f"未找到目标审查表:{target_heading}"]
|
||||
|
||||
document = Document(docx_path)
|
||||
table = document.tables[target.table_index]
|
||||
for criterion in target.criteria:
|
||||
row = table.rows[criterion.row_index]
|
||||
marks = [
|
||||
row.cells[target.pass_col].text.strip(),
|
||||
row.cells[target.fail_col].text.strip(),
|
||||
row.cells[target.na_col].text.strip(),
|
||||
]
|
||||
if sum(1 for value in marks if value == mark) != 1:
|
||||
errors.append(f"{target.heading} 序号 {criterion.sequence} 审查结果不是有且仅有一个勾:{marks}")
|
||||
return errors
|
||||
|
||||
|
||||
def validate_review_tables(docx_path: Path | str, target_headings: list[str], mark: str = "✔") -> list[str]:
|
||||
errors: list[str] = []
|
||||
for heading in target_headings:
|
||||
errors.extend(validate_review_results(docx_path, heading, mark=mark))
|
||||
return errors
|
||||
|
||||
|
||||
def fill_review_docx_from_analysis(
|
||||
analysis_markdown_path: Path | str,
|
||||
review_docx_path: Path | str,
|
||||
output_docx_path: Path | str,
|
||||
target_heading: str | None = None,
|
||||
) -> ReviewFillResult:
|
||||
analysis = parse_analysis_markdown(analysis_markdown_path)
|
||||
tables = extract_review_tables(review_docx_path)
|
||||
target_tables = _target_review_tables(tables, target_heading)
|
||||
if not target_tables:
|
||||
raise ValueError(f"Unable to find target review table: {target_heading}")
|
||||
|
||||
decisions = build_review_decisions_for_tables(analysis, target_tables)
|
||||
output_path = apply_review_decisions(review_docx_path, decisions, output_docx_path)
|
||||
errors = validate_review_tables(output_path, [table.heading for table in target_tables])
|
||||
if errors:
|
||||
raise ValueError("; ".join(errors))
|
||||
|
||||
return ReviewFillResult(
|
||||
analysis_markdown=str(analysis_markdown_path),
|
||||
review_docx=str(review_docx_path),
|
||||
output_docx=str(output_path),
|
||||
target_heading="; ".join(table.heading for table in target_tables),
|
||||
decisions=decisions,
|
||||
)
|
||||
|
||||
|
||||
def write_decisions_json(result: ReviewFillResult, output_path: Path | str) -> Path:
|
||||
path = Path(output_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(_jsonable(result), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def _parse_review_table(table: DocxTable, heading: str, table_index: int) -> ReviewTable | None:
|
||||
header = _find_review_header(table)
|
||||
if header is None:
|
||||
return None
|
||||
header_row_index, sequence_col, category_col, content_col, pass_col, fail_col, na_col = header
|
||||
criteria: list[ReviewCriterion] = []
|
||||
for row_index in range(header_row_index + 1, len(table.rows)):
|
||||
cells = table.rows[row_index].cells
|
||||
if max(sequence_col, category_col, content_col, pass_col, fail_col, na_col) >= len(cells):
|
||||
continue
|
||||
sequence = _cell_text(cells[sequence_col])
|
||||
if not sequence.isdigit():
|
||||
continue
|
||||
criteria.append(
|
||||
ReviewCriterion(
|
||||
table_heading=heading,
|
||||
table_index=table_index,
|
||||
row_index=row_index,
|
||||
sequence=sequence,
|
||||
category=_cell_text(cells[category_col]),
|
||||
content=_cell_text(cells[content_col]),
|
||||
)
|
||||
)
|
||||
if not criteria:
|
||||
return None
|
||||
return ReviewTable(
|
||||
heading=heading,
|
||||
table_index=table_index,
|
||||
header_row_index=header_row_index,
|
||||
sequence_col=sequence_col,
|
||||
category_col=category_col,
|
||||
content_col=content_col,
|
||||
pass_col=pass_col,
|
||||
fail_col=fail_col,
|
||||
na_col=na_col,
|
||||
criteria=criteria,
|
||||
)
|
||||
|
||||
|
||||
def _find_review_header(table: DocxTable) -> tuple[int, int, int, int, int, int, int] | None:
|
||||
for row_index, row in enumerate(table.rows):
|
||||
texts = [_cell_text(cell) for cell in row.cells]
|
||||
if "序号" not in texts or "审查内容" not in texts:
|
||||
continue
|
||||
for option_row_index in range(row_index, min(row_index + 3, len(table.rows))):
|
||||
option_texts = [_cell_text(cell) for cell in table.rows[option_row_index].cells]
|
||||
if {"通过", "未通过", "不适用"}.issubset(option_texts):
|
||||
return (
|
||||
option_row_index,
|
||||
texts.index("序号"),
|
||||
texts.index("审查项") if "审查项" in texts else 1,
|
||||
texts.index("审查内容"),
|
||||
option_texts.index("通过"),
|
||||
option_texts.index("未通过"),
|
||||
option_texts.index("不适用"),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _extract_evidences(model_text: str) -> list[AnalysisEvidence]:
|
||||
heading_re = re.compile(r"^###\s*(?:\d+[.、]\s*)?(.+?)\s*$")
|
||||
current_section = ""
|
||||
blocks: dict[str, list[str]] = {}
|
||||
for line in model_text.splitlines():
|
||||
match = heading_re.match(line.strip())
|
||||
if match:
|
||||
title = _normalize_section_title(match.group(1))
|
||||
current_section = title if title in SECTION_POLARITY else ""
|
||||
if current_section:
|
||||
blocks.setdefault(current_section, [])
|
||||
continue
|
||||
if current_section:
|
||||
blocks[current_section].append(line)
|
||||
|
||||
evidences: list[AnalysisEvidence] = []
|
||||
for section, lines in blocks.items():
|
||||
for item in _paragraph_items(lines):
|
||||
topic = _extract_topic(item)
|
||||
evidences.append(
|
||||
AnalysisEvidence(
|
||||
section=section,
|
||||
topic=topic,
|
||||
text=item,
|
||||
polarity=SECTION_POLARITY[section],
|
||||
)
|
||||
)
|
||||
return evidences
|
||||
|
||||
|
||||
def _paragraph_items(lines: list[str]) -> list[str]:
|
||||
items: list[str] = []
|
||||
current: list[str] = []
|
||||
bullet_re = re.compile(r"^\s*(?:[-*]|\d+[.、])\s+(.+)")
|
||||
for raw_line in lines:
|
||||
line = raw_line.strip()
|
||||
if not line or line in {"---"}:
|
||||
continue
|
||||
bullet = bullet_re.match(line)
|
||||
if bullet:
|
||||
if current:
|
||||
items.append(_clean_markdown(" ".join(current)))
|
||||
current = [bullet.group(1)]
|
||||
elif current:
|
||||
current.append(line)
|
||||
else:
|
||||
current = [line]
|
||||
if current:
|
||||
items.append(_clean_markdown(" ".join(current)))
|
||||
return [item for item in items if item]
|
||||
|
||||
|
||||
def _match_evidence(criterion: ReviewCriterion, evidences: list[AnalysisEvidence]) -> list[AnalysisEvidence]:
|
||||
keywords = _criterion_keywords(criterion)
|
||||
scored: list[tuple[int, AnalysisEvidence]] = []
|
||||
for evidence in evidences:
|
||||
normalized = _normalize_text(f"{evidence.topic} {evidence.text}")
|
||||
score = sum(1 for keyword in keywords if _normalize_text(keyword) in normalized)
|
||||
if criterion.category == "准确性" and evidence.polarity in {"manual", "negative"}:
|
||||
score += sum(1 for term in ["歧义", "明确", "可验证", "可测试", "粒度", "清晰"] if term in normalized)
|
||||
if criterion.category == "一致性":
|
||||
score += sum(1 for term in ["一致", "冲突", "矛盾"] if term in normalized)
|
||||
if score >= 2:
|
||||
scored.append((score, evidence))
|
||||
scored.sort(key=lambda item: (-item[0], -_polarity_weight(item[1].polarity)))
|
||||
return [evidence for _, evidence in scored[:5]]
|
||||
|
||||
|
||||
def _decide_review_result(criterion: ReviewCriterion, evidences: list[AnalysisEvidence]) -> ReviewDecision:
|
||||
negative = [item for item in evidences if item.polarity == "negative"]
|
||||
manual = [item for item in evidences if item.polarity == "manual"]
|
||||
positive = [item for item in evidences if item.polarity == "positive"]
|
||||
|
||||
if _is_not_applicable(criterion):
|
||||
return ReviewDecision(criterion, "不适用", 0.7, "该审查内容不适用于当前自动选择的审查单。", evidences)
|
||||
|
||||
if negative:
|
||||
return ReviewDecision(criterion, "未通过", 0.88, _reason_from_evidence("分析报告存在不符合或缺失证据", negative), evidences)
|
||||
|
||||
if criterion.category == "准确性" and manual:
|
||||
return ReviewDecision(criterion, "未通过", 0.74, _reason_from_evidence("分析报告提示需要人工复核,不能作为通过证据", manual), evidences)
|
||||
|
||||
if criterion.category == "一致性" and not positive:
|
||||
return ReviewDecision(criterion, "未通过", 0.62, "当前分析报告未提供足够的一致性通过证据。", evidences)
|
||||
|
||||
if positive:
|
||||
return ReviewDecision(criterion, "通过", 0.78, _reason_from_evidence("分析报告提供了符合证据", positive), evidences)
|
||||
|
||||
if manual:
|
||||
return ReviewDecision(criterion, "未通过", 0.65, _reason_from_evidence("分析报告提示需要人工复核", manual), evidences)
|
||||
|
||||
return ReviewDecision(criterion, "未通过", 0.5, "分析报告未提供充分通过证据。", evidences)
|
||||
|
||||
|
||||
def _criterion_keywords(criterion: ReviewCriterion) -> set[str]:
|
||||
keywords = {token for token in _tokenize(criterion.content) if re.search(r"[A-Za-z0-9]", token)}
|
||||
for group_name, group_keywords in KEYWORD_GROUPS.items():
|
||||
name_match = group_name in criterion.content and group_name != "标识"
|
||||
if any(keyword in criterion.content for keyword in group_keywords) or name_match:
|
||||
keywords.update(group_keywords)
|
||||
if criterion.category == "准确性":
|
||||
keywords.update({"准确", "清晰", "明确", "歧义", "可验证", "可测试", "粒度"})
|
||||
elif criterion.category == "一致性":
|
||||
keywords.update({"一致", "冲突", "矛盾"})
|
||||
return {keyword for keyword in keywords if len(keyword) >= 2}
|
||||
|
||||
|
||||
def _tokenize(text: str) -> list[str]:
|
||||
ascii_tokens = re.findall(r"[A-Za-z][A-Za-z0-9_-]{1,}", text)
|
||||
chinese_tokens = re.findall(r"[\u4e00-\u9fff]{2,}", text)
|
||||
return ascii_tokens + chinese_tokens
|
||||
|
||||
|
||||
def _extract_front_matter_value(text: str, label: str) -> str:
|
||||
match = re.search(rf"^- {re.escape(label)}:(.+)$", text, flags=re.MULTILINE)
|
||||
return match.group(1).strip() if match else ""
|
||||
|
||||
|
||||
def _normalize_section_title(title: str) -> str:
|
||||
stripped = title.strip(" #::")
|
||||
for key, value in SECTION_ALIASES:
|
||||
if key in stripped:
|
||||
return value
|
||||
return stripped
|
||||
|
||||
|
||||
def _extract_topic(text: str) -> str:
|
||||
cleaned = text.strip()
|
||||
cleaned = cleaned.lstrip("-*0123456789.、 ")
|
||||
if ":" in cleaned:
|
||||
return cleaned.split(":", 1)[0].strip()
|
||||
if ":" in cleaned:
|
||||
return cleaned.split(":", 1)[0].strip()
|
||||
return cleaned[:30]
|
||||
|
||||
|
||||
def _clean_markdown(text: str) -> str:
|
||||
cleaned = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
cleaned = re.sub(r"\*\*([^*]+)\*\*", r"\1", cleaned)
|
||||
cleaned = re.sub(r"\s+", " ", cleaned)
|
||||
return cleaned.strip()
|
||||
|
||||
|
||||
def _normalize_text(text: str) -> str:
|
||||
return re.sub(r"[\s`*_#::,。,;;()()、\-]+", "", text).lower()
|
||||
|
||||
|
||||
def _paragraph_text(element) -> str:
|
||||
return "".join(text.text or "" for text in element.iter(qn("w:t"))).strip()
|
||||
|
||||
|
||||
def _cell_text(cell) -> str:
|
||||
return cell.text.strip().replace("\n", "")
|
||||
|
||||
|
||||
def _term_score(text: str, terms: list[str]) -> int:
|
||||
return sum(4 for term in terms if term in text)
|
||||
|
||||
|
||||
def _polarity_weight(polarity: EvidencePolarity) -> int:
|
||||
return {"negative": 4, "manual": 3, "positive": 2, "suggestion": 1}[polarity]
|
||||
|
||||
|
||||
def _is_not_applicable(criterion: ReviewCriterion) -> bool:
|
||||
content = criterion.content
|
||||
if "用户" in criterion.table_heading and "CSCI" in content:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _reason_from_evidence(prefix: str, evidences: list[AnalysisEvidence]) -> str:
|
||||
if not evidences:
|
||||
return prefix
|
||||
text = evidences[0].text
|
||||
return f"{prefix}:{text[:160]}"
|
||||
|
||||
|
||||
def _find_table_by_heading(tables: list[ReviewTable], heading: str | None) -> ReviewTable | None:
|
||||
if not heading:
|
||||
return None
|
||||
for table in tables:
|
||||
if heading in table.heading:
|
||||
return table
|
||||
return None
|
||||
|
||||
|
||||
def _target_review_tables(tables: list[ReviewTable], target_heading: str | None) -> list[ReviewTable]:
|
||||
if target_heading:
|
||||
table = _find_table_by_heading(tables, target_heading)
|
||||
return [table] if table is not None else []
|
||||
return [table for table in tables if "A.1" not in table.heading]
|
||||
|
||||
|
||||
def _jsonable(value):
|
||||
if hasattr(value, "__dataclass_fields__"):
|
||||
return {key: _jsonable(item) for key, item in asdict(value).items()}
|
||||
if isinstance(value, list):
|
||||
return [_jsonable(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _jsonable(item) for key, item in value.items()}
|
||||
return value
|
||||
Reference in New Issue
Block a user