This commit is contained in:
deng
2025-11-04 23:29:20 +08:00
parent 4ff430d2c3
commit addc09e9db
9 changed files with 313 additions and 13 deletions

View File

@ -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
args: [--fix]
- id: ruff-format

182
embrace_life/app.py Normal file
View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,11 @@
遺囑
立遺囑人 {testator_name}(身分證字號 {testator_id}),特親筆立遺囑內容如下:
一、 本人指定 {executor_name} 為本遺囑之遺囑執行人,處理本遺囑之一切事宜。
二、 {prop_dist_content}
立遺囑人: {testator_name}
中華民國 {year} 年 {month} 月 {day} 日

9
embrace_life/config.yaml Normal file
View File

@ -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

View File

@ -1,6 +0,0 @@
def main():
print("Hello from embrace-life!")
if __name__ == "__main__":
main()

103
embrace_life/utils.py Normal file
View File

@ -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

View File

@ -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"
quote-style = "single"

2
uv.lock generated
View File

@ -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" },
]