320 lines
12 KiB
Python
320 lines
12 KiB
Python
import json
|
||
import ast
|
||
from typing import Dict, Any
|
||
import time
|
||
import datetime
|
||
from openai import OpenAI
|
||
from langchain_openai import ChatOpenAI
|
||
|
||
today = f'{datetime.datetime.now().year}{datetime.datetime.now().month}{datetime.datetime.now().day}'
|
||
|
||
'''
|
||
این سورس ، حکمت ها و نامه ها و خطبه های نهج البلاغه
|
||
به همراه پاراگراف هایشان از فایل جیسون به همراه پرامپت به llm مورد نظر میفرسته
|
||
و پاسخ که یک دیکشنری پایتونی ( شامل مفاهیم کلیدی، شخصیت ها، عنوان ، آیدی ، قوانین و ... ) هست
|
||
رو به صورت جیسون ذخیره میکنه
|
||
'''
|
||
|
||
|
||
SYSTEM_PROMPT = """
|
||
تو یک استخراجگر ساختاریافته اطلاعات برای متون فارسی هستی.
|
||
وظیفه تو تحلیل هر پاراگراف و تولید خروجی دقیق بر اساس تعاریف زیر است.
|
||
|
||
فقط و فقط بر اساس متن ورودی عمل کن و هیچ دانش، تفسیر یا مفهوم خارجی اضافه نکن.
|
||
|
||
ساختار ورودی
|
||
|
||
هر ورودی شامل موارد زیر است:
|
||
|
||
id : شناسه متن
|
||
paragraphs : لیستی از پاراگرافها که هرکدام شامل:
|
||
paragraph_id
|
||
text
|
||
|
||
قوانین بنیادین سختگیرانه:
|
||
|
||
خروجی باید فقط یک دیکشنری معتبر پایتون باشد.
|
||
|
||
هیچ توضیح، مقدمه یا متن اضافی تولید نکن.
|
||
|
||
تمام مقادیر باید به زبان فارسی باشند.
|
||
|
||
اگر دادهای در متن وجود نداشت، مقدار آن را [] یا None قرار بده.
|
||
|
||
تعاریف عملیاتی استخراج:
|
||
1. title
|
||
|
||
یک رشته بین 4 تا 7 کلمه
|
||
|
||
فقط با واژگان موجود در متن ساخته شود
|
||
|
||
جهتگیری، تنش یا دوراهی اصلی متن را نشان دهد
|
||
|
||
2. central_concepts
|
||
|
||
شامل مفاهیم اصلی، محوری و بسیار مهم هر پاراگراف باشد
|
||
|
||
تعداد آن محدود و فقط شامل مفاهیم با اهمیت بالا باشد
|
||
|
||
هر مفهوم دقیقاً دو کلمهای باشد
|
||
|
||
اسامی خاص و اشخاص به هیچ وجه به عنوان کلیدواژه انتخاب نشوند
|
||
|
||
کلیدواژه ها مستقیماً از متن استخراج شود
|
||
|
||
فقط در قالب:
|
||
|
||
مضاف و مضافالیه
|
||
|
||
صفت و موصوف
|
||
|
||
بدون استفاده از حروف عطف(هرگز هرگز هرگز کلمات کلیدواژه با حرف «و» به هم عطف نشوند(کاملا سختگیرانه))
|
||
|
||
paragraph_effect
|
||
|
||
برای هر مفهوم مرکزی، احساس متن نسبت به آن باید به صورت طیفی عددی مشخص شود:
|
||
|
||
یک عدد اعشاری یا صحیح در بازه -1 تا +1
|
||
|
||
+1 → بیشترین میزان تقویت
|
||
|
||
-1 → بیشترین میزان تضعیف
|
||
|
||
هرچه عدد به +1 نزدیکتر باشد، مفهوم بیشتر تقویت شده است
|
||
|
||
هرچه عدد به -1 نزدیکتر باشد، مفهوم بیشتر تضعیف شده است
|
||
|
||
مقادیر بین این دو (مثلاً 0.2 ، -0.4 ، 0.75) مجاز و نشاندهنده شدت نسبی هستند
|
||
|
||
3. persons
|
||
|
||
فقط شخصیتهای کاملاً حقیقی (افراد واقعی)
|
||
|
||
نام باید صریحاً و دقیقاً در متن آمده باشد
|
||
|
||
اگر هیچ شخصیت حقیقی وجود نداشت، مقدار آن [] باشد
|
||
|
||
شخصیتهای فرضی، نمادین یا کلی وارد نشوند
|
||
|
||
4. rules
|
||
|
||
فقط قواعد بسیار مهم و محوری متن استخراج شوند
|
||
|
||
تعداد قواعد محدود باشد
|
||
|
||
هر قاعده یک جمله کوتاه، مستقل و انتزاعی باشد
|
||
|
||
انواع قواعد:
|
||
|
||
قاعده توصیفی
|
||
|
||
بیان رابطه بین دو مفهوم (موضوع + محمول)
|
||
|
||
قاعده هنجاری
|
||
|
||
بیانگر الزام، بایستگی یا ضرورت
|
||
|
||
معمولاً شامل واژگانی مانند:
|
||
باید، لازم است، ضروری است، نیازمند است، واجب است، حیاتی است و مانند آن
|
||
|
||
5. paragraph_type
|
||
برای هر پاراگراف یکی از موارد زیر را انتخاب کن (مرتبطترین گزینه):
|
||
خطبه: طلیعه سخن (بسم الله و الحمدلله، خوش آمدگویی، تبریک، تسلیت و ...)
|
||
اشاره یا مقدمه: (اشاره و توضیحی در رابطه با محتوای بحث و مناسبت آن)
|
||
تیتر: اگر این پاراگراف یک تیتر یا زیرتیتر یا سوتیتر باشد
|
||
شعر: محتوای پاراگراف یک مصرع یا بیت شعری است
|
||
آیه: اگر پاراگراف یک آیه از قرآن باشد
|
||
حدیث: اگر پاراگراف یک روایت یا حدیث از معصومین باشد
|
||
ارجاع: پاراگراف ارجاع به منابع و پاورقی بخشی از متن است
|
||
بدنه: اگر از انواع بالا نباشد
|
||
|
||
|
||
ساختار دقیق خروجی مورد انتظار باید لیستی از دیکشنری ها باشد که بازای هر پاراگراف تولید شده باشد و به صورت زیر باشد:
|
||
{
|
||
"paragraph_id": str,
|
||
"title": str,
|
||
"central_concepts": [
|
||
{
|
||
"concept": str,
|
||
"paragraph_effect": float
|
||
}
|
||
],
|
||
"paragraph_type": str,
|
||
"persons": [str],
|
||
"rules": [
|
||
{
|
||
"rule": str,
|
||
"type": "توصیفی" | "هنجاری"
|
||
}
|
||
]
|
||
}
|
||
"""
|
||
|
||
USER_PROMPT = '''
|
||
متن زیر را بر اساس دستورالعملهای سیستمی تحلیل کن و خروجی را در قالب دیکشنری پایتون ارائه بده:
|
||
|
||
## ساختار جیسون برای تحلیل:
|
||
'''
|
||
|
||
def get_key():
|
||
key = 'aa-Fu5oeQv8jx8NCWV39WenJ7Yy1mbcFJ4P20CLQURkql2Eleta' # nahj key
|
||
return key
|
||
|
||
def get_client():
|
||
url = "https://api.avalapis.ir/v1" #"https://api.avalai.ir/v1"
|
||
client = OpenAI(
|
||
api_key=get_key(),
|
||
base_url=url,
|
||
)
|
||
return client
|
||
|
||
def llm_request(text, model="gemini-2.5-flash-lite"):
|
||
# print(f'using model: {model}')
|
||
|
||
try:
|
||
messages = [
|
||
{"role": "system", "content": SYSTEM_PROMPT},
|
||
{"role": "user", "content": f"{USER_PROMPT}\n{text}"}]
|
||
|
||
response = client.chat.completions.create(
|
||
messages=messages,
|
||
model=model,
|
||
)
|
||
answer = response.choices[0].message.content
|
||
# messages.append({"role": "assistant", "content": answer})
|
||
except Exception as error:
|
||
with open(llm_error_path, mode='a+', encoding='utf-8') as file:
|
||
error_message = f'\n\ntext: {str(text)}\nerror:{error} \n-------------------------------\n'
|
||
file.write(error_message)
|
||
return 'Ooops ... Error!'
|
||
return answer
|
||
|
||
def text_to_dict(text: str) -> Dict[str, Any]:
|
||
text = text.replace('\n','')
|
||
text = text.lstrip('```json')
|
||
text = text.lstrip('json')
|
||
text = text.lstrip('```python')
|
||
text = text.rstrip('```')
|
||
text = text.strip()
|
||
try:
|
||
return json.loads(text)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return ast.literal_eval(text)
|
||
|
||
|
||
client = get_client()
|
||
models = [ "gemini-2.5-flash-lite", "gpt-4o-mini","deepseek-reasoner"]
|
||
|
||
date = str((datetime.datetime.now())).replace(' ','-').replace(':','').replace('.','-')
|
||
|
||
def find_passed_data_ids(output_metadata_jsonl_path):
|
||
passed_data_ids = []
|
||
with open(output_metadata_jsonl_path, 'r', encoding='utf-8') as file:
|
||
passed_data = file.readlines()
|
||
for pd in passed_data:
|
||
passed_data_ids.append(str(json.loads(pd)['id']))
|
||
return passed_data_ids
|
||
|
||
if __name__ == "__main__":
|
||
|
||
input_data_path = './nahj_data/all_nahj_CONTEXT.json' # شامل تمامی (خطبه و نامه و حکمت ها) به همراه پاراگراف هایشان
|
||
llm_error_path = './nahj-answer/error-in-getting-metadata-Final.txt'
|
||
previous_peroid_errors_path = "./nahj_data/error-ids-Final.txt"
|
||
current_peroid_errors_path = "./nahj_data/error-ids3-Final.txt"
|
||
output_metadata_jsonl_path = './nahj_data/nahj-metadata-jsonline.json'
|
||
output_metadata_json_path = './nahj_data/nahj-metadata.json'
|
||
|
||
with open(input_data_path, 'r', encoding='utf-8') as file:
|
||
data = json.load(file)
|
||
|
||
passed_data_ids = []
|
||
passed_data_ids = find_passed_data_ids(output_metadata_jsonl_path)
|
||
|
||
failed_ids = []
|
||
with open(previous_peroid_errors_path, "r", encoding="utf-8") as f:
|
||
failed = f.read()
|
||
failed_ids = failed.splitlines()
|
||
start = (datetime.datetime.now())
|
||
print(f'start: {start}')
|
||
# len_pars = 0
|
||
# for item in data:
|
||
# len_pars += len(item['paragraphs'])
|
||
|
||
error_ids = []
|
||
test_enteries = []
|
||
all_paragraphs = 0
|
||
|
||
period = 1
|
||
end = False
|
||
while True:
|
||
|
||
print(f"******* PERIOD :: {period} *******")
|
||
for index ,entery in enumerate(data, 1):
|
||
|
||
if index > 10:
|
||
end = True
|
||
break
|
||
id = entery['id']
|
||
|
||
# خارج کردن داده هایی که قبلا کرول شده
|
||
if str(id) in passed_data_ids:
|
||
continue
|
||
|
||
# برای دور دوم به بعد که برخی از شناسه ها به خطر خورده ، شرط زیر از کامنت خارج شود
|
||
# if not str(id) in failed_ids:
|
||
# continue
|
||
|
||
# if not id == 27793
|
||
# continue
|
||
|
||
print(f'id: {id} - record: {index}/{len(data)} - period: {period}')
|
||
|
||
llm_answer_data = ''
|
||
new_entry = {}
|
||
new_paragraphs = []
|
||
new_entry['id'] = id
|
||
# new_entry['keywords'] = entery['keywords']
|
||
paragraphs = entery["paragraphs"]
|
||
for p in paragraphs:
|
||
large_title = p['large_title']
|
||
new_paragraphs.append({
|
||
'paragraph_id': p['paragraph_id'],
|
||
'text': p['text']
|
||
# 'text': f"بخشی از {large_title} : {p['text'] }"
|
||
})
|
||
new_entry['paragraphs'] = new_paragraphs
|
||
try:
|
||
result_data = llm_request(new_entry)#gpt-4o
|
||
llm_answer_data = text_to_dict(result_data)
|
||
except Exception as e:
|
||
print(f'error id: {id} - {e} >> llm result: {result_data}')
|
||
# error_ids.append(id)
|
||
with open(current_peroid_errors_path, "a", encoding="utf-8") as f:
|
||
f.write(f"{id}\n")
|
||
continue
|
||
entery['paragraph_metadata'] = llm_answer_data
|
||
test_enteries.append(entery)
|
||
with open(output_metadata_jsonl_path, 'a', encoding='utf-8') as f:
|
||
json.dump(entery, f, ensure_ascii=False)
|
||
f.write('\n')
|
||
|
||
time.sleep(1)
|
||
|
||
passed_data_ids = find_passed_data_ids(output_metadata_jsonl_path)
|
||
if len(data) == len(passed_data_ids):
|
||
print('ALL DATA PASSED OK!')
|
||
break
|
||
print(f'##### period result: passd {len(passed_data_ids)}/{len(data)} #####')
|
||
period+= 1
|
||
if end == True:
|
||
break
|
||
# with open(f'./leader_data/leader-metadata-bayanat-{id}.json', mode='w', encoding='utf-8') as file:
|
||
with open(output_metadata_json_path, mode='w', encoding='utf-8') as file:
|
||
result_message = json.dump(test_enteries, file, ensure_ascii=False, indent=2)
|
||
print('all done!')
|
||
|
||
print('---------------------------------------------')
|
||
print(f'full duration: {(datetime.datetime.now() - start).total_seconds()}')
|
||
print(f'all_paragraphs: {all_paragraphs}')
|
||
print('---------------------------------------------')
|