1919import hashlib
2020import shutil
2121import tempfile
22+ import inspect
2223from html import unescape
2324from datetime import datetime
2425from typing import Dict , List , Optional , Any , Tuple
9293 or "2026-04-27-v4"
9394)
9495READING_REPORT_EVIDENCE_CACHE_ENABLED = os .environ .get ("READING_REPORT_EVIDENCE_CACHE_ENABLED" , "1" ).strip ().lower () not in {"0" , "false" , "off" , "no" }
95- READING_REPORT_OUTPUT_VERSION = os .environ .get ("READING_REPORT_OUTPUT_VERSION" , "2026-05-31-v4 " ).strip () or "2026-05-31-v4 "
96+ READING_REPORT_OUTPUT_VERSION = os .environ .get ("READING_REPORT_OUTPUT_VERSION" , "2026-06-04-v5 " ).strip () or "2026-06-04-v5 "
9697READING_REPORT_PROFILE_RETRIEVAL_WEIGHT = float (os .environ .get ("READING_REPORT_PROFILE_RETRIEVAL_WEIGHT" , "0.25" ))
9798HTTP_RETRY_TOTAL = int (os .environ .get ("PAPERFLOW_HTTP_RETRIES" , "2" ))
9899HTTP_RETRY_BACKOFF = float (os .environ .get ("PAPERFLOW_HTTP_BACKOFF" , "0.8" ))
@@ -132,7 +133,7 @@ def _resolve_configured_dir(
132133 base_dir = Path (configured ).expanduser () if configured else PROJECT_ROOT / default_relative
133134 if not base_dir .is_absolute ():
134135 base_dir = PROJECT_ROOT / base_dir
135- if user_id :
136+ if user_id and not configured :
136137 base_dir = role_utils .apply_output_scope (
137138 base_dir ,
138139 user_id ,
@@ -2930,7 +2931,7 @@ def build_heuristic_report_payload(
29302931 }
29312932
29322933
2933- def _normalize_string_list (value : Any , limit : int = 4 ) -> List [str ]:
2934+ def _normalize_string_list (value : Any , limit : int = 4 , max_chars : int = 180 ) -> List [str ]:
29342935 if isinstance (value , str ):
29352936 candidates = re .split (r"\n+|[;;]+" , value )
29362937 elif isinstance (value , list ):
@@ -2942,10 +2943,55 @@ def _normalize_string_list(value: Any, limit: int = 4) -> List[str]:
29422943 for candidate in candidates :
29432944 cleaned = re .sub (r"^\s*[-*•\d\.\)\(]+\s*" , "" , str (candidate )).strip ()
29442945 if len (cleaned ) >= 4 :
2945- items .append (_truncate_text (cleaned , 180 ))
2946+ items .append (_truncate_text (cleaned , max_chars ))
29462947 return _unique_preserve_order (items )[:limit ]
29472948
29482949
2950+ def _looks_like_extraction_noise (text : str ) -> bool :
2951+ cleaned = _clean_text (text )
2952+ if not cleaned :
2953+ return False
2954+ lowered = cleaned .lower ()
2955+ noise_markers = (
2956+ "arxiv:" ,
2957+ "[cs." ,
2958+ "correspondence to:" ,
2959+ "copyright" ,
2960+ "accepted by" ,
2961+ "proceedings of" ,
2962+ "doi:" ,
2963+ )
2964+ marker_hits = sum (1 for marker in noise_markers if marker in lowered )
2965+ short_line_count = sum (1 for line in cleaned .splitlines () if 0 < len (line .strip ()) <= 18 )
2966+ digit_ratio = sum (ch .isdigit () for ch in cleaned ) / max (1 , len (cleaned ))
2967+ return marker_hits >= 2 or short_line_count >= 6 or digit_ratio > 0.12
2968+
2969+
2970+ def _append_template_qa_block (lines : List [str ], qid : str , question : str , content : Any ) -> None :
2971+ lines .append (f"{ qid } : { question } " )
2972+ lines .append ("" )
2973+ if isinstance (content , list ):
2974+ values = [_clean_text (item ) for item in content if _clean_text (item )]
2975+ if values :
2976+ for item in values :
2977+ lines .append (f"- { item } " )
2978+ else :
2979+ lines .append ("当前信息不足,建议回到原文对应章节核对。" )
2980+ else :
2981+ text = _clean_text (content )
2982+ if text :
2983+ for paragraph in re .split (r"\n{2,}" , text ):
2984+ paragraph = paragraph .strip ()
2985+ if paragraph :
2986+ lines .append (paragraph )
2987+ lines .append ("" )
2988+ if lines and lines [- 1 ] == "" :
2989+ lines .pop ()
2990+ else :
2991+ lines .append ("当前信息不足,建议回到原文对应章节核对。" )
2992+ lines .append ("" )
2993+
2994+
29492995def _synthesize_report_with_llm (
29502996 paper : Dict [str , Any ],
29512997 user_profile : Dict [str , Any ],
@@ -2979,12 +3025,39 @@ def _synthesize_report_with_llm(
29793025 return {"analysis_note" : fallback_note }
29803026
29813027
3028+ def _enrich_paper_for_reading_report_compat (
3029+ paper : Dict [str , Any ],
3030+ * ,
3031+ user_id : Optional [str ] = None ,
3032+ ) -> Tuple [Dict [str , Any ], Optional [Dict [str , Any ]], Optional [str ]]:
3033+ """Call the enrichment hook while tolerating older one-argument test doubles."""
3034+ try :
3035+ signature = inspect .signature (enrich_paper_for_reading_report )
3036+ if "user_id" in signature .parameters :
3037+ return enrich_paper_for_reading_report (paper , user_id = user_id )
3038+ except (TypeError , ValueError ):
3039+ pass
3040+ return enrich_paper_for_reading_report (paper )
3041+
3042+
29823043def _merge_report_payload (base : Dict [str , Any ], llm_payload : Optional [Dict [str , Any ]]) -> Dict [str , Any ]:
29833044 if not isinstance (llm_payload , dict ):
29843045 return base
29853046
29863047 merged = dict (base )
2987- for key in ("one_sentence_summary" , "research_background" , "core_method" , "key_results" ):
3048+ for key in (
3049+ "one_sentence_summary" ,
3050+ "clean_abstract_summary" ,
3051+ "problem_analysis" ,
3052+ "related_work" ,
3053+ "solution_approach" ,
3054+ "experiments" ,
3055+ "future_directions" ,
3056+ "paper_summary" ,
3057+ "research_background" ,
3058+ "core_method" ,
3059+ "key_results" ,
3060+ ):
29883061 value = _clean_text (llm_payload .get (key ))
29893062 if value :
29903063 merged [key ] = value
@@ -2994,7 +3067,7 @@ def _merge_report_payload(base: Dict[str, Any], llm_payload: Optional[Dict[str,
29943067 merged ["analysis_note" ] = _append_analysis_note (merged .get ("analysis_note" ), analysis_note )
29953068
29963069 for key in ("main_contributions" , "limitations" , "relevance_points" , "reading_focus" ):
2997- values = _normalize_string_list (llm_payload .get (key ))
3070+ values = _normalize_string_list (llm_payload .get (key ), max_chars = 320 )
29983071 if values :
29993072 merged [key ] = values
30003073
@@ -3052,6 +3125,9 @@ def generate_reading_report(
30523125 payload = report_payload or build_heuristic_report_payload (paper , user_profile )
30533126 title = _clean_text (paper .get ("title" )) or "Untitled Paper"
30543127 abstract = _clean_abstract_text (payload .get ("abstract" ) or paper .get ("abstract" ))
3128+ clean_abstract_summary = _clean_text (payload .get ("clean_abstract_summary" ))
3129+ if clean_abstract_summary and (not _clean_text (abstract ) or _looks_like_extraction_noise (abstract )):
3130+ abstract = clean_abstract_summary
30553131 if not _clean_text (abstract ):
30563132 fallback_source = _get_direct_pdf_url (paper ) or _get_first_url (
30573133 paper ,
@@ -3124,52 +3200,51 @@ def generate_reading_report(
31243200 lines .append (f"> { paragraph .strip ()} " )
31253201 lines .append ("" )
31263202
3127- _append_qa_block (
3128- lines ,
3129- "Q1" ,
3130- "这篇论文试图解决什么问题?" ,
3131- payload .get ("research_background" ) or "建议先回到原文摘要和引言确认研究问题。" ,
3132- )
3133- _append_qa_block (
3134- lines ,
3135- "Q2" ,
3136- "它提出了什么方法?" ,
3137- payload .get ("core_method" ) or "当前未成功提炼方法细节,请重点阅读 Method / Approach 部分。" ,
3138- )
3139- _append_qa_block (
3140- lines ,
3141- "Q3" ,
3142- "主要结果是什么?" ,
3143- payload .get ("key_results" ) or "当前没有提炼出明确结果,请重点核对实验表格和主要指标。" ,
3144- )
3145- _append_qa_block (
3146- lines ,
3147- "Q4" ,
3148- "主要贡献或创新点是什么?" ,
3149- payload .get ("main_contributions" ) or [],
3150- )
3151- _append_qa_block (
3152- lines ,
3153- "Q5" ,
3154- "局限性和注意事项是什么?" ,
3155- payload .get ("limitations" ) or [],
3156- )
3157- _append_qa_block (
3158- lines ,
3159- "Q6" ,
3160- "这篇论文和我的研究有什么关系?" ,
3161- payload .get ("relevance_points" ) or [],
3162- )
3163- reading_plan = []
3164- if analysis_note :
3165- reading_plan .append (analysis_note )
3166- reading_plan .extend (payload .get ("reading_focus" ) or [])
3167- _append_qa_block (
3168- lines ,
3169- "Q7" ,
3170- "我应该怎么读?" ,
3171- reading_plan ,
3172- )
3203+ problem_analysis = payload .get ("problem_analysis" ) or payload .get ("research_background" ) or "建议先回到原文摘要和引言确认研究问题。"
3204+ related_work = payload .get ("related_work" )
3205+ if not related_work :
3206+ related_work_items = payload .get ("main_contributions" ) or []
3207+ related_work = (
3208+ "当前自动解析没有稳定提取出 Related Work 的完整脉络。可先从论文引言、相关工作章节和引用线索核对它主要对比了哪些方法。"
3209+ )
3210+ if related_work_items :
3211+ related_work += "\n \n 从当前证据可见,论文的定位至少包括:" + ";" .join (str (item ) for item in related_work_items [:3 ]) + "。"
3212+ solution_approach = payload .get ("solution_approach" ) or payload .get ("core_method" ) or "当前未成功提炼方法细节,请重点阅读 Method / Approach 部分。"
3213+ experiments = payload .get ("experiments" )
3214+ if not experiments :
3215+ result_text = _clean_text (payload .get ("key_results" ))
3216+ experiments = result_text or "当前没有提炼出明确实验设置,请重点核对 Experiments / Results、表格、图示和主要指标。"
3217+ future_directions = payload .get ("future_directions" )
3218+ if not future_directions :
3219+ future_items = []
3220+ future_items .extend (payload .get ("limitations" ) or [])
3221+ future_items .extend (payload .get ("reading_focus" ) or [])
3222+ future_directions = future_items or ["当前信息不足,建议从局限性、失败案例、消融缺口和跨领域泛化四个角度回原文继续挖掘。" ]
3223+ paper_summary = payload .get ("paper_summary" )
3224+ if not paper_summary :
3225+ summary_parts = [
3226+ _clean_text (payload .get ("one_sentence_summary" )),
3227+ _clean_text (payload .get ("research_background" )),
3228+ _clean_text (payload .get ("core_method" )),
3229+ _clean_text (payload .get ("key_results" )),
3230+ ]
3231+ contributions = payload .get ("main_contributions" ) or []
3232+ limitations = payload .get ("limitations" ) or []
3233+ relevance = payload .get ("relevance_points" ) or []
3234+ if contributions :
3235+ summary_parts .append ("主要贡献包括:" + ";" .join (str (item ) for item in contributions [:4 ]) + "。" )
3236+ if limitations :
3237+ summary_parts .append ("需要注意的边界包括:" + ";" .join (str (item ) for item in limitations [:3 ]) + "。" )
3238+ if relevance :
3239+ summary_parts .append ("与用户画像的关系:" + ";" .join (str (item ) for item in relevance [:3 ]) + "。" )
3240+ paper_summary = "\n \n " .join (part for part in summary_parts if part ) or "当前信息不足,建议回到原文摘要、引言、方法和结论串联主线。"
3241+
3242+ _append_template_qa_block (lines , "Q1" , "这篇论文试图解决什么问题?" , problem_analysis )
3243+ _append_template_qa_block (lines , "Q2" , "有哪些相关研究?" , related_work )
3244+ _append_template_qa_block (lines , "Q3" , "论文如何解决这个问题?" , solution_approach )
3245+ _append_template_qa_block (lines , "Q4" , "论文做了哪些实验?" , experiments )
3246+ _append_template_qa_block (lines , "Q5" , "有什么可以进一步探索的点?" , future_directions )
3247+ _append_template_qa_block (lines , "Q6" , "总结一下论文的主要内容" , paper_summary )
31733248
31743249 if report_evidence_anchors :
31753250 bucket_labels = {
@@ -3521,7 +3596,10 @@ def create_reading_report(
35213596 )
35223597
35233598 try :
3524- enriched_paper , parsed_pdf , pdf_error = enrich_paper_for_reading_report (raw_paper , user_id = user_id )
3599+ enriched_paper , parsed_pdf , pdf_error = _enrich_paper_for_reading_report_compat (
3600+ raw_paper ,
3601+ user_id = user_id ,
3602+ )
35253603 heuristic_payload = build_heuristic_report_payload (
35263604 enriched_paper ,
35273605 profile ,
0 commit comments