mvp
This commit is contained in:
@ -9,11 +9,9 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-docstring-first
|
- id: check-docstring-first
|
||||||
|
|
||||||
- repo: local
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.12.11
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: ruff
|
args: [--fix]
|
||||||
entry: ruff check .
|
- id: ruff-format
|
||||||
language: python
|
|
||||||
types: [python]
|
|
||||||
always_run: true
|
|
||||||
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 = [
|
dependencies = [
|
||||||
"langchain>=0.3.27",
|
"langchain>=0.3.27",
|
||||||
"langchain-openai>=0.3.32",
|
"langchain-openai>=0.3.32",
|
||||||
|
"pandas>=2.3.2",
|
||||||
"streamlit>=1.49.1",
|
"streamlit>=1.49.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -25,4 +26,4 @@ target-version = "py313"
|
|||||||
select = ["E", "F", "I"]
|
select = ["E", "F", "I"]
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "single"
|
quote-style = "single"
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -162,6 +162,7 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "langchain" },
|
{ name = "langchain" },
|
||||||
{ name = "langchain-openai" },
|
{ name = "langchain-openai" },
|
||||||
|
{ name = "pandas" },
|
||||||
{ name = "streamlit" },
|
{ name = "streamlit" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -176,6 +177,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "langchain", specifier = ">=0.3.27" },
|
{ name = "langchain", specifier = ">=0.3.27" },
|
||||||
{ name = "langchain-openai", specifier = ">=0.3.32" },
|
{ name = "langchain-openai", specifier = ">=0.3.32" },
|
||||||
|
{ name = "pandas", specifier = ">=2.3.2" },
|
||||||
{ name = "streamlit", specifier = ">=1.49.1" },
|
{ name = "streamlit", specifier = ">=1.49.1" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user