mvp
This commit is contained in:
@ -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
182
embrace_life/app.py
Normal 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()
|
||||
BIN
embrace_life/assets/favicon.jpg
Normal file
BIN
embrace_life/assets/favicon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
11
embrace_life/assets/will_template.txt
Normal file
11
embrace_life/assets/will_template.txt
Normal 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
9
embrace_life/config.yaml
Normal 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
|
||||
@ -1,6 +0,0 @@
|
||||
def main():
|
||||
print("Hello from embrace-life!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
103
embrace_life/utils.py
Normal file
103
embrace_life/utils.py
Normal 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
|
||||
@ -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
2
uv.lock
generated
@ -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" },
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user