From addc09e9dba2dcbf16e4f95553ee1fe6fa5c741e Mon Sep 17 00:00:00 2001 From: deng Date: Tue, 4 Nov 2025 23:29:20 +0800 Subject: [PATCH] mvp --- .pre-commit-config.yaml | 10 +- embrace_life/app.py | 182 ++++++++++++++++++++++++++ embrace_life/assets/favicon.jpg | Bin 0 -> 2792 bytes embrace_life/assets/will_template.txt | 11 ++ embrace_life/config.yaml | 9 ++ embrace_life/main.py | 6 - embrace_life/utils.py | 103 +++++++++++++++ pyproject.toml | 3 +- uv.lock | 2 + 9 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 embrace_life/app.py create mode 100644 embrace_life/assets/favicon.jpg create mode 100644 embrace_life/assets/will_template.txt create mode 100644 embrace_life/config.yaml delete mode 100644 embrace_life/main.py create mode 100644 embrace_life/utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06e08fe..ed21cb0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,11 +9,9 @@ repos: - id: check-yaml - id: check-docstring-first - - repo: local + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.11 hooks: - id: ruff - name: ruff - entry: ruff check . - language: python - types: [python] - always_run: true \ No newline at end of file + args: [--fix] + - id: ruff-format \ No newline at end of file diff --git a/embrace_life/app.py b/embrace_life/app.py new file mode 100644 index 0000000..aa0c141 --- /dev/null +++ b/embrace_life/app.py @@ -0,0 +1,182 @@ +# app.py +# +# author: deng +# date: 20251104 + +from datetime import datetime + +import pandas as pd +import streamlit as st +from langchain.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI +from utils import is_valid_taiwan_id, parse_config, parse_txt + + +class EmbraceLifeApp: + def __init__(self, config_path: str = 'config.yaml', will_template_path: str = 'assets/will_template.txt') -> None: + self._config = parse_config(config_path) + self._will_template_path = will_template_path + self._will_content = None + self._chain = None + + def _prepare_chain(self) -> None: + """Prepare the chain for translation""" + if self._chain is not None: + return + + system_template = ( + '你是臺灣民法專家,請判斷接下來自書遺囑內容在請立囑人親筆書寫簽名後是否有效,' + '有效的話則回傳有效,無效的話請一百字內告訴我哪些內容需要修改。' + ) + user_template = '{input_text}' + + llm = ChatOpenAI( + model=self._config['app']['openai']['model_name'], + temperature=self._config['app']['openai']['temperature'], + max_tokens=self._config['app']['openai']['max_tokens'], + top_p=self._config['app']['openai']['top_p'], + ) + + prompt = ChatPromptTemplate.from_messages([('system', system_template), ('human', user_template)]) + self._chain = prompt | llm + + def run(self): + # Interface + # 1) Page Info + st.set_page_config( + page_title=self._config['app']['page_title'], + page_icon=self._config['app']['page_favicon_path'], + ) + + # 2) Title and description + st.title(body=self._config['app']['page_title']) + st.text(body=self._config['app']['page_description']) + with st.expander('流程說明'): + st.write(""" + 1. 請填入以下所有資料 + 2. 點擊「製作」產生遺囑內容 + 3. 準備紙筆,由立囑人親筆謄寫所有遺囑內容 + 4. 妥善保存紙本遺囑,並將保存地點告訴遺囑執行人 + """) + + # 3) Testator Info + testator_name = st.text_input(label='立遺囑人姓名') + testator_id = st.text_input(label='立遺囑人身分證字號') + executor_name = st.text_input( + label='遺囑執行人姓名', + help='遺囑執行人即遺囑生效後,負責實行遺囑內容各種事項的人(不得為未成年、受監護宣告或輔助宣告之人),也就是您信任的人', + ) + + # 4) Property distribution + prop_dist_type = st.radio( + label='財產分配', + options=['給一人', '給多人'], + horizontal=True, + help='將您名下的財產,例如存款、汽車、房子、股票等,分配給您想給的對象', + ) + prop_dist_content = '' + if prop_dist_type == '給一人': + sole_heir = st.text_input( + '繼承者', help='繼承者可以是家人、朋友或機構(機構請填寫全名,例如:`財團法人伊甸社會福利基金會`)' + ) + if sole_heir: + prop_dist_content = f'本人名下所有之全部財產,由 {sole_heir} 單獨繼承。\n' + else: + props = st.data_editor( + data=pd.DataFrame([{'財產名稱': None, '繼承者': None, '財產說明': None}]), + column_config={ + '財產名稱': st.column_config.SelectboxColumn( + '財產名稱', + options=[ + '存款', + '汽車', + '機車', + '房子', + '土地', + '股票', + '其他', + ], + required=True, + ), + '繼承者': st.column_config.TextColumn( + '繼承者', + help='可以是家人、朋友或機構(機構請填寫全名,例如:`財團法人伊甸社會福利基金會`)', + required=True, + ), + '財產說明': st.column_config.TextColumn( + '財產說明', + help=( + '- 存款請輸入銀行全名,例如:`國泰世華銀行萬華分行`\n' + '- 汽/機車請輸入車號,例如:`ABC-1234`\n' + '- 房子請輸入建號與地號(請確認您的權狀),例如:`建號19998-000地號0427-0013`\n' + '- 土地請輸入地號(請確認您的權狀),例如:`0427-0013`\n' + '- 股票請輸入股票代號,例如:`0050`\n' + '- 其他請輸入財產名稱,例如:`金飾`' + ), + required=True, + ), + }, + num_rows='dynamic', + use_container_width=True, + hide_index=True, + ) + if len(props) > 0: + prop_dist_content = '本人將名下財產分配如下:\n' + for i, row in props.iterrows(): + prop_name = row['財產名稱'] + heir = row['繼承者'] + prop_desc = row['財產說明'] + if prop_name == '存款': + prop_dist_content += f' {i + 1}. 本人於{prop_desc}之所有存款,由 {heir} 單獨繼承。\n' + elif prop_name in ['汽車', '機車']: + prop_dist_content += f' {i + 1}. 本人所有之車號{prop_desc}{prop_name}乙輛,由 {heir} 單獨繼承。\n' + elif prop_name == '房子': + prop_dist_content += f' {i + 1}. 本人所有之房地({prop_desc}),由 {heir} 單獨繼承。\n' + elif prop_name == '土地': + prop_dist_content += f' {i + 1}. 本人所有之土地({prop_desc}),由 {heir} 單獨繼承。\n' + elif prop_name == '股票': + prop_dist_content += f' {i + 1}. 本人所有之股票代號{prop_desc}之所有股票,由 {heir} 單獨繼承\n' + elif prop_name == '其他': + prop_dist_content += f' {i + 1}. 本人所有之{prop_desc},由 {heir} 單獨繼承。\n' + + submit_botton = st.button('製作') + + # Prepare chain + self._prepare_chain() + + # Action + if testator_id: + if not is_valid_taiwan_id(testator_id): + st.warning('請輸入有效的身分證字號') + + if submit_botton or (testator_name and testator_id and executor_name and prop_dist_content): + self._will_content = parse_txt(self._will_template_path).format( + testator_name=testator_name, + testator_id=testator_id, + executor_name=executor_name, + prop_dist_content=prop_dist_content, + year=datetime.now().year - 1911, + month=datetime.now().month, + day=datetime.now().day, + ) + st.code(self._will_content, language=None) + st.download_button( + label='下載', + data=self._will_content, + file_name='遺囑.txt', + mime='text/plain', + icon=':material/download:', + ) + ai_check_button = ai_check_button = st.button('AI校稿', icon=':material/search:') + if ai_check_button: + with st.spinner('校稿中...'): + result = self._chain.stream({'input_text': self._will_content}) + st.write_stream(result) + + else: + st.warning('請填入所有資料') + + +if __name__ == '__main__': + app = EmbraceLifeApp() + app.run() diff --git a/embrace_life/assets/favicon.jpg b/embrace_life/assets/favicon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3786c5d341dcf8649b5819ede1ba8f7c21e8947a GIT binary patch literal 2792 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 WoME^4EQ>5rtRmUszPZcc$o~LEY`LZY literal 0 HcmV?d00001 diff --git a/embrace_life/assets/will_template.txt b/embrace_life/assets/will_template.txt new file mode 100644 index 0000000..c5c67e8 --- /dev/null +++ b/embrace_life/assets/will_template.txt @@ -0,0 +1,11 @@ +遺囑 + +立遺囑人 {testator_name}(身分證字號 {testator_id}),特親筆立遺囑內容如下: + +一、 本人指定 {executor_name} 為本遺囑之遺囑執行人,處理本遺囑之一切事宜。 + +二、 {prop_dist_content} + +立遺囑人: {testator_name} + +中華民國 {year} 年 {month} 月 {day} 日 \ No newline at end of file diff --git a/embrace_life/config.yaml b/embrace_life/config.yaml new file mode 100644 index 0000000..4c3c2e1 --- /dev/null +++ b/embrace_life/config.yaml @@ -0,0 +1,9 @@ +app: + page_title: 自書遺囑製作 + page_description: 這是一封重新擁抱生命的書信,也是對所愛之人的一份溫柔承諾。 + page_favicon_path: ./assets/favicon.jpg + openai: + model_name: gpt-4.1-mini + max_tokens: 1024 + temperature: 0.2 + top_p: 0.9 \ No newline at end of file diff --git a/embrace_life/main.py b/embrace_life/main.py deleted file mode 100644 index 6b27d14..0000000 --- a/embrace_life/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from embrace-life!") - - -if __name__ == "__main__": - main() diff --git a/embrace_life/utils.py b/embrace_life/utils.py new file mode 100644 index 0000000..766968f --- /dev/null +++ b/embrace_life/utils.py @@ -0,0 +1,103 @@ +# utils.py +# +# author: deng +# date : 202501104 + +import re + +import yaml + + +def parse_config(config_path: str) -> dict: + """Config parser + + Args: + config_path (str): path of config yaml. + + Returns: + dict: configuration dictionary + """ + with open(config_path, 'r', encoding='utf-8') as file: + config = yaml.safe_load(file) + return config + + +def parse_txt(txt_path: str) -> str: + """Txt parser + + Args: + txt_path (str): path of txt. + + Returns: + str: content of txt + """ + with open(txt_path, 'r', encoding='utf-8') as file: + content = file.read() + return content + + +def is_valid_taiwan_id(id_str: str) -> bool: + """Check given Taiwan id is valid or not. + + Args: + id_str (str): id + + Returns: + bool: valid or not + """ + if not isinstance(id_str, str): + return False + + id_str = id_str.upper().strip() + + if not re.match(r'^[A-Z][1289]\d{8}$', id_str): + return False + + letter_map = { + 'A': 10, + 'B': 11, + 'C': 12, + 'D': 13, + 'E': 14, + 'F': 15, + 'G': 16, + 'H': 17, + 'I': 34, + 'J': 18, + 'K': 19, + 'L': 20, + 'M': 21, + 'N': 22, + 'O': 35, + 'P': 23, + 'Q': 24, + 'R': 25, + 'S': 26, + 'T': 27, + 'U': 28, + 'V': 29, + 'W': 30, + 'X': 31, + 'Y': 32, + 'Z': 33, + } + + weights = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1] + + letter_num_str = str(letter_map[id_str[0]]) + + all_digits = [int(d) for d in letter_num_str] + [int(d) for d in id_str[1:]] + + total_sum = 0 + for i in range(10): + total_sum += all_digits[i] * weights[i] + + remainder = total_sum % 10 + if remainder == 0: + check_digit = 0 + else: + check_digit = 10 - remainder + + actual_check_digit = all_digits[10] + + return check_digit == actual_check_digit diff --git a/pyproject.toml b/pyproject.toml index 62607dc..62c6e84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.13" dependencies = [ "langchain>=0.3.27", "langchain-openai>=0.3.32", + "pandas>=2.3.2", "streamlit>=1.49.1", ] @@ -25,4 +26,4 @@ target-version = "py313" select = ["E", "F", "I"] [tool.ruff.format] -quote-style = "single" \ No newline at end of file +quote-style = "single" diff --git a/uv.lock b/uv.lock index 55ac5db..fcd2855 100644 --- a/uv.lock +++ b/uv.lock @@ -162,6 +162,7 @@ source = { virtual = "." } dependencies = [ { name = "langchain" }, { name = "langchain-openai" }, + { name = "pandas" }, { name = "streamlit" }, ] @@ -176,6 +177,7 @@ dev = [ requires-dist = [ { name = "langchain", specifier = ">=0.3.27" }, { name = "langchain-openai", specifier = ">=0.3.32" }, + { name = "pandas", specifier = ">=2.3.2" }, { name = "streamlit", specifier = ">=1.49.1" }, ]