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 0000000..3786c5d Binary files /dev/null and b/embrace_life/assets/favicon.jpg differ 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" }, ]