This commit is contained in:
ajokar 2025-11-29 20:11:27 +00:00
parent 713776f6b0
commit d7ee4bdea0
37 changed files with 1226 additions and 18638 deletions

View File

@ -1,2 +1,3 @@
./qavanin-faiss
./llm-answer
./llm-answer
./data

8
.env Executable file
View File

@ -0,0 +1,8 @@
api_key="aa-fdh9d847ANcBxQCBTZD5hrrAdl0UrPEnJOScYmOncrkagYPf"
aval_ai_key = 'aa-4tvAEazUBovEN1i7i7tdl1PR93OaWXs6hMflR4oQbIIA4K7Z'
aval_ai_url = "https://api.avalai.ir/v1"
rerank_batch_size ="256"
es_url = "https://192.168.23.160:9200"
es_username = ""
es_index_name = ""
es_password = ""

3
.gitignore vendored Normal file → Executable file
View File

@ -1,5 +1,4 @@
__pycache__/
qavanin-faiss/faiss_index_qavanin_285k_metadata.json
qavanin-faiss/faiss_index_qavanin_285k.index
data/
.vscode
.gitignore

0
baleqabot/bot.log → __init__.py Normal file → Executable file
View File

View File

@ -1,155 +0,0 @@
print(f'import bale madule ...')
import asyncio
import json
from fastapi import FastAPI, Request
from pydantic import BaseModel
import requests
import logging
import uvicorn
import chatbot_handler as ch
# ===========================
# پیکربندی اولیه
# ===========================
TOKEN = '2052165365:Tt7u2qXB9oRTPISeZ0wmoAGpPsIgjq-vAxM'
API_URL = f"https://tapi.bale.ai/bot{TOKEN}/"
# راه‌اندازی لاگر
logging.basicConfig(
filename="./baleqabot/bot.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
# ===========================
# define import model class
# ===========================
class Message(BaseModel):
chat: dict
text: str | None = None
class Update(BaseModel):
message: Message | None = None
# ===========================
# کلاس اصلی ربات بله
# ===========================
class BaleBot:
def __init__(self, token: str):
# self.api_url = f"https://api.bale.ai/bot{token}/"
self.api_url = API_URL
async def get_updates(self, offset = None):
params = {"timeout": 20}
if offset:
params["offset"] = offset
resp = requests.get(f"{self.api_url}getUpdates", params=params)
return resp.json().get("result", [])
async def send_message(self, chat_id: int, text: str, keyboard=None):
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": "HTML"
}
if keyboard:
payload["reply_markup"] = keyboard
try:
response = requests.post(self.api_url + "sendMessage", json=payload)
return response.json()
except Exception as e:
logging.error(f"Error sending message: {e}")
return None
async def get_latest_req_id(self):
latest_req_id = 0
with open('./baleqabot/requests.json', 'r', encoding='utf-8') as file:
prev_reqs = json.load(file)
if prev_reqs:
latest_req_id = prev_reqs[-1]['update_id']
return latest_req_id + 1
async def save_entery(self, update):
all_reqs = []
with open('./baleqabot/requests.json', 'r', encoding='utf-8') as file:
prev_reqs = json.load(file)
all_reqs.extend(prev_reqs)
all_reqs.extend(update)
with open('./baleqabot/requests.json', 'w', encoding='utf-8') as file:
data = json.dumps(all_reqs, ensure_ascii=False, indent=2)
file.write(data)
return True
async def handle_update(self, update_reqs: dict):
data = update_reqs[0]
if "message" not in data:
return
message = data["message"]
chat_id = message["chat"]["id"]
text = message.get("text", "").strip()
logging.info(f"Received message from {chat_id}: {text}")
if text == "/start":
reply = "سلام، من دستیار هوشمند قوانین هستم. لطفا پرسش خود را وارد نمائید ..."
# keyboard = {
# "keyboard": [["/help", "/status"]],
# "resize_keyboard": True,
# "one_time_keyboard": True
# }
self.send_message(chat_id, reply)
elif text == "/chat":
reply = "لطفا متن پرسش از قوانین را وارد نمائید ..."
self.send_message(chat_id, reply)
# elif text == "/help":
# reply = (
# "دستورهای موجود:\n"
# "/start - شروع ربات\n"
# "/chat - گفت‌گو با دستیار هوشمند قانون\n"
# "/status - وضعیت ربات"
# )
# self.send_message(chat_id, reply)
elif text == "/status":
reply = "ربات فعال است ✅"
self.send_message(chat_id, reply)
else:
answer = await chat.run_chatbot(text, chat.create_chat_id())
if answer:
reply = answer
else:
reply = 'خطا در تولید پاسخ!'
self.send_message(chat_id, reply)
async def bale_main():
print(f'bale-qabot is Readey!!!')
while True:
last_req_id = bale_bot.get_latest_req_id()
update = bale_bot.get_updates(last_req_id)
if update:
bale_bot.save_entery(update)
bale_bot.handle_update(update)
print('ok')
# ===========================
# ساخت اپلیکیشن FastAPI
# ===========================
# app = FastAPI()
bale_bot = BaleBot(TOKEN)
result = asyncio.run(bale_main())
# ===========================
# (local execution)
# ===========================
if __name__ == "__main__":
# uvicorn.run("chatbot:app", host="https://bale.tavasi.ir/", port=5000)
bale_main()

View File

@ -1,443 +0,0 @@
[
{
"update_id": 1,
"message": {
"message_id": 1,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761831331,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 2,
"message": {
"message_id": 2,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761833932,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 3,
"message": {
"message_id": 3,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761836593,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 4,
"message": {
"message_id": 4,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761836694,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 5,
"message": {
"message_id": 5,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761836899,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 6,
"message": {
"message_id": 6,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761836915,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 7,
"message": {
"message_id": 7,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1761837001,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 8,
"message": {
"message_id": 8,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762099430,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 9,
"message": {
"message_id": 9,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762099450,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 10,
"message": {
"message_id": 10,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762100301,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "سلام"
}
},
{
"update_id": 11,
"message": {
"message_id": 11,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762100357,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 12,
"message": {
"message_id": 12,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762100360,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "سلام"
}
},
{
"update_id": 13,
"message": {
"message_id": 13,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762100364,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "رلیردت"
}
},
{
"update_id": 14,
"message": {
"message_id": 14,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762179038,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 26,
"message": {
"message_id": 95,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762181681,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "تست"
}
},
{
"update_id": 27,
"message": {
"message_id": 97,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762182073,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "/start",
"entities": [
{
"type": "bot_command",
"offset": 0,
"length": 6
}
]
}
},
{
"update_id": 28,
"message": {
"message_id": 99,
"from": {
"id": 899452608,
"is_bot": false,
"first_name": "جوکار",
"last_name": "",
"username": "nasle_sevvom"
},
"date": 1762182086,
"chat": {
"id": 899452608,
"type": "private",
"username": "nasle_sevvom",
"first_name": "جوکار"
},
"text": "سلام"
}
}
]

View File

@ -1,115 +0,0 @@
import json
import chatbot_handler as chat
# import bale_qabot
import os
import numpy as np
import torch
import faiss
from typing import List, Tuple
from sentence_transformers import SentenceTransformer
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import datetime
import re
import random
from fastapi.middleware.cors import CORSMiddleware
from embedder_sbert_qavanin_285k import PersianVectorAnalyzer
#from normalizer import cleaning
from fastapi import FastAPI ,Header
from pydantic import BaseModel
# LLM Libs
from openai import OpenAI
from langchain_openai import ChatOpenAI # pip install -U langchain_openai
import requests
from FlagEmbedding import FlagReranker # deldar-reranker-v2
import aiofiles
chatbot = FastAPI()
origins = ["*"]
chatbot.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
print('#'*19)
print('-Chatbot is Ready-')
print('#'*19)
# تعریف مدل داده‌ها برای درخواست‌های API
class Query(BaseModel):
query: str
# مسیر API برای اجرا کردن run_chatbot
@chatbot.get("/")
async def simple():
return "ai rag caht qanon OK"
@chatbot.get("/ping")
async def ping():
return "ai rag caht qanon OK"
@chatbot.post("/emergency_call")
async def emergency_call(query: Query):
print('emergency generate answer ...')
chat_id = await chat.create_chat_id()
answer = await chat.ask_chatbot_avalai(query.query, chat_id)
await chat.credit_refresh()
return {"answer": answer}
@chatbot.post("/run_chat")
async def run_chat(query: Query):
print('regular generate answer ...')
chat_id = await chat.create_chat_id()
answer = await chat.ask_chatbot(query.query, chat_id)
await chat.credit_refresh()
return {"answer": answer}
# uvicorn src.app:app --reload
if __name__ == "__main__":
# query = 'در قانون حمایت از خانواده و جوانی جمعیت چه خدماتی در نظر گرفته شده است؟'
while True:
query = input('enter your qustion:')
if query == '':
print('لطفا متن سوال را وارد نمائید')
continue
start = (datetime.datetime.now())
# result = test_dataset()
result = chat.single_query(query)
end_retrive = datetime.datetime.now()
print('-'*40)
print(f'retrive duration: {(end_retrive - start).total_seconds()}')
prompt = f'برای پرسش "{query}" از میان مواد قانونی "{result}" .پاسخ مناسب و دقیق را استخراج کن. درصورتی که مطلبی مرتبط با پرسش در متن پیدا نشد، فقط پاسخ بده: "متاسفانه در منابع، پاسخی پیدا نشد!"'
llm_answer = chat.llm_request(prompt)
print('-'*40)
print(f'llm duration: {(datetime.datetime.now() - end_retrive).total_seconds()}')
refrences = ''
recognized_refrences = chat.find_refrences(llm_answer)
llm_answer = chat.replace_refrences(llm_answer, recognized_refrences)
with open('./llm-answer/result.txt', mode='a+', encoding='utf-8') as file:
result_message = f'متن پرامپت: {query.strip()}\n\nپاسخ: {llm_answer} \n----------------------------------------------------------\n'
file.write(result_message)
with open('./llm-answer/passages.txt', mode='a+', encoding='utf-8') as file:
result_message = f'متن پرامپت: {query.strip()}\n\مواد مشابه: {result} \n----------------------------------------------------------\n'
file.write(result_message)
print('----------------------------------------------------------')
print(f'full duration: {(datetime.datetime.now() - start).total_seconds()}')
print('----------------------------------------------------------')
print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')

View File

@ -1,949 +0,0 @@
import json
import os
import numpy as np
import torch
import faiss
from typing import List, Tuple
from sentence_transformers import SentenceTransformer
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import datetime
import re
import random
from fastapi.middleware.cors import CORSMiddleware
from embedder_sbert_qavanin_285k import PersianVectorAnalyzer
# from normalizer import cleaning
from fastapi import FastAPI ,Header
from pydantic import BaseModel
# LLM Libs
from openai import OpenAI
from langchain_openai import ChatOpenAI # pip install -U langchain_openai
import requests
# from FlagEmbedding import FlagReranker # deldar-reranker-v2
import aiofiles
import oss
# chatbot = FastAPI()
# origins = ["*"]
# chatbot.add_middleware(
# CORSMiddleware,
# allow_origins=origins,
# allow_credentials=True,
# allow_methods=["*"],
# allow_headers=["*"],
# )
# -------------------
# مدل‌ها و مسیر دادهsrc/app/qavanin-faiss/faiss_index_qavanin_285k_metadata.json
# -------------------/src/app/qavanin-faiss
EMBED_MODEL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
RERANKER_MODEL = "BAAI/bge-reranker-v2-m3"
FAISS_INDEX_PATH = "/src/app/qavanin-faiss/faiss_index_qavanin_285k.index"
FAISS_METADATA_PATH = "/src/app/qavanin-faiss/faiss_index_qavanin_285k_metadata.json"
RERANK_BATCH = int(os.environ.get("RERANK_BATCH", 256))
# print(f'RERANK_BATCH: {RERANK_BATCH}')
determine_refrence = '''شناسه هر ماده قانونی در ابتدای آن و با فرمت "id: {idvalue}" آمده است که id-value همان شناسه ماده است. بازای هربخش از پاسخی که تولید می شود، ضروری است شناسه ماده ای که در ایجاد پاسخ از آن استفاده شده، در انتهای پاراگراف یا جمله مربوطه با فرمت {idvalue} اضافه شود. همیشه idvalue با رشته "qs" شروع می شود'''
messages = [
# {
# "role": "system",
# "content": "تو یک دستیار خبره در زمینه حقوق و قوانین مرتبط به آن هستی و می توانی متون حقوقی را به صورت دقیق توضیح بدهی . پاسخ ها باید الزاما به زبان فارسی باشد. پاسخ ها فقط از متون قانونی که در پرامپت وجود دارد استخراج شود. پاسخ تولید شده باید کاملا ساده و بدون هیچ مارک داون یا علائم افزوده ای باشد. لحن متن باید رسمی باشد.",
# },
{"role": "developer", "content": determine_refrence},
]
models = ["gpt-4o-mini" ,"gemini-2.5-flash-lite", "deepseek-chat"]
normalizer_obj = PersianVectorAnalyzer()
pipe = None
content_list, ids, prefix_list, faiss_index = [], [], [], []
async def get_key():
key = 'aa-fdh9d847ANcBxQCBTZD5hrrAdl0UrPEnJOScYmOncrkagYPf'
return key
def load_faiss_index(index_path: str, metadata_path: str):
"""بارگذاری ایندکس FAISS و متادیتا (لیست جملات + عناوین)."""
index = faiss.read_index(index_path)
with open(metadata_path, "r", encoding="utf-8") as f:
metadata = json.load(f)
content_list, ids, prefix_list = [], [], []
for item in metadata:
content_list.append(item["content"])
ids.append(item["id"])
prefix_list.append(item["prefix"])
return content_list, ids, prefix_list, index
async def get_client():
url = "https://api.avalai.ir/v1"
# key = 'aa-4tvAEazUBovEN1i7i7tdl1PR93OaWXs6hMflR4oQbIIA4K7Z'
client = OpenAI(
api_key=await get_key(),
base_url=url, # آدرس پایه
)
return client
async def llm_base_request(system_prompt, user_prompt):
client = await get_client() # فرض می‌کنیم get_client یک متد async است
base_messages = []
try:
if system_prompt:
base_messages.append({
"role": "system",
"content": system_prompt
})
base_messages.append({
"role": "user",
"content": user_prompt
})
for model in models:
response = client.chat.completions.create( # متد create به صورت async فراخوانی می‌شود
messages=base_messages,
model=model
)
answer = response.choices[0].message.content
cost = response.estimated_cost['irt']
break
except Exception as error:
# برای مدیریت خطاها، می‌توانید فایل‌نویسی را به صورت async انجام دهید (در صورت نیاز)
async with aiofiles.open('./llm-answer/error-in-llm.txt', mode='a+', encoding='utf-8') as file:
error_message = f'\n\nquery: {user_prompt.strip()}\nerror:{error} \n------------------------------\n'
await file.write(error_message) # فایل‌نویسی async
return '', 0
return answer, cost
def llm_base_request2(system_prompt, user_prompt):
client = get_client()
base_messages = []
try:
if system_prompt:
base_messages.append(system_prompt)
base_messages.append({
"role": "user",
"content": user_prompt
})
for model in models:
response = client.chat.completions.create(
messages = base_messages,
model= model)
answer = response.choices[0].message.content
cost = response.estimated_cost['irt']
break
except Exception as error:
with open('./llm-answer/error-in-llm.txt', mode='a+', encoding='utf-8') as file:
error_message = f'\n\nquery: {query.strip()}\nerror:{error} \n-------------------------------\n'
file.write(error_message)
return '', 0
return answer, cost
async def oss_base_request(sys_prompt, user_prompt):
base_messages = []
try:
if sys_prompt:
base_messages.append({
"role": "system",
"content": sys_prompt
})
base_messages.append({
"role": "user",
"content": user_prompt
})
response = await oss.process_item(base_messages, reasoning_effort='low', temperature=0.1, max_tokens=40)
if response[0]:
answer = response[1]
else:
answer = ''
cost = 0
except Exception as error:
# برای مدیریت خطاها، می‌توانید فایل‌نویسی را به صورت async انجام دهید (در صورت نیاز)
async with aiofiles.open('./llm-answer/error-in-llm.txt', mode='a+', encoding='utf-8') as file:
error_message = f'\n\nquery: {user_prompt.strip()}\nerror:{error} \n------------------------------\n'
await file.write(error_message) # فایل‌نویسی async
return '', 0
return answer, cost
async def oss_request(query):
if query == '':
return 'لطفا متن سوال را وارد نمائید', 0
try:
messages.append({"role": "user", "content": query})
print(f'final prompt request attempt')
response = await oss.process_item(messages= messages, reasoning_effort='low') # reasoning_effort='high'
print(response)
if response[0]:
answer = response[1]
else:
answer = 'متاسفانه پاسخی دریافت نشد'
cost_prompt = 0
# پاسخ را هم به سابقه اضافه می‌کنیم
# messages.append({"role": "assistant", "content": answer})
response_dict = {}
response_dict['output'] = str(response)
async with aiofiles. open('./llm-answer/messages.json', mode='w', encoding='utf-8') as output:
await output.write(json.dumps(response_dict, ensure_ascii=False, indent=2))
print('response created')
async with aiofiles.open('./llm-answer/chat-objs.txt', mode='a+', encoding='utf-8') as file:
response_value = '0'
await file.write(response_value) # estimated_cost
except Exception as error:
print(f'error-in-llm.txt writing ...')
async with aiofiles.open('./llm-answer/error-in-llm.txt', mode='a+', encoding='utf-8') as file:
error_message = f'\n\nquery: {query.strip()}\nerror:{error} \n-------------------------------\n'
await file.write(error_message)
return 'با عرض پوزش؛ متاسفانه خطایی رخ داده است. لطفا لحظاتی دیگر دوباره تلاش نمائید', 0
print('================')
print(f'len messages: {len(messages)}')
print('================')
return answer, cost_prompt
async def llm_request(query, model):
if query == '':
return 'لطفا متن سوال را وارد نمائید', 0
client = await get_client()
try:
messages.append({"role": "user", "content": query})
response = client.chat.completions.create(
messages = messages,
model = model,
temperature = 0.3) # "gpt-4o", "gpt-4o-mini", "deepseek-chat" , "gemini-2.0-flash", gemini-2.5-flash-lite
# gpt-4o : 500
# gpt-4o-mini : 34
# deepseek-chat: : 150
# gemini-2.0-flash : error
# cf.gemma-3-12b-it : 1
# gemini-2.5-flash-lite : 35 خیلی خوب
answer = response.choices[0].message.content
print('$'*50)
print(f'answer: {answer}')
print('$'*50)
cost_prompt = response.estimated_cost['irt']
print('$'*50)
print(f'answer: {cost_prompt}')
print('$'*50)
# پاسخ را هم به سابقه اضافه می‌کنیم
# messages.append({"role": "assistant", "content": answer})
# print(f'type(response): {type(response)}')
# print(f'response: {response}')
response_dict = {}
response_dict['output'] = str(response)
async with aiofiles. open('./llm-answer/messages.json', mode='w', encoding='utf-8') as output:
await output.write(json.dumps(response_dict, ensure_ascii=False, indent=2))
print('response created')
async with aiofiles.open('./llm-answer/chat-objs.txt', mode='a+', encoding='utf-8') as file:
response_value = f"{response.estimated_cost['irt']}\n-------------------------------\n\n"
await file.write(response_value) # estimated_cost
except Exception as error:
print(f'error-in-llm.txt writing ...')
async with aiofiles.open('./llm-answer/error-in-llm.txt', mode='a+', encoding='utf-8') as file:
error_message = f'\n\nquery: {query.strip()}\nerror:{error} \n-------------------------------\n'
await file.write(error_message)
return 'با عرض پوزش؛ متاسفانه خطایی رخ داده است. لطفا لحظاتی دیگر دوباره تلاش نمائید', 0
print('================')
print(f'len messages: {len(messages)}')
print('================')
return answer, cost_prompt
class HybridRetrieverReranker:
__slots__ = (
"device", "content_list", "ids", "prefix_list", "N", "embedder", "faiss_index",
"vectorizer", "tfidf_matrix", "tokenizer", "reranker", "dense_alpha"
)
def __init__(self, content_list: List[str],ids: List[str], prefix_list: List[str], faiss_index,
dense_alpha: float = 0.6, device: str = None):
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
self.device = device
self.content_list = content_list
self.ids = ids
self.prefix_list = prefix_list
self.faiss_index = faiss_index
self.N = len(content_list)
# Dense
self.embedder = SentenceTransformer(EMBED_MODEL,cache_folder='/src/MODELS', device=self.device)
#self.embedder = SentenceTransformer(EMBED_MODEL, device=self.device)
# Sparse (مثل قبل برای حفظ خروجی)
self.vectorizer = TfidfVectorizer(
analyzer="word",
ngram_range=(1, 2),
token_pattern=r"(?u)\b[\w\u0600-\u06FF]{2,}\b",
)
self.tfidf_matrix = self.vectorizer.fit_transform(self.content_list)
# Reranker
self.tokenizer = AutoTokenizer.from_pretrained(RERANKER_MODEL,cache_dir='/src/MODELS', use_fast=True)
# self.reranker = FlagReranker(RERANKER_MODEL,cache_dir="/src/MODELS", use_fp16=True)
self.reranker = AutoModelForSequenceClassification.from_pretrained(
RERANKER_MODEL
).to(self.device)
self.dense_alpha = float(dense_alpha)
# --- Dense (FAISS) ---
def dense_retrieve(self, query: str, top_k: int):
if top_k <= 0:
return [], np.array([], dtype=np.float32)
q_emb = self.embedder.encode(query, convert_to_numpy=True).astype(np.float32)
D, I = self.faiss_index.search(np.expand_dims(q_emb, axis=0), top_k)
return I[0].tolist(), D[0]
# --- Sparse ---
def sparse_retrieve(self, query: str, top_k: int):
if top_k <= 0:
return [], np.array([], dtype=np.float32)
k = min(top_k, self.N)
q_vec = self.vectorizer.transform([query])
sims = cosine_similarity(q_vec, self.tfidf_matrix).ravel()
idx = np.argpartition(-sims, kth=k-1)[:k]
idx = idx[np.argsort(-sims[idx], kind="mergesort")]
return idx.tolist(), sims[idx]
# --- Utils ---
@staticmethod
def _minmax_norm(arr: np.ndarray) -> np.ndarray:
if arr.size == 0:
return arr
a_min = arr.min()
a_max = arr.max()
rng = a_max - a_min
if rng < 1e-12:
return np.zeros_like(arr)
return (arr - a_min) / rng
def fuse(self, d_idx, d_scores, s_idx, s_scores, top_k=50, k_rrf=60):
"""
ادغام نتایج دو retriever (dense و sparse) با استفاده از Reciprocal Rank Fusion (RRF)
Args:
d_idx (list or np.ndarray): ایندکسهای نتایج dense retriever
d_scores (list or np.ndarray): نمرات dense retriever
s_idx (list or np.ndarray): ایندکسهای نتایج sparse retriever
s_scores (list or np.ndarray): نمرات sparse retriever
top_k (int): تعداد نتایج نهایی
k_rrf (int): ثابت در فرمول RRF برای کاهش تأثیر رتبههای پایینتر
Returns:
list: لیست ایندکسهای ادغامشده به ترتیب نمره
"""
combined = {}
# dense retriever
for rank, idx in enumerate(d_idx):
score = 1.0 / (k_rrf + rank)
combined[idx] = combined.get(idx, 0) + score
# sparse retriever
for rank, idx in enumerate(s_idx):
score = 1.0 / (k_rrf + rank)
combined[idx] = combined.get(idx, 0) + score
# مرتب‌سازی نهایی
sorted_items = sorted(combined.items(), key=lambda x: x[1], reverse=True)
cand_idx = [item[0] for item in sorted_items[:top_k]]
return cand_idx
def rerank2(self, query: str, candidate_indices: List[int], passages: List[str], final_k:int=4):
z_results = [[query, sentence] for sentence in passages]
# The scores map into 0-1 by set "normalize=True", which will apply sigmoid function to the score
scores = self.reranker.compute_score(z_results, normalize=True)
s_results = sorted(zip(scores, z_results, candidate_indices), key=lambda x: x[0], reverse=True)
s_results2 = s_results[:final_k]
results = [[i[0], i[1][1], i[2]] for i in s_results2]
print('%'*50)
print('%'*50)
print(results)
with open('./llm-answer/reranker-result.txt', mode='a+', encoding='utf-8') as file:
for item in results:
file.write(f'{item}\n')
print('%'*50)
print('%'*50)
return results
def rerank(self, query: str, candidate_indices: List[int], passages: List[str], final_k: int) -> List[Tuple[int, float]]:
"""
Rerank candidate passages using a cross-encoder (e.g., MonoT5, MiniLM, etc.).
Args:
query (str): پرسش کاربر
candidate_indices (List[int]): ایندکسهای کاندیدا (از retriever)
passages (List[str]): کل جملات/پاراگرافها
final_k (int): تعداد نتایج نهایی
Returns:
List[Tuple[int, float]]: لیستی از (ایندکس، امتیاز) برای بهترین نتایج
"""
if final_k <= 0 or not candidate_indices:
return []
# آماده‌سازی جفت‌های (query, passage)
texts = [query] * len(candidate_indices)
pairs = passages
scores: List[float] = []
def _iter_batches(max_bs: int):
bs = max_bs
while bs >= 16: # حداقل batch_size
try:
with torch.inference_mode():
for start in range(0, len(pairs), bs):
batch_texts = texts[start:start + bs]
batch_pairs = pairs[start:start + bs]
inputs = self.tokenizer(
batch_texts,
batch_pairs,
padding=True,
truncation=True,
max_length=512,
return_tensors="pt",
).to(self.device)
logits = self.reranker(**inputs).logits.view(-1)
scores.extend(logits.detach().cpu().tolist())
return True
except torch.cuda.OutOfMemoryError:
if torch.cuda.is_available():
torch.cuda.empty_cache()
bs //= 2
return False
# اجرای reranking
success = _iter_batches(max_bs=64)
if not success:
raise RuntimeError("Reranker failed due to CUDA OOM, even with small batch size.")
# مرتب‌سازی نتایج بر اساس نمره
reranked = sorted(
zip(candidate_indices, scores),
key=lambda x: x[1],
reverse=True
)[:final_k]
return reranked
def get_passages(self, cand_idx, content_list):
passages = []
for idx in cand_idx:
passages.append(content_list[idx])
return passages
# --- Search (بدون تغییر) ---
def search(self, query: str, content_list, topk_dense=50, topk_sparse=50,
pre_rerank_k=50, final_k=10):
start_time = datetime.datetime.now()
# form embedder model
d_idx, d_scores = self.dense_retrieve(query, topk_dense)
dense_retrieve_end = datetime.datetime.now()
# print('@'*50)
# print(f'dense_retrieve_duration: {(dense_retrieve_end - start_time).total_seconds()}')
# from tfidf_matrix
s_idx, s_scores = self.sparse_retrieve(query, topk_sparse)
sparse_retrieve_end = datetime.datetime.now()
# print(f'sparse_retrieve_duration: {(sparse_retrieve_end - dense_retrieve_end).total_seconds()}')
cand_idx = self.fuse(d_idx, d_scores, s_idx, s_scores, pre_rerank_k)
fuse_end = datetime.datetime.now()
# print(f'fuse_duration: {(fuse_end - sparse_retrieve_end).total_seconds()}')
passages = self.get_passages(cand_idx, content_list)
get_passages_end = datetime.datetime.now()
# print(f'get_passages_duration: {(get_passages_end - fuse_end).total_seconds()}')
reranked = self.rerank(query, cand_idx, passages, final_k) # rerank2
rerank_end = datetime.datetime.now()
# print(f'rerank_duration: {(rerank_end - get_passages_end).total_seconds()}')
# print('@'*50)
return [{"idx": i, "content": self.content_list[i],"prefix": self.prefix_list[i], "rerank_score": score}
for i, score in reranked]
async def single_query(query: str):
# query = cleaning(query)
retrived_sections_ids = []
retrived_sections = pipe.search(query, content_list, topk_dense=100, topk_sparse=100, pre_rerank_k=100, final_k=15)
final_similars = ''
for i, row in enumerate(retrived_sections, 1):
id_value = '{' + str(ids[row['idx']]) + '}'
result = f"id: {id_value} \n{row['prefix']} {row['content']}\n"
retrived_sections_ids.append(ids[row['idx']])
final_similars += ''.join(result)
return final_similars, retrived_sections_ids
async def find_refrences(llm_answer: str) -> List[str]:
"""
شناسایی شناسه هایی که مدل زبانی، برای تهیه پاسخ از آنها استفاده کرده است
Args:
llm_answer(str): متنی که مدل زبانی تولید کرده است
Returns:
refrence_ids(List[str]): لیستی از شناسه های تشخیص داده شده
"""
pattern = r"\{[^\}]+\}"
# pattern = r"(?:\{([^\}]+)\}|【([^】]+)】)"
refrence_ids = re.findall(pattern, llm_answer)
new_refrences_ids = []
for itm in refrence_ids:
# print(itm)
refrence = itm.lstrip('{')
refrence = refrence.lstrip('}')
new_refrences_ids.append(refrence)
refrence_ids = [item.lstrip('{').rstrip('}') for item in refrence_ids]
return refrence_ids
async def replace_refrences(llm_answer: str, refrences_list:List[str]) -> List[str]:
"""
شناسایی شناسه هایی که مدل زبانی، برای تهیه پاسخ از آنها استفاده کرده است
Args:
llm_answer(str): متنی که مدل زبانی تولید کرده است
refrences_list(List[str]): لیست شناسه ماده های مورد استفاده در پاسخ مدل زبانی
Returns:
llm_answer(str), : متن بازسازی شده پاسخ مدل زبانی که شناسه ماده های مورد استفاده در آن، اصلاح شده است
"""
# refrences = ''
for index, ref in enumerate(refrences_list,1):
new_ref = '{' + str(ref) + '}'
llm_answer = llm_answer.replace(new_ref, f'{str(index)}»](https://majles.tavasi.ir/entity/detail/view/qsection/{ref}) ')
# id = ref.lstrip('{')
# id = id.rstrip('}')
# refrences += ''.join(f'[{index}] https://majles.tavasi.ir/entity/detail/view/qsection/{id}\n')
# llm_answer = f'{llm_answer}\n\nمنابع پاسخ‌:\n{refrences.strip()}'
return llm_answer.strip()
def initial_model():
global pipe
global content_list, ids, prefix_list, faiss_index
if not pipe :
# load basic items
content_list, ids, prefix_list, faiss_index = load_faiss_index(FAISS_INDEX_PATH, FAISS_METADATA_PATH)
pipe = HybridRetrieverReranker(content_list, ids, prefix_list, faiss_index, dense_alpha=0.6)
# query preprocess and normalize
def save_result(chat_obj: object) -> bool:
# index result in elastic
pass
async def get_title_user_prompt(query: str):
"""
get a query and prepare a prompt to generate title based on that
"""
title_prompt = f'برای متن {query} یک عنوان با معنا که بین 3 تا 6 کلمه داشته باشد، در قالب یک رشته متن ایجاد کن. سبک و لحن عنوان، حقوقی و کاملا رسمی باشد. عنوان تولید شده کاملا ساده و بدون هیچ مارک داون یا علائم افزوده ای باشد. غیر از عنوان، به هیچ وجه توضیح اضافه ای در قبل یا بعد آن اضافه نکن.'
return title_prompt
async def get_title_system_prompt():
"""
returns a system prompt due to generate title
"""
title_system_prompt = f'تو یک دستیار حقوقی هستی و می توانی متون و سوالات حقوقی را به زبان ساده و دقیق توضیح بدهی.'
return title_system_prompt
async def ask_chatbot_avalai(query:str, chat_id:str):
prompt_status = True
llm_model = ''
llm_answer = ''
cost_prompt = 0
cost_title = 0
status_text = 'لطفا متن سوال را وارد نمائید'
if query == '':
prompt_status = False
# در صورتی که وضعیت پرامپت معتبر باشد، وارد فرایند شو
if prompt_status:
before_title_time = datetime.datetime.now()
title_system_prompt = await get_title_system_prompt()
title_user_prompt = await get_title_user_prompt(query)
title, cost_title = await llm_base_request(title_system_prompt, title_user_prompt)
# title, cost_title = await oss_base_request(title_system_prompt, title_user_prompt)
if not title:
title = query
title_prompt_duration = (datetime.datetime.now() - before_title_time).total_seconds()
if title == '':
title = query.split()[0:10]
start_time = (datetime.datetime.now())
result_passages_text, result_passages_ids = await single_query(query)
end_retrive = datetime.datetime.now()
print('-'*40)
print(f'title_prompt_duration: {title_prompt_duration}')
retrive_duration = (end_retrive - start_time).total_seconds()
print(f'retrive duration: {str(retrive_duration)}')
prompt = f'''برای پرسش "{query}" از میان متون قانونی زیر، پاسخ مناسب و دقیق را استخراج کن.
متون قانونی:
"{result_passages_text}"
'''
try:
for model in models:
before_prompt_credit = await credit_refresh()
llm_model = model
print(f'using model: {model}')
try:
llm_answer, cost_prompt = await llm_request(prompt, model)
# llm_answer, cost_prompt = await oss_request(prompt)
except:
print(f'error in ask-chatbot-avalai model:{model}')
continue
break
except Exception as error:
after_prompt_credit = await credit_refresh()
prompt_cost = int(before_prompt_credit) - int(after_prompt_credit)
error = f'model: {model} \n{error}\n\n'
print('+++++++++++++++++')
print(f'llm-error.txt writing error: {error}')
print('+++++++++++++++++')
async with aiofiles.open('./llm-answer/llm-error.txt', mode='a+', encoding='utf-8') as file:
await file.write(error)
prompt_status = False
status_text = 'با عرض پوزش، سرویس موقتا در دسترس نیست. لطفا دقایقی دیگر دوباره تلاش نمائید!'
# حالتی که وضعیت پرامپت، نامعتبر باشد، یک شی با مقادیر زیر برگردانده می شود
else:
chat_obj = {
'id' : chat_id, # str
'title' : '', # str
'user_id' : '',
'user_query' : query, # str
'model_key' : llm_model, # str
'retrived_passage' : '', # str
'retrived_ref_ids' : '', # list[obj]
'prompt_type' : 'question-answer', # str
'retrived_duration' : '', # str
'llm_duration' : '0', # str
'full_duration' : '0', # str
'cost_prompt' : str(cost_prompt), # str
'cost_title' : str(cost_title), # str
'cost_total' : str(cost_prompt + cost_title), # str
'time_create' : str(start_time), # str
'used_ref_ids' : [], # list[str]
'prompt_answer' : '', # str
'status_text' : status_text,
'status' : prompt_status, # or False # bool
}
# بازگرداندن آبجکت ایجاد شده
return chat_obj, status_text
llm_answer_duration = (datetime.datetime.now() - end_retrive).total_seconds()
print(f'llm answer duration: {str(llm_answer_duration)}')
used_refrences_in_answer = await find_refrences(llm_answer)
llm_answer = await replace_refrences(llm_answer, used_refrences_in_answer)
full_prompt_duration = (datetime.datetime.now() - start_time).total_seconds()
print(f'full prompt duration: {full_prompt_duration}')
print('~'*40)
status_text ='پاسخ با موفقیت ایجاد شد'
print(f'cost_prompt: {cost_prompt}')
print(f'cost_title: {cost_title}')
chat_obj = {
'id' : chat_id, # str
'title' : title, # str
'user_id' : '', #
'user_query' : query, # str
'model_key' : llm_model, # str
'retrived_passage' : result_passages_text, # str
'retrived_ref_ids' : result_passages_ids, # list[obj]
'prompt_type' : 'question-answer', # str
'retrived_duration' : retrive_duration, # str
'llm_duration' : llm_answer_duration, # str
'full_duration' : full_prompt_duration, # str
'cost_prompt' : str(cost_prompt), # str
'cost_title' : str(cost_title), # str
'cost_total' : str(cost_prompt + cost_title), # str
'time_create' : str(start_time), # str
'used_ref_ids' : used_refrences_in_answer, # list[str]
'prompt_answer' : llm_answer, # str
'status_text' : status_text, # str
'status' : True, # or False # bool
}
prev_chat_data = []
try:
async with aiofiles.open('./llm-answer/chat-messages1.json', mode='r', encoding='utf-8') as file:
content = await file.read()
prev_chat_data = json.loads(content)
prev_chat_data.append(chat_obj)
except:
pass
prev_chat_data.append(chat_obj)
async with aiofiles. open('./llm-answer/chat-messages1.json', mode='w', encoding='utf-8') as output:
await output.write(json.dumps(prev_chat_data, ensure_ascii=False, indent=2))
# save_result(chat_obj)
# ایجاد آبجکت بازگشتی به فرانت
# chat_obj.pop('retrived_passage')
# chat_obj.pop('prompt_type')
print('~'*40)
return chat_obj
async def ask_chatbot(query:str, chat_id:str):
prompt_status = True
llm_model = 'gpt.oss.120b'
llm_answer = ''
cost_prompt = 0
cost_title = 0
status_text = 'لطفا متن سوال را وارد نمائید'
if query == '':
prompt_status = False
# در صورتی که وضعیت پرامپت معتبر باشد، وارد فرایند شو
if prompt_status:
before_title_time = datetime.datetime.now()
title_system_prompt = await get_title_system_prompt()
title_user_prompt = await get_title_user_prompt(query)
# title, cost_title = await llm_base_request(title_system_prompt, title_user_prompt)
title, cost_title = await oss_base_request(title_system_prompt, title_user_prompt)
if not title:
title = query
title_prompt_duration = (datetime.datetime.now() - before_title_time).total_seconds()
if title == '':
title = query.split()[0:10]
start_time = (datetime.datetime.now())
result_passages_text, result_passages_ids = await single_query(query)
end_retrive = datetime.datetime.now()
print('-'*40)
print(f'title_prompt_duration: {title_prompt_duration}')
retrive_duration = (end_retrive - start_time).total_seconds()
print(f'retrive duration: {str(retrive_duration)}')
prompt = f''' برای پرسش "{query}" از میان متون قانونی زیر، پاسخ مناسب و دقیق را استخراج کن.
متون قانونی:
"{result_passages_text}"
'''
# for model in models:
# before_prompt_credit = credit_refresh()
try:
# llm_model = model
# print(f'using model: {llm_model}')
# llm_answer, cost_prompt = await llm_request(prompt, model)
llm_answer, cost_prompt = await oss_request(prompt)
except Exception as error:
# after_prompt_credit = credit_refresh()
# prompt_cost = int(before_prompt_credit) - int(after_prompt_credit)
error = f'model: gpt.oss.120b \n{error}\n\n'
print('+++++++++++++++++')
print(f'llm-error.txt writing error: {error}')
print('+++++++++++++++++')
async with aiofiles.open('./llm-answer/llm-error.txt', mode='a+', encoding='utf-8') as file:
await file.write(error)
prompt_status = False
status_text = 'با عرض پوزش، سرویس موقتا در دسترس نیست. لطفا دقایقی دیگر دوباره تلاش نمائید!'
# حالتی که وضعیت پرامپت، نامعتبر باشد، یک شی با مقادیر زیر برگردانده می شود
else:
chat_obj = {
'id' : chat_id, # str
'title' : '', # str
'user_id' : '',
'user_query' : query, # str
'model_key' : llm_model, # str
'retrived_passage' : '', # str
'retrived_ref_ids' : '', # list[obj]
'prompt_type' : 'question-answer', # str
'retrived_duration' : '', # str
'llm_duration' : '0', # str
'full_duration' : '0', # str
'cost_prompt' : str(cost_prompt), # str
'cost_title' : str(cost_title), # str
'cost_total' : str(cost_prompt + cost_title), # str
'time_create' : str(start_time), # str
'used_ref_ids' : [], # list[str]
'prompt_answer' : '', # str
'status_text' : status_text,
'status' : prompt_status, # or False # bool
}
# بازگرداندن آبجکت ایجاد شده
return chat_obj, status_text
llm_answer_duration = (datetime.datetime.now() - end_retrive).total_seconds()
print(f'llm answer duration: {str(llm_answer_duration)}')
llm_answer = llm_answer.replace('','{')
llm_answer = llm_answer.replace('','}')
used_refrences_in_answer = await find_refrences(llm_answer)
llm_answer = await replace_refrences(llm_answer, used_refrences_in_answer)
full_prompt_duration = (datetime.datetime.now() - start_time).total_seconds()
print(f'full prompt duration: {full_prompt_duration}')
print('~'*40)
status_text ='پاسخ با موفقیت ایجاد شد'
print(f'cost_prompt: {cost_prompt}')
print(f'cost_title: {cost_title}')
chat_obj = {
'id' : chat_id, # str
'title' : title, # str
'user_id' : '',
'user_query' : query, # str
'model_key' : llm_model, # str
'retrived_passage' : result_passages_text, # str
'retrived_ref_ids' : result_passages_ids, # list[obj]
'prompt_type' : 'question-answer', # str
'retrived_duration' : retrive_duration, # str
'llm_duration' : llm_answer_duration, # str
'full_duration' : full_prompt_duration, # str
'cost_prompt' : str(cost_prompt), # str
'cost_title' : str(cost_title), # str
'cost_total' : str(cost_prompt + cost_title), # str
'time_create' : str(start_time), # str
'used_ref_ids' : used_refrences_in_answer, # list[str]
'prompt_answer' : llm_answer, # str
'status_text' : status_text, # str
'status' : True, # or False # bool
}
prev_chat_data = []
async with aiofiles.open('./llm-answer/chat-messages1.json', mode='r', encoding='utf-8') as file:
content = await file.read()
prev_chat_data = json.loads(content)
prev_chat_data.append(chat_obj)
async with aiofiles. open('./llm-answer/chat-messages1.json', mode='w', encoding='utf-8') as output:
await output.write(json.dumps(prev_chat_data, ensure_ascii=False, indent=2))
# save_result(chat_obj)
# ایجاد آبجکت بازگشتی به فرانت
# chat_obj.pop('retrived_passage')
# chat_obj.pop('prompt_type')
print('~'*40)
return chat_obj
async def credit_refresh():
"""
Returns remained credit
"""
url = "https://api.avalai.ir/user/credit"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {await get_key()}"
}
remained_credit = requests.get(url, headers=headers)
remained_credit_value = str(remained_credit.json()['remaining_irt'])
async with aiofiles.open('./llm-answer/credit.txt', mode='a+', encoding='utf-8') as file:
await file.write(f'{remained_credit_value}\n')
return remained_credit_value
async def create_chat_id():
date = str((datetime.datetime.now())).replace(' ','-').replace(':','').replace('.','-')
chat_id = f'{date}-{random.randint(100000, 999999)}'
return chat_id
# تعریف مدل داده‌ها برای درخواست‌های API
class Query(BaseModel):
query: str
initial_model()
# uvicorn src.app:app --reload
if __name__ == "__main__":
# query = 'در قانون حمایت از خانواده و جوانی جمعیت چه خدماتی در نظر گرفته شده است؟'
while True:
query = input('enter your qustion:')
if query == '':
print('لطفا متن سوال را وارد نمائید')
continue
start = (datetime.datetime.now())
# result = test_dataset()
result = single_query(query)
end_retrive = datetime.datetime.now()
print('-'*40)
print(f'retrive duration: {(end_retrive - start).total_seconds()}')
prompt = f'برای پرسش "{query}" از میان مواد قانونی "{result}" .پاسخ مناسب و دقیق را استخراج کن. درصورتی که مطلبی مرتبط با پرسش در متن پیدا نشد، فقط پاسخ بده: "متاسفانه در منابع، پاسخی پیدا نشد!"'
llm_answer = llm_request(prompt)
print('-'*40)
print(f'llm duration: {(datetime.datetime.now() - end_retrive).total_seconds()}')
refrences = ''
recognized_refrences = find_refrences(llm_answer)
llm_answer = replace_refrences(llm_answer, recognized_refrences)
with open('./llm-answer/result.txt', mode='a+', encoding='utf-8') as file:
result_message = f'متن پرامپت: {query.strip()}\n\nپاسخ: {llm_answer} \n----------------------------------------------------------\n'
file.write(result_message)
with open('./llm-answer/passages.txt', mode='a+', encoding='utf-8') as file:
result_message = f'متن پرامپت: {query.strip()}\n\مواد مشابه: {result} \n----------------------------------------------------------\n'
file.write(result_message)
print('----------------------------------------------------------')
print(f'full duration: {(datetime.datetime.now() - start).total_seconds()}')
print('----------------------------------------------------------')
print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')

1
config.env Executable file
View File

@ -0,0 +1 @@
LLM_URL="http://172.16.29.102:8001/v1/"

View File

@ -6,5 +6,5 @@ COPY . /src/app
EXPOSE 80
CMD [ "uvicorn","chatbot:chatbot","--reload","--port","80","--host=0.0.0.0"]
CMD [ "uvicorn","main:app","--reload","--port","80","--host=0.0.0.0"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,743 +0,0 @@
ChatCompletion(id='chatcmpl-CMEPWQSoYM74Yu0kGagCV6ruLCpoN', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='شرایط معافیت از پرداخت عوارض شهرداری به شرح زیر است:\n\n1. **تخفیف و بخشودگی:** هرگونه تخفیف یا بخشودگی حقوق و عوارض شهرداری\u200cها باید از بودجه عمومی سالانه کشور تأمین شود. در غیر این صورت، بخشودگی و تخفیف ممنوع است {qs738926}.\n\n2. **خانواده شهداء و ایثارگران:** خانواده شهداء و ایثارگران و جانبازان بیست و پنج درصد (۲۵٪) و بالاتر از پرداخت عوارض شهرداری برای احداث یک واحد مسکونی حداکثر ۱۲۵ متر مربع برای یک بار معاف هستند {qs911698}.\n\n3. **جانبازان:** جانبازان بیست و پنج درصد (۲۵٪) و بالاتر نیز از پرداخت عوارض شهرداری برای احداث یک واحد مسکونی حداکثر ۱۲۵ متر مربع برای یک بار معاف می\u200cباشند {qs289963}.\n\n4. **سازمان رادیو تلویزیون:** سازمان رادیو تلویزیون ملی ایران از پرداخت هرگونه عوارض شهرداری معاف است {qs814894}.\n\n5. **طرح\u200cهای عمومی عمرانی:** طرح\u200cهای مربوط به خرید و تملک اراضی و املاک برای اجرای برنامه\u200cهای عمومی، عمرانی و نظامی دولت از تاریخ شروع به اجرا، از پرداخت هر نوع عوارض مستقیم به شهرداری\u200cها معاف هستند {qs217429}.\n\n6. **واحدهای آموزشی:** واحدهای آموزشی و پرورشی با تأیید وزارت آموزش و پرورش از پرداخت هرگونه عوارض شهرداری معاف می\u200cباشند {qs993425} {qs212450}.\n\n7. **انتقال بلاعوض به نفع دولت و شهرداری\u200cها:** هرگونه انتقال بلاعوض به نفع دولت و شهرداری\u200cها از پرداخت عوارض معاف می\u200cباشد {qs120991}.\n\n8. **گواهی وزارت دارایی:** دولت و شهرداری\u200cها و موسسات وابسته به آن\u200cها با گواهی وزارت دارایی از پرداخت این نوع مالیات معاف خواهند بود {qs725533} {qs748695}.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))], created=1759414646, model='gpt-4o-mini-2024-07-18', object='chat.completion', service_tier=None, system_fingerprint='fp_efad92c60b', usage=CompletionUsage(completion_tokens=465, prompt_tokens=8403, total_tokens=8868, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0, text_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0, text_tokens=None, image_tokens=None)), estimated_cost={'unit': '0.0016933950', 'irt': 197.03, 'exchange_rate': 116350})
-------------------------------
ChatCompletion(id='f4neaIupMtXp7M8P9viLgQs', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='شرایط معافیت از پرداخت عوارض شهرداری به شرح زیر است:\n\n* **محدودیت تخفیف و بخشودگی:** هرگونه تخفیف یا بخشودگی حقوق و عوارض شهرداری\u200cها توسط دولت و قوانین مصوب، مشروط به تأمین آن از بودجه عمومی سالانه کشور است. در غیر این صورت، بخشودگی و تخفیف ممنوع است {qs738926}.\n\n* **خانواده شهداء، ایثارگران و جانبازان:** خانواده شهداء، ایثارگران و جانبازان با از خودگذشتگی بیست و پنج درصد (۲۵٪) و بالاتر، از پرداخت عوارض شهرداری برای احداث یک واحد مسکونی با حداکثر مساحت ۱۲۵ متر مربع، برای یک بار معاف می\u200cباشند {qs911698}.\n\n* **جانبازان:** جانبازان با از خودگذشتگی بیست و پنج درصد (۲۵٪) و بالاتر، از پرداخت عوارض شهرداری برای احداث یک واحد مسکونی با حداکثر مساحت ۱۲۵ متر مربع، برای یک بار معاف هستند {qs289963}.\n\n* **سازمان رادیو تلویزیون ملی ایران:** این سازمان از پرداخت هرگونه عوارض شهرداری معاف است {qs814894}.\n\n* **طرح\u200cهای عمومی، عمرانی و نظامی:** طرح\u200cهایی که طبق لایحه قانونی نحوه خرید و تملک اراضی و املاک برای اجرای برنامه\u200cهای عمومی، عمرانی و نظامی دولت اجرا می\u200cشوند، از تاریخ شروع به اجرا، از پرداخت هر نوع عوارض مستقیم به شهرداری\u200cها، مانند عوارض زمین، ساختمان، اموال منقول و غیرمنقول، حق تشرف و حق مرغوبیت معاف هستند {qs217429}.\n\n* **واحدهای آموزشی و پرورشی:** واحدهای آموزشی و پرورشی، با تأیید وزارت آموزش و پرورش، از پرداخت هرگونه عوارض شهرداری معاف می\u200cباشند {qs993425} {qs212450}.\n\n* **انتقال بلاعوض به نفع دولت و شهرداری\u200cها:** هرگونه انتقال بلاعوض که به نفع دولت و شهرداری\u200cها صورت می\u200cگیرد، از پرداخت عوارض و اخذ هرگونه گواهی معاف است {qs120991}.\n\n* **معافیت دولت، شهرداری\u200cها و موسسات وابسته:** دولت، شهرداری\u200cها و موسسات وابسته به آن\u200cها، با گواهی وزارت دارایی، از پرداخت مالیات نقل و انتقالات قطعی معاف خواهند بود {qs725533} {qs748695}.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None, images=[], thinking_blocks=[]))], created=1759414653, model='gemini-2.5-flash-lite', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=596, prompt_tokens=10041, total_tokens=10637, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetails(audio_tokens=None, cached_tokens=None, text_tokens=10041, image_tokens=None)), vertex_ai_grounding_metadata=[], vertex_ai_url_context_metadata=[], vertex_ai_safety_results=[], vertex_ai_citation_metadata=[], estimated_cost={'unit': '0.0012425000', 'irt': 144.56, 'exchange_rate': 116350})
-------------------------------
ChatCompletion(id='chatcmpl-CMEW5PqnEzAVDKITc6s0ZgLtR6A0q', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='حمایت از خانواده در جمهوری اسلامی ایران بر اساس مواد قانونی متعددی انجام می\u200cشود که به شرح زیر است:\n\n1. **حمایت کلی از خانواده:** ماده ۸۱ قانون برنامه پنجساله هفتم پیشرفت جمهوری اسلامی ایران، به حمایت از خانواده و ارتقای کارآمدی ساختار سازمانی در حوزه\u200cهای خانواده، زنان و جوانان و همچنین حمایت همه\u200cجانبه از فرزندآوری و رفع موانع اشاره دارد {qs3390392}.\n\n2. **تخصیص زمین به خانواده\u200cها:** به منظور حمایت از خانواده و تحقق جوانی جمعیت، وزارت راه و شهرسازی موظف است در سال\u200cهای ۱۴۰۲، ۱۴۰۳ و ۱۴۰۴، زمین یا واحد مسکونی به صورت رایگان به خانواده\u200cهای دارای چهار فرزند و بیشتر زیر بیست سال اختصاص دهد {qs3186952} {qs3328892} {qs67a862ff4e15f_202}. \n\n3. **حمایت حقوقی و فرهنگی:** همچنین بر اساس بند ۱۴ سیاست\u200cهای کلی خانواده، حمایت حقوقی، اقتصادی و فرهنگی از خانواده\u200cهای با سرپرستی زنان و تسهیل ازدواج آنان مورد تأکید قرار گرفته است {qs2248511}.\n\n4. **حمایت از خانواده زندانیان:** حمایت از خانواده زندانیان و معدومین از طریق سازمان\u200cها و نهادهای خیریه مردمی و غیردولتی نیز پیش\u200cبینی شده است {qs108297}.\n\n5. **تخفیف مالیاتی:** در راستای قانون حمایت از خانواده و جوانی جمعیت، اشخاص حقیقی موضوع قانون مالیات\u200cهای مستقیم که فرزند سوم و بیشتر دارند، مشمول افزایش پانزده درصد (۱۵٪) در تخفیف مالیاتی می\u200cشوند {qs3186827} {qs67a862ff4e15f_052}.\n\n6. **اهداف تشکیل خانواده:** اهداف تشکیل و تحکیم خانواده شامل ارتقاء آگاهی اعضای خانواده نسبت به حقوق و وظایف یکدیگر، پیشگیری از تزلزل و فروپاشی نهاد خانواده و حمایت از خانواده\u200cهای آسیب\u200cدیده و کودکان است {qs1028727}.\n\n7. **راهبردهای حمایت:** راهبردها شامل محافظت از خانواده در برابر آسیب\u200cهای اجتماعی، اتخاذ تدابیر مناسب برای حمایت از خانواده\u200cهای آسیب\u200cدیده و حمایت از زنان و کودکان در برابر تعرضات نیز می\u200cباشد {qs1028747}.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))], created=1759415053, model='gpt-4o-mini-2024-07-18', object='chat.completion', service_tier=None, system_fingerprint='fp_efad92c60b', usage=CompletionUsage(completion_tokens=521, prompt_tokens=16933, total_tokens=17454, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0, text_tokens=None), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0, text_tokens=None, image_tokens=None)), estimated_cost={'unit': '0.0031378050', 'irt': 363.51, 'exchange_rate': 115850})
-------------------------------
ChatCompletion(id='GIveaNm2KKeUkdUPzcWboAU', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='قوانین و سیاست\u200cهای موجود در زمینه حمایت از خانواده شامل موارد زیر است:\n\n* **قانون حمایت از خانواده و جوانی جمعیت:** این قانون به منظور حمایت جامع از خانواده، ارتقاء کارآمدی ساختارهای سازمانی مرتبط با خانواده، زنان و جوانان، حمایت همه\u200cجانبه از فرزندآوری، رفع موانع و ایجاد مشوق\u200cهای مؤثر و اصلاح فرهنگی تدوین شده است {qs3390392}.\n\n* **اعطای زمین رایگان به خانواده\u200cهای پرجمعیت:** در راستای حمایت از خانواده و تحقق جوانی جمعیت، وزارت راه و شهرسازی مکلف است در سال\u200cهای ۱۴۰۲، ۱۴۰۳ و ۱۴۰۴، زمین یا واحد مسکونی رایگان به خانواده\u200cهای دارای چهار فرزند و بیشتر زیر بیست سال اختصاص دهد {qs3186952} {qs3328892} {qs67a862ff4e15f_202}.\n\n* **حمایت حقوقی، اقتصادی و فرهنگی از خانواده\u200cهای زنان سرپرست:** سیاست\u200cهای کلی خانواده بر حمایت حقوقی، اقتصادی و فرهنگی از خانواده\u200cهای با سرپرستی زنان و همچنین تشویق و تسهیل ازدواج آنان تأکید دارد {qs2248511}.\n\n* **حمایت از خانواده زندانیان:** حمایت از خانواده زندانیان و معدومین از طریق سازمان\u200cها و نهادهای خیریه مردمی، غیردولتی و انجمن\u200cهای حمایت از زندانیان در نظر گرفته شده است {qs108297}.\n\n* **تخفیف مالیاتی برای فرزندان:** اشخاص حقیقی که فرزند سوم و بیشتر آن\u200cها از آبان ۱۴۰۰ به بعد متولد شده است، مشمول پانزده درصد (۱۵٪) افزایش در تخفیف مالیاتی به ازای هر فرزند می\u200cشوند {qs3186827} {qs67a862ff4e15f_052}.\n\n* **اهداف و راهبردهای تحکیم خانواده:** اهداف این سیاست\u200cها شامل ارتقاء جایگاه خانواده، حمایت از تشکیل و تحکیم آن، ارتقاء سطح فرهنگی و تربیتی اعضا، هماهنگی سیاست\u200cها، آگاهی از حقوق و وظایف، گسترش ارزش\u200cهای اسلامی، و ایمن\u200cسازی خانواده از آسیب\u200cهای اجتماعی و حمایت از خانواده\u200cهای آسیب\u200cدیده است {qs1028727}. راهبردها نیز بر ارتقای آگاهی، محافظت از آسیب\u200cپذیری خانواده، مبارزه با ناهنجاری\u200cهای اجتماعی و حمایت از زنان و کودکان تأکید دارند {qs1028747}.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None, images=[], thinking_blocks=[]))], created=1759415062, model='gemini-2.5-flash-lite', object='chat.completion', service_tier=None, system_fingerprint=None, usage=CompletionUsage(completion_tokens=571, prompt_tokens=19342, total_tokens=19913, completion_tokens_details=None, prompt_tokens_details=PromptTokensDetails(audio_tokens=None, cached_tokens=None, text_tokens=19342, image_tokens=None)), vertex_ai_grounding_metadata=[], vertex_ai_url_context_metadata=[], vertex_ai_safety_results=[], vertex_ai_citation_metadata=[], estimated_cost={'unit': '0.0021626000', 'irt': 250.54, 'exchange_rate': 115850})
-------------------------------
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='متاسفانه در منابع، پاسخی پیدا نشد!', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))
-------------------------------
Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='اتومبیل\u200cهای نمایندگی\u200cهای سیاسی و کنسولی مقیم ایران و اعضای رسمی آنها، هنگام ترخیص، مشروط بر عمل متقابل، از پرداخت کلیه حقوق گمرکی، سود بازرگانی، عوارض و مالیات\u200cها معاف می\u200cباشند. {qs852944} همچنین، این اتومبیل\u200cها پس از گذشت ۳ سال از تاریخ ترخیص، در صورت عمل متقابل، هنگام فروش از پرداخت حقوق گمرکی، سود بازرگانی و عوارض متعلقه معاف خواهند بود. {qs852947}\n\nوسایل نقلیه مسافری خارجی با شرایطی از پرداخت عوارض راه و حق توقف معاف هستند. {qs335260}\n\nمتصدیان حمل کشورهای طرف موافقت\u200cنامه حمل و نقل بین\u200cالمللی جاده\u200cای کالا و مسافر (مانند کرواسی و لیتوانی)، برای انجام حمل و نقل بین\u200cالمللی جاده\u200cای و به صورت متقابل، از پرداخت عوارض و سایر پرداخت\u200cهای مربوط به مالکیت یا استفاده از وسیله نقلیه و همچنین عوارض مربوط به استفاده یا نگهداری راه\u200cهای کشور طرف دیگر معاف می\u200cشوند. {qs3437134} , {qs935848} , {qs985052}\n\nهرگونه انتقال بلاعوض به نفع دولت و شهرداری\u200cها از پرداخت عوارض معاف است. {qs120991}\n\nمالکان خودرو در صورت پرداخت و تسویه حساب قبوض جریمه\u200cهای رانندگی تا پایان آذرماه سال\u200cهای ۱۳۹۷ و ۱۳۹۵، از جریمه دیرکرد ناشی از عدم پرداخت تا پایان سال\u200cهای ۱۳۹۶ و ۱۳۹۴ بخشیده می\u200cشوند. {qs2565159} , {qs2207235}\n\nمسافران خاصی از پرداخت عوارض خروج از کشور معاف می\u200cباشند. {qs863196}', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None, images=[], thinking_blocks=[]))
-------------------------------
#####################################################
#####################################################
#####################################################
#####################################################
[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='برای معافیت از پرداخت عوارض شهرداری، شرایط زیر وجود دارد:\n\n1. خانواده شهداء و ایثارگران و جانبازان بیست و پنج درصد (۲۵ ٪) و بالاتر، از پرداخت عوارض شهرداری برای احداث یک واحد مسکونی حداکثر یکصد و بیست و پنج متر مربع برای یک\u200cبار معاف می\u200cباشند {qs911698}.\n\n2. جانبازان بیست و پنج درصد (۲۵ ٪) و بالاتر، از پرداخت عوارض شهرداری برای احداث یک واحد مسکونی حداکثر ۱۲۵ متر مربع برای یک\u200cبار معاف می\u200cباشند {qs289963}.\n\n3. سازمان رادیو تلویزیون ملی ایران از پرداخت هر گونه عوارض شهرداری معاف است و مشمول معافیت مذکور در ماده ۲۶ قانون نوسازی و عمران شهری نیز خواهد بود {qs814894}.\n\n4. طرح\u200cهای موضوع قانون نحوه خرید و تملک اراضی و املاک برای اجرای برنامه\u200cهای عمومی، عمرانی و نظامی دولت از تاریخ شروع به اجراء از پرداخت هر نوع عوارض مستقیم به شهرداری\u200cها معاف هستند {qs217429}.\n\n5. واحدهای آموزشی و پرورشی با تأیید وزارت آموزش و پرورش از پرداخت هر گونه عوارض شهرداری معاف می\u200cباشند {qs993425} و {qs212450}.\n\n6. هرگونه انتقال بلاعوض به نفع دولت و شهرداری\u200cها از پرداخت عوارض و اخذ هرگونه گواهی معاف می\u200cباشد {qs120991}. \n\nاین شرایط نشان\u200cدهنده مواردی است که می\u200cتوان بر اساس آن\u200cها از پرداخت عوارض شهرداری معاف شد.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))]
-------------------------------
[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='برای معافیت از پرداخت عوارض شهرداری، شرایط زیر وجود دارد:\n\n* خانواده شهداء، ایثارگران و جانبازان با درصد جانبازی بیست و پنج درصد (۲۵ ٪) و بالاتر، برای احداث یک واحد مسکونی حداکثر به متراژ یکصد و بیست و پنج متر مربع، برای یک\u200cبار از پرداخت عوارض شهرداری معاف می\u200cباشند {qs911698}.\n* جانبازان با درصد جانبازی بیست و پنج درصد (۲۵ ٪) و بالاتر، برای احداث یک واحد مسکونی حداکثر به متراژ ۱۲۵ متر مربع، برای یک\u200cبار از پرداخت عوارض شهرداری معاف می\u200cباشند {qs289963}.\n* سازمان رادیو تلویزیون ملی ایران از پرداخت هر گونه عوارض شهرداری معاف است {qs814894}.\n* طرح\u200cهای موضوع قانون نحوه خرید و تملک اراضی و املاک برای اجرای برنامه\u200cهای عمومی، عمرانی و نظامی دولت، از تاریخ شروع اجرا، از پرداخت هر نوع عوارض مستقیم به شهرداری\u200cها معاف هستند {qs217429}.\n* واحدهای آموزشی و پرورشی با تأیید وزارت آموزش و پرورش از پرداخت هر گونه عوارض شهرداری معاف می\u200cباشند {qs993425} و {qs212450}.\n* هرگونه انتقال بلاعوض به نفع دولت و شهرداری\u200cها از پرداخت عوارض و اخذ هرگونه گواهی معاف می\u200cباشد {qs120991}.\n* دولت و شهرداری\u200cها و موسسات وابسته به آن\u200cها با ارائه گواهی از وزارت دارایی، از پرداخت مالیات نقل و انتقالات قطعی معاف خواهند بود {qs725533} و {qs748695}.\n\nشرط کلی برای هرگونه تخفیف یا بخشودگی حقوق و عوارض شهرداری توسط دولت و قوانین مصوب، تأمین آن از بودجه عمومی سالانه کشور است. در غیر این صورت، بخشودگی و تخفیف حقوق و عوارض شهرداری ممنوع است {qs738926}.', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None, images=[], thinking_blocks=[]))]
-------------------------------
-------------------------------
-------------------------------
{'unit': '0.0006956400', 'irt': 80.42, 'exchange_rate': 115600}
-------------------------------
{'unit': '0.0011443000', 'irt': 132.28, 'exchange_rate': 115600}
-------------------------------
47.58
-------------------------------
89.14
-------------------------------
117.51
-------------------------------
149.32
-------------------------------
238.13
-------------------------------
199.27
-------------------------------
20.63
-------------------------------
22.99
-------------------------------
77.09
-------------------------------
79.66
-------------------------------
55.49
-------------------------------
69.28
-------------------------------
64.43
-------------------------------
85.99
-------------------------------
108.5
-------------------------------
58.21
-------------------------------
66.88
-------------------------------
97.08
-------------------------------
85.15
-------------------------------
98.28
-------------------------------
111.41
-------------------------------
124.53
-------------------------------
50.12
-------------------------------
206.18
-------------------------------
219.27
-------------------------------
249.11
-------------------------------
286.66
-------------------------------
281.35
-------------------------------
297.52
-------------------------------
331.9
-------------------------------
363.84
-------------------------------
376.68
-------------------------------
423.29
-------------------------------
466.93
-------------------------------
475.04
-------------------------------
20.42
-------------------------------
36.08
-------------------------------
50.47
-------------------------------
41.91
-------------------------------
55.16
-------------------------------
39.06
-------------------------------
61.63
-------------------------------
40.3
-------------------------------
69.5
-------------------------------
82.35
-------------------------------
140.04
-------------------------------
137.97
-------------------------------
200.29
-------------------------------
244.43
-------------------------------
257.28
-------------------------------
313.51
-------------------------------
315.89
-------------------------------
355.38
-------------------------------
396.78
-------------------------------
435.83
-------------------------------
478.95
-------------------------------
482.55
-------------------------------
249.69
-------------------------------
536.13
-------------------------------
575.45
-------------------------------
609.36
-------------------------------
643.95
-------------------------------
706.14
-------------------------------
748.32
-------------------------------
762.59
-------------------------------
527.55
-------------------------------
875.18
-------------------------------
939.63
-------------------------------
955.53
-------------------------------
1001.8
-------------------------------
1065.1
-------------------------------
1078.9
-------------------------------
1147.06
-------------------------------
1150.96
-------------------------------
1220.98
-------------------------------
1219.78
-------------------------------
1265.87
-------------------------------
1296.9
-------------------------------
47.79
-------------------------------
73.08
-------------------------------
87.81
-------------------------------
112.63
-------------------------------
137.99
-------------------------------
157.22
-------------------------------
186.68
-------------------------------
214.6
-------------------------------
242.14
-------------------------------
256.68
-------------------------------
283.86
-------------------------------
80.44
-------------------------------
121.42
-------------------------------
139.51
-------------------------------
101.81
-------------------------------
71.52
-------------------------------
180.64
-------------------------------
80.55
-------------------------------
97.99
-------------------------------
104.13
-------------------------------
155.02
-------------------------------
170.09
-------------------------------
96.74
-------------------------------
131.7
-------------------------------
80.5
-------------------------------
135.59
-------------------------------
148.67
-------------------------------
102.43
-------------------------------
131.11
-------------------------------
148.85
-------------------------------
98.64
-------------------------------
186.92
-------------------------------
188.55
-------------------------------
238.4
-------------------------------
235.39
-------------------------------
258.96
-------------------------------
209.47
-------------------------------
259.7
-------------------------------
234.19
-------------------------------
292.11
-------------------------------
297.42
-------------------------------
275.51
-------------------------------
286.96
-------------------------------
319.76
-------------------------------
386.88
-------------------------------
344.8
-------------------------------
406.07
-------------------------------
438.77
-------------------------------
659.01
-------------------------------
539.14
-------------------------------
551.07
-------------------------------
575.04
-------------------------------
72.18
-------------------------------
96.69
-------------------------------
106.42
-------------------------------
133.98
-------------------------------
162.94
-------------------------------
182.09
-------------------------------
206.28
-------------------------------
268.84
-------------------------------
275.0
-------------------------------
24.96
-------------------------------
56.4
-------------------------------
43.72
-------------------------------
63.34
-------------------------------
77.81
-------------------------------
78.82
-------------------------------
18.85
-------------------------------
49.17
-------------------------------
51.87
-------------------------------
76.95
-------------------------------
101.75
-------------------------------
122.79
-------------------------------
118.51
-------------------------------
101.94
-------------------------------
84.07
-------------------------------
224.65
-------------------------------
130.38
-------------------------------
108.94
-------------------------------
295.75
-------------------------------
105.6
-------------------------------
345.01
-------------------------------
344.0
-------------------------------
137.18
-------------------------------
395.53
-------------------------------
414.09
-------------------------------
204.4
-------------------------------
191.2
-------------------------------
31.68
-------------------------------
45.93
-------------------------------
74.96
-------------------------------
100.02
-------------------------------
129.08
-------------------------------
123.44
-------------------------------
236.05
-------------------------------
259.93
-------------------------------
291.77
-------------------------------
319.9
-------------------------------
348.04
-------------------------------
383.19
-------------------------------
235.45
-------------------------------
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001239.35
-------------------------------
860.89
-------------------------------
2803.63
-------------------------------
01440.25
-------------------------------
411.56
-------------------------------
1213.3
-------------------------------
063.92
-------------------------------
99.39
-------------------------------
000000000084.84
-------------------------------
37.3
-------------------------------
75.81
-------------------------------
147.93
-------------------------------
144.36
-------------------------------
85.03
-------------------------------
217.33
-------------------------------
00000149.16
-------------------------------
0205.91
-------------------------------
0140.51
-------------------------------
0148.86
-------------------------------
0130.56
-------------------------------
0000000069.39
-------------------------------
0000000132.37
-------------------------------
0220.48
-------------------------------
0000000000336.06
-------------------------------
0414.51
-------------------------------
401.78
-------------------------------
00393.24
-------------------------------
457.39
-------------------------------
00879.57
-------------------------------
0859.83
-------------------------------
01311.88
-------------------------------
01576.05
-------------------------------
1619.78
-------------------------------
1524.98
-------------------------------
837.66
-------------------------------
1641.84
-------------------------------
890.8
-------------------------------
883.3
-------------------------------
1953.82
-------------------------------
1821.97
-------------------------------
1876.64
-------------------------------
1124.28
-------------------------------
1093.42
-------------------------------
1922.2
-------------------------------
0152.77
-------------------------------
57.82
-------------------------------
00200.13
-------------------------------
0202.76
-------------------------------
0405.73
-------------------------------
0357.92
-------------------------------
54.72
-------------------------------
00230.81
-------------------------------
147.45
-------------------------------
0295.99
-------------------------------
0439.79
-------------------------------
0

View File

@ -1,725 +0,0 @@
108315.8
108315.8
108315.8
108315.8
108315.8
108315.8
108315.8
108315.8
108273.66
108315.8
108315.8
108315.8
108148.33
108148.33
108148.33
108148.33
108148.33
108148.33
108148.33
108148.33
108148.33
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107820.53
107311.44
107311.44
107311.44
107311.44
107311.44
107311.44
106600.25
106600.25
106600.25
106600.25
106600.25
106600.25
106600.25
106600.25
105674.64
105674.64
105674.64
105674.64
105674.64
105674.64
105674.64
105674.64
105545.4
105545.4
105545.4
105392.43
105392.43
105319.32
105064.98
105064.98
105064.98
104994.01
104844.02
104844.02
104844.02
104844.02
104633.2
104633.2
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
104427.13
103993.0
103503.05
103503.05
103427.1
103350.01
103269.16
103269.16
103269.16
103269.16
103269.16
103269.16
103212.11
103212.11
103116.15
103050.78
103050.78
103050.78
102963.69
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102854.07
102794.63
102794.63
102794.63
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102726.67
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102628.35
102542.09
102442.65
102330.13
102204.53
102144.41
102144.41
101937.29
101716.9
101716.9
101466.67
101466.67
101466.67
101466.67
101178.93
100896.33
100597.57
100264.5
100264.5
99899.47
99899.47
99521.35
99096.83
99096.83
99096.83
99096.83
98628.66
98628.66
98628.66
98628.66
98628.66
98628.66
98628.66
96472.15
96472.15
96450.82
96260.75
96260.75
96160.86
96103.13
95868.76
95803.86
95802.19
95597.97
95596.25
95285.35
95061.39
94827.5
94540.93
94539.27
94250.29
93564.76
93563.21
93165.04
92289.47
92287.87
91552.15
91025.68
91025.68
89884.47
89882.95
88558.88
87865.16
87336.23
86537.6
85683.38
84813.13
83900.61
82930.69
81948.34
79857.44
77636.63
77634.84
75303.16
75301.25
75179.23
75176.18
75087.4
74835.09
74676.85
74675.47
74487.78
74271.65
73770.94
73485.89
70998.28
70945.89
70657.52
70366.94
70115.51
68157.72
67863.0
67699.81
67612.06
67340.76
67226.26
67151.4
67038.79
66906.9
66794.53
66685.69
66581.6
66551.47
66420.04
66364.76
66232.0
66227.19
66002.66
65997.93
65911.03
56269.22
56197.89
56107.82
55996.72
55915.96
55828.47
55723.6
55652.33
55535.28
55419.11
55279.65
55145.03
55005.74
54881.07
54750.93
54615.69
54473.45
54151.09
54007.49
53828.13
53639.72
53456.63
53263.79
53047.02
52569.35
50475.24
50219.15
49950.43
49878.25
49779.95
49671.93
49534.53
49371.59
49187.86
48978.13
48709.29
48432.68
48406.11
48348.01
48302.61
48237.55
48158.0
48077.56
48056.84
48056.84
47952.03
47873.31
47769.79
47645.39
47516.55
47514.88
47512.76
47512.76
47433.21
47433.21
47099.25
46841.66
46841.66
46544.26
46389.25
46042.58
45696.9
45528.18
45528.18
45528.18
44715.19
44711.6
44499.03
44238.73
44238.73
44238.73
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
44201.47
43951.09
42258.63
42258.63
42258.63
42258.63
42258.63
42258.63
42258.63
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
42219.82
233219.82
233219.82
231116.5
228275.02
226390.3
225658.78
225658.78
225490.08
225490.08
225490.08
225490.08
225490.08
225490.08
225490.08
225490.08
225490.08
225490.08
225490.08
225402.31
225402.31
225362.36
225362.36
225362.36
225283.85
225283.85
225143.68
224999.32
224999.32
224912.5
224907.85
224907.85
224903.2
224721.37
224721.37
224721.37
224721.37
224721.37
224582.94
224374.03
224230.2
224091.97
223958.58
223958.58
223958.58
223958.58
223958.58
223958.58
223958.58
223958.58
223958.58
223958.58
223886.16
223886.16
223886.16
223886.16
223886.16
223886.16
223886.16
223886.16
223886.16
223751.03
223751.03
223751.03
223527.8
223527.8
223527.8
223527.8
223527.8
223527.8
223527.8
223527.8
223527.8
223527.8
223527.8
223188.5
223188.5
223188.5
222771.05
222768.48
222768.48
222366.71
222363.93
222363.93
221970.69
221970.69
221970.69
221510.58
220625.24
219765.41
218447.93
217015.15
217015.15
217015.15
217009.55
215537.02
214006.44
214003.7
214003.7
211518.61
211518.61
210632.26
210632.26
210632.26
210632.26
201231.56
201231.56
201231.56
201231.56
201231.56
201231.56
201231.56
201231.56
200138.15
200138.15
200138.15
199996.16
199996.16
199993.39
199935.57
199935.57
199935.57
199732.61
199729.61
199729.61
199526.85
199523.93
199155.08
199152.27
199152.27
198791.4
198791.4
198791.4
198741.66
198738.93
198738.93
198736.29
198736.29
198736.29
198379.02
198379.02
198379.02
198080.2
198077.46

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,10 +0,0 @@
[0.43728234058085713, 'الف - حقوق بنیادین کار (آزادی انجمنها و حمایت از حق تشکلهای مدنی روابط کار، حق سازماندهی و مذاکره دسته\u200cجمعی، تساوی مزدها برای زن و مرد در مقابل کار هم ارزش، منع تبعیض در اشتغال و حرفه، رعایت حداقل سن کار، ممنوعیت کار کودک، رعایت حداقل مزد متناسب با حداقل معیشت).', 186639]
[0.17097510097612545, 'تبصره ۱۱ - بمنظور ایجاد مرجع صلاحیتدار و بیطرفی برای حل اختلافات بین کارگر و کارفرما و ایجاد حسن تفاهم در بین آنان و تمرکز امور مربوط بکار و مراقبت در تهیه و اجرای مقررات قانون کار و قانون بیمه کارگران و همچنین حمایت و تأمین بهداشت و رفاه و بالا بردن سطح زندگی کارگران و وضع و اجرای مقررات بیمه\u200cهای اجتماعی و برقرار نمودن روابط با تشکیلات بین\u200cالمللی کار وزارتخانه\u200cای بنام وزارت کار تأسیس می\u200cشود.', 128416]
[0.15169625817516322, 'ث) حمایت از کارگران و نمایندگان آنها در برابر اقدامات انضباطی ناشی از اعمالی که آنها مطابق سیاست موضوع ماده (۴) فوق به طور معقول انجام داده\u200cاند.', 75037]
[0.11213845051838162, 'ماده ۷ - دولتهای طرف این میثاق حق هر کس را به تمتع از شرایط عادلانه و مساعد کار که بویژه متضمن مراتب زیر باشد برسمیت بشناسند: الف - اجرتی که لااقل امور ذیل را برای کلیه کارگران تأمین نماید: ۱ - مزد منصفانه و اجرت مساوی برای کار با ارزش مساوی بدون هیچ نوع تمایز بویژه اینکه زنان تضمین داشته باشند که شرایط کار آنان پائین\u200cتر از\u200cشرایط مورد استفاده مردان نباشد و برای کار مساوی مزد مساوی با مردان دریافت دارند. ۲ - مزایای کافی برای آنان و خانواده\u200cشان طبق مقررات این میثاق: ب - ایمنی و بهداشت کار. ج - تساوی فرصت برای هر کس که بتواند در خدمت خود بمدارج مناسب عالیتری ارتقاء یابد بدون در نظر گرفتن هیچگونه ملاحظات دیگری جز\u200cطول مدت خدمت و لیاقت. د - استراحت - فراغت و محدودیت معقول ساعات کار و مرخصی اداری با استفاده از حقوق همچنین مزد ایام تعطیل رسمی.', 194273]
[0.1079329839273747, '۲۰ - بمنظور تأمین شرائط مناسب\u200cتر کار و زندگی کارگران مهاجر نسبت بشرائطی که قانون یا رویه عملی برای سایر کارگران که در خدمت متشابه\u200cاشتغال دارند مقرر داشته و همچنین برای اینکه کارگران مهاجر هم مثل کارگران دیگر مشمول مقیاسهای حمایت بنحوی که در بندهای آتی این سفارش نامه خواهد آمد گردند کلیۀ مساعی باید صورت گیرد.', 8843]
[0.0531841966906351, '۱۳ ایجاد نظام جامع تأمین اجتماعی برای حمایت از حقوق محرومان و مستضعفان و مبارزه با فقر و حمایت از نهادهای عمومی و موسسات و خیریه\u200cهای مردمی با رعایت ملاحظات دینی و انقلابی.', 213766]
[0.05166811304646011, '۲ - ایجاد نظام جامع تأمین اجتماعی برای حمایت از حقوق محرومان و مستضعفان و مبارزه با فقر و حمایت از نهاد\u200cهای عمومی و موسسات و خیریه\u200cهای مردمی با رعایت ملاحظات دینی و انقلابی.', 53933]
[0.051528153447387044, 'ج تقویت همسویی منافع کارگران و کارفرمایان و تکالیف دولت با رویکرد حمایت از تولید و سه\u200cجانبه گرایی', 185751]
[0.024949120491999023, 'ماده ۲ - ۱ - هر یک از کشورهای عضو باید بوسائلی که منطبق با روشهای معمول جهت تعیین میزان اجرت باشد اجرای اصل تساوی اجرت کارگر\u200cزن و مرد را در قبال کار هم ارزش تشویق و تا حدودی که با روشهای فوق\u200cالذکر تطبیق نماید اجرای آنرا درباره عموم کارگران تأمین کند. ۲ - اجرای این اصل ممکن است بطرق زیر صورت گیرد: الف - وضع قوانین داخلی. ب - هر روشی که جهت تعیین میزان اجرت\u200cها ضمن قوانین پیش\u200cبینی\u200cشده باشد. ج - انعقاد پیمان\u200cهای دسته\u200cجمعی بین کارفرمایان و کارگران. د - ترکیبی از این روشهای مختلف.', 204904]
[0.024270693471581787, 'ز تلاش در جهت گسترش امکانات رفاهی و حفظ حقوق قانونی کارکنان واحد.', 35580]

View File

File diff suppressed because one or more lines are too long

58
main.py Executable file
View File

@ -0,0 +1,58 @@
from fastapi import FastAPI
from routers.rag_base import router as rag_base
from contextlib import asynccontextmanager
from fastapi.middleware.cors import CORSMiddleware
# --- Lifespan manager ---
@asynccontextmanager
async def lifespan(app: FastAPI):
# 🚀 Startup
print("🚀 Starting up RAG system...")
# ایجاد OSS client و ذخیره در app.state
# --- نکته مهم: اگر elastic_client هم می‌خوای توی startup درست کنی، اینجا اضافه کن ---
# elastic_client = get_elastic_client()
# app.state.elastic_client = elastic_client
yield # برنامه در این حالت اجرا می‌شود
# 🛑 Shutdown
print("🛑 Shutting down RAG system...")
# بستن اتصال‌های باز
client = getattr(app.state, "elastic_client", None)
if client is not None:
await client.close()
# --- ساخت اپلیکیشن ---
def create_app() -> FastAPI:
app = FastAPI(
title="qachat2 Backend",
version="0.1.0",
lifespan=lifespan, # ✅ اینجا lifespan رو متصل می‌کنیم
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def simple():
return "ai rag chat qanon OK"
@app.get("/ping")
async def ping():
return "ai rag chat qanon OK"
app.include_router(rag_base, prefix="")
return app
# ✅ نمونه‌سازی نهایی
app = create_app()

16
new_requirements.txt Executable file
View File

@ -0,0 +1,16 @@
cleantext
elasticsearch7
faiss_cpu
fastapi
hazm
langchain_openai
numpy
openai
pandas
pydantic
scikit_learn
sentence_transformers
torch
transformers
orjson
FlagEmbedding==1.3.5

63
oss.py
View File

@ -1,63 +0,0 @@
from openai import AsyncOpenAI
LLM_URL = "http://172.16.29.102:8001/v1/"
# item structure:
# item = {
# 'id' : '',
# 'system_prompt' : '',
# 'user_prompt' : '',
# 'assistant_prompt' : '',
# }
async def process_item(messages, reasoning_effort= 'medium', temperature= 0.4, top_p= 0.9, max_tokens= 2048):
"""
generates answer with gpt-oss-120b model
**Args:
reasoning_effort = 'medium' # -> low / high / medium
temperature = 0.4 # 0-1 -> creativity
top_p = 0.9 # 0-1 -> logic
max_tokens = 2048 # -> ... 128K
** Returns(tuple):
returns True, generated answer / False, failed message
"""
try:
async with AsyncOpenAI(base_url= LLM_URL, api_key="EMPTY") as client:
model_name = 'gpt-oss-120b'
# messages = [
# {"role": "system", "content": prompt_params.get("system_prompt", "")},
# {"role": "user", "content": prompt_params.get("user_prompt", "")},
# ]
# if prompt_params.get("assistant_prompt"):
# messages.append(
# {"role": "assistant", "content": prompt_params["assistant_prompt"]}
# )
# print(f'==== max_token {max_token}')
response = await client.chat.completions.parse(
model= model_name,
messages= messages,
temperature= temperature, # 0-1
top_p=top_p, # 0-1
reasoning_effort= reasoning_effort, # low , high , medium
# max_tokens= max_tokens, # ... 128K
stop= None,
)
# print('666666666666666666666666666666666')
# print(f"response.choices[0].message.parsed: {response.choices[0].message.parsed}")
# print('666666666666666666666666666666666')
if response and response.choices : # and response.choices[0].message.parsed:
response_message = response.choices[0].message.content
return True, response_message
except Exception as e:
response_message = 'error in llm response generation!'
print('!!!!!!!!!!!!!!!!!!!!!!!!!')
print(e)
print('!!!!!!!!!!!!!!!!!!!!!!!!!')
return False, response_message

425
routers/ai_data_parser.py Executable file
View File

@ -0,0 +1,425 @@
from typing import List
from pathlib import Path
import os, orjson, time, json, re, asyncio, traceback
from openai import AsyncOpenAI
# ------------------------------ پردازش API ------------------------------
class AsyncCore:
def __init__(
self,
model_name,
task_name,
output_schema,
api_url,
data_path=None,
reasoning_effort="low",
top_p=1,
temperature=0.0,
max_token=128000,
output_path=None,
ai_code_version=None,
request_timeout=30, # ثانیه
api_key="EMPTY",
save_number=2,
semaphore_number=5,
):
self.save_number = save_number
# json file of data
self.data_path = data_path
self.semaphore_number = semaphore_number
self.task_name = task_name
if output_path is None:
output_path = f"./{task_name}"
self.output_path = Path(output_path)
self._temp_path = self.output_path / "batch_data"
self._temp_processed_id_path = self._temp_path / "processed_id.json"
# Create output directory and subdirectories if they don't exist
self.output_path.mkdir(parents=True, exist_ok=True)
self._temp_path.mkdir(parents=True, exist_ok=True)
# self._temp_processed_id_path.mkdir(parents=True, exist_ok=True)
self.request_timeout = request_timeout
self.model_name = model_name
self.api_key = api_key
self.output_schema = output_schema
self.api_url = api_url
self.reasoning_effort = reasoning_effort
self.top_p = top_p
self.temperature = temperature
self.max_token = max_token
if ai_code_version is None:
ai_code_version = f"{model_name}_{reasoning_effort}"
self.ai_code_version = ai_code_version
self.PRIMARY_KEY = {"system_prompt", "user_prompt", "id"}
if data_path != None:
try:
self.data = self.__data_process()
print(f"📦 Loaded {len(self.data)} words")
except Exception as e:
raise ValueError(
f"Data loading/validation failed: {e}\n{traceback.format_exc()}"
)
def __validate_item(self, item, idx):
# Mandatory fields
for key in self.PRIMARY_KEY:
if key not in item:
raise ValueError(f"Missing mandatory key '{key}' in item #{idx}")
if not isinstance(item[key], str):
raise TypeError(
f"Item #{idx}: '{key}' must be a string, got {type(item[key]).__name__}"
)
# Optional field: assistant_prompt
if "assistant_prompt" not in item or item["assistant_prompt"] is None:
item["assistant_prompt"] = None
else:
if not isinstance(item["assistant_prompt"], str):
raise TypeError(
f"Item #{idx}: 'assistant_prompt' must be a string or absent, got {type(item['assistant_prompt']).__name__}"
)
return item # now normalized
def __data_process(self):
raw_data = self.__load_orjson(self.data_path)
if not isinstance(raw_data, list):
raise ValueError("Data must be a list of dictionaries.")
processed_data = []
for idx, item in enumerate(raw_data):
if not isinstance(item, dict):
raise ValueError(f"Item #{idx} is not a dictionary.")
validated_item = self.__validate_item(item, idx)
processed_data.append(validated_item)
return processed_data
def __get_max_number_file(self, directory):
# Pattern to match filenames like out_1.json, out_25.json, etc.
pattern = re.compile(r"output_(\d+)\.json$")
max_num = 0
for filename in os.listdir(directory):
match = pattern.match(filename)
if match:
num = int(match.group(1))
if num > max_num:
max_num = num
return max_num + 1
def __load_orjson(self, path: str | Path):
path = Path(path)
with path.open("rb") as f: # باید باینری باز بشه برای orjson
return orjson.loads(f.read())
def __save_orjson(self, path, data):
with open(path, "wb") as f:
f.write(
orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS)
)
def merge_json_dir(self, input_path, output_path):
directory = Path(input_path)
if not directory.is_dir():
raise ValueError(f"Not valid PATH: {input_path}")
seen_ids = set() # برای ردیابی idهای دیده‌شده (سریع!)
unique_data = [] # فقط داده‌های یکتا
failed_files = []
json_files = list(directory.glob("*.json"))
if not json_files:
print("⚠️ NO JSON File Found In This PATH")
return
for json_file in json_files:
try:
data = self.__load_orjson(json_file)
if not data: # خالی یا None
failed_files.append(json_file.name)
continue
if isinstance(data, list) and isinstance(data[0], dict):
for item in data:
item_id = item.get("id")
if item_id is None:
# اگر id نداشت، می‌تونی تصمیم بگیری: نگه داری یا ردش کنی
# اینجا فرض می‌کنیم فقط مواردی با id معتبر مهم هستند
continue
if item_id not in seen_ids:
seen_ids.add(item_id)
unique_data.append(item)
else:
raise ValueError(f"no list available in this json -> {json_file}")
except (
json.JSONDecodeError,
ValueError,
OSError,
KeyError,
TypeError,
) as e:
# print(f"❌ Failed in process '{json_file.name}': {e}")
failed_files.append(json_file.name)
# گزارش خطاها
if failed_files:
print("\n❌ We lose this file:")
for name in failed_files:
print(f" - {name}")
else:
print("\n✅ All JSON added")
# ذخیره خروجی
try:
self.__save_orjson(data=unique_data, path=output_path)
print(
f"\n💾 Final file saved: {output_path} (Total unique items: {len(unique_data)})"
)
except Exception as e:
print(f"❌ Error in saving final file: {e}")
def make_new_proccessed_ids_from_file(self, json_in, out_path):
data = self.__load_orjson(json_in)
finall_data = []
for d in data:
if d["id"]:
finall_data.append(d["id"])
finall_data = set(finall_data)
finall_data = list(finall_data)
print(f"-- len ids {len(finall_data)}")
self.__save_orjson(data=finall_data, path=out_path)
# ------------------------------ Main ------------------------------
async def __process_item(self, client, item):
try:
messages = [
{"role": "user", "content": item["user_prompt"]},
]
if item.get("system_prompt"):
messages.append(
{"role": "system", "content": item["system_prompt"]}
)
if item.get("assistant_prompt"):
messages.append(
{"role": "assistant", "content": item["assistant_prompt"]}
)
response = await client.chat.completions.parse(
model=self.model_name,
messages=messages,
temperature=self.temperature,
top_p=self.top_p,
reasoning_effort=self.reasoning_effort,
max_tokens=self.max_token,
stop=None,
response_format=self.output_schema,
)
parsed = (
response.choices[0].message.parsed
if response and response.choices and response.choices[0].message.parsed
else {"raw_text": str(response)}
)
parsed = self.output_schema.model_validate(parsed)
parsed = parsed.model_dump()
parsed = dict(parsed)
parsed["ai_code_version"] = self.ai_code_version
parsed["id"] = item["id"]
# parsed["item"] = item
return parsed, 200
except asyncio.TimeoutError:
print(f"⏳ Timeout on item {item['id']}")
return None, 408
except Exception as e:
print(f"⚠️ Error __process_item {item['id']}: {traceback.print_exc()}")
return None, 400
def async_eval(self, processed_id: List = []):
try:
asyncio.run(self.__async_eval(processed_id))
except KeyboardInterrupt:
print("\n🛑 Interrupted by user.")
traceback.print_exc()
async def __async_eval(self, processed_id: List):
"""
اجرای اصلی تکهستهای و async برای تولید خروجی نهایی.
"""
print("🔹 Starting async data processing...")
# ------------------ مرحله ۱: بازیابی شناسه‌های قبلاً پردازش‌شده ------------------
if not processed_id:
try:
processed_id = self.__load_orjson(self._temp_processed_id_path)
print(
f"📂 Loaded existing processed_id from {self._temp_processed_id_path}"
)
except Exception:
print("⚠️ No valid processed_id found. Starting fresh.")
processed_id = []
# ------------------ مرحله ۲: آماده‌سازی داده‌ها ------------------
all_processed_id = set(processed_id)
all_results = []
total_time = []
data = [item for item in self.data if item.get("id") not in all_processed_id]
print(
f" Total items: {len(self.data)} - {len(all_processed_id)} = {len(data)}"
)
# اگر چیزی برای پردازش نیست
if not data:
print("✅ Nothing new to process. All items are already done.")
return
# ------------------ مرحله ۳: شروع پردازش ------------------
print(f"🤖 Model: {self.model_name} | Reasoning: {self.reasoning_effort}")
async with AsyncOpenAI(base_url=self.api_url, api_key=self.api_key) as client:
semaphore = asyncio.Semaphore(5)
async def limited_process(item):
async with semaphore:
return await self.__process_item(client, item)
tasks = [asyncio.create_task(limited_process(item)) for item in data]
total_i = 0
# ✅ پردازش به ترتیب تکمیل (نه ترتیب لیست)
for i, task in enumerate(asyncio.as_completed(tasks), start=1):
start = time.time()
try:
parsed, status_code = await asyncio.wait_for(
task, timeout=self.request_timeout
) # ⏱ حداکثر 2 دقیقه
except asyncio.TimeoutError:
print(f"⏳ Task {i} timed out completely")
parsed, status_code = None, 408
total_time.append(time.time() - start)
if status_code == 200:
all_results.append(parsed)
all_processed_id.add(parsed.get("id"))
else:
print(f"⚠️ Skipped item (status={status_code})")
total_i += 1
# ✅ ذخیره‌ی موقت هر n مورد
if total_i >= self.save_number:
print(f"total_i {total_i}")
print(f"self.save_number {self.save_number}")
total_i = 0
self.__save_orjson(
data=list(all_processed_id),
path=self._temp_processed_id_path,
)
print(f"💾 Auto-saved processed ids: {len(all_processed_id)}")
number = self.__get_max_number_file(self._temp_path)
print(f"number {number}")
temp_output_path = self._temp_path / f"output_{number}.json"
self.__save_orjson(data=list(all_results), path=temp_output_path)
print(f"💾 Auto-saved partial data: {len(all_results)}")
all_results.clear()
# ✅ بعد از پایان تمام تسک‌ها، ذخیره نهایی برای داده‌های باقیمانده
if total_i > 0 or len(all_results) > 0:
print("💾 Final save of remaining data...")
self.__save_orjson(
data=list(all_processed_id),
path=self._temp_processed_id_path,
)
print(f"💾 Auto-saved processed ids: {len(all_processed_id)}")
number = self.__get_max_number_file(self._temp_path)
print(f"number {number}")
temp_output_path = self._temp_path / f"output_{number}.json"
self.__save_orjson(data=list(all_results), path=temp_output_path)
print(f"💾 Auto-saved partial data: {len(all_results)}")
all_results.clear()
# ------------------ مرحله ۴: ذخیره خروجی ------------------
final_data_path = self.output_path / f"final_data_{self.task_name}.json"
processed_id_path = self.output_path / "processed_id.json"
self.merge_json_dir(input_path=self._temp_path, output_path=final_data_path)
all_results = self.__load_orjson(final_data_path)
# make_new_proccessed_ids_from_file()
self.__save_orjson(data=list(all_processed_id), path=processed_id_path)
self.__save_orjson(data=all_results, path=final_data_path)
avg_time = (sum(total_time) / len(total_time)) if total_time else 0
print(
f"\n✅ Processing completed!\n"
f"📊 Total-Data: {len(data)} | "
f"⭕ Ignored-Data: {len(processed_id)} | "
f"📦 Proccessed-Data: {len(all_results)} | "
f"❌ Loss-Data: {len(data)-len(all_results)} | "
f"🕒 Avg Time: {avg_time:.2f}'s per item | "
f"🕒 Total Time: {sum(total_time):.4f}'s | "
f"💾 Results saved to: {final_data_path}"
)
async def single_simple_async_proccess_item(self, item, functions, function_name):
async with AsyncOpenAI(base_url=self.api_url, api_key=self.api_key) as client:
semaphore = asyncio.Semaphore(5)
async with semaphore:
try:
messages = [
{"role": "user", "content": item["user_prompt"]},
]
if item.get("system_prompt"):
messages.append(
{"role": "system", "content": item["system_prompt"]}
)
if item.get("assistant_prompt"):
messages.append(
{"role": "assistant", "content": item["assistant_prompt"]}
)
response = await client.chat.completions.parse(
model=self.model_name,
messages=messages,
temperature=self.temperature,
top_p=self.top_p,
reasoning_effort=self.reasoning_effort,
max_tokens=self.max_token,
stop=None,
response_format=self.output_schema,
functions=functions,
function_call={"name": function_name}
)
parsed = (
response.choices[0].message.parsed
if response and response.choices and response.choices[0].message.parsed
else {"raw_text": str(response)}
)
parsed = self.output_schema.model_validate(parsed)
parsed = parsed.model_dump()
parsed = dict(parsed)
parsed["ai_code_version"] = self.ai_code_version
return parsed, 200
except asyncio.TimeoutError:
print(f"⏳ Timeout on item {item}")
return None, 408
except Exception as e:
print(f"⚠️ Error __process_item {item}: {traceback.print_exc()}")
return None, 400

28
routers/base_model.py Executable file
View File

@ -0,0 +1,28 @@
from pydantic import BaseModel
from typing import List
class Title(BaseModel):
title: str
class Query(BaseModel):
query: str
class ChatObject(BaseModel):
title: str
user_query: str
model_key: str
retrived_passage: str
retrived_ref_ids: str
model_answer: str
status:str='success'
prompt_type: str= "question-answer"
class LLMOutput(BaseModel):
text : str
source : List[str]
class LLMInput(BaseModel):
query : str
knowledge : List[dict]

337
routers/chatbot_handler.py Executable file
View File

@ -0,0 +1,337 @@
import numpy as np
import torch, orjson, faiss, re
from typing import List
from sentence_transformers import SentenceTransformer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from FlagEmbedding import FlagReranker
from pathlib import Path
# nlist = 2048
# quantizer = faiss.IndexFlatIP(dim)
# index = faiss.IndexIVFFlat(quantizer, dim, nlist)
# index.train(embeddings)
# index.add(embeddings)
class InitHybridRetrieverReranker:
def __init__(
self,
embeder_path,
reranker_path,
dict_content: List[dict],
faiss_index,
dense_alpha: float = 0.6,
device: str = None,
cache_dir="/src/MODELS",
batch_size=512,
):
if device is None:
device = "cuda" if torch.cuda.is_available() else "cpu"
self.device = device
self.dense_alpha = dense_alpha
# ===============================
# تبدیل ورودی فقط یک بار
# ===============================
self.content_list = [x["content"] for x in dict_content]
self.ids_list = [x["id"] for x in dict_content]
self.N = len(self.content_list)
self.faiss_index = faiss_index
# Dense embedder
self.embedder = SentenceTransformer(
local_files_only=True,
model_name_or_path=embeder_path,
cache_folder=cache_dir,
device=self.device,
similarity_fn_name="cosine",
)
# TF-IDF
self.vectorizer = TfidfVectorizer(
analyzer="word",
ngram_range=(1, 2),
token_pattern=r"(?u)\b[\w\u0600-\u06FF]{2,}\b",
)
self.tfidf_matrix = self.vectorizer.fit_transform(self.content_list)
# Reranker
self.reranker = FlagReranker(
model_name_or_path=reranker_path,
local_files_only=True,
use_fp16=True,
devices=device,
cache_dir=cache_dir,
batch_size=batch_size,
normalize=True,
# max_length=1024,
# trust_remote_code=False,
# query_max_length=
)
print("RAG Ready — Retriever + Reranker Loaded")
# ================================
# Dense Search (FAISS)
# ================================
async def dense_retrieve(self, query: str, top_k: int):
if top_k <= 0:
return [], np.array([])
emb = self.embedder.encode(query, convert_to_numpy=True).astype(np.float32)
D, I = self.faiss_index.search(emb.reshape(1, -1), top_k)
return I[0], D[0]
# ================================
# Sparse Search (TF-IDF)
# ================================
async def sparse_retrieve(self, query: str, top_k: int):
if top_k <= 0:
return [], np.array([])
q_vec = self.vectorizer.transform([query])
sims = cosine_similarity(q_vec, self.tfidf_matrix)[0]
k = min(top_k, len(sims))
idx = np.argpartition(-sims, k - 1)[:k]
idx = idx[np.argsort(-sims[idx], kind="mergesort")]
return idx, sims[idx]
# ================================
# Reciprocal Rank Fusion
# ================================
async def fuse(self, d_idx, d_scores, s_idx, s_scores, top_k=50, k_rrf=60):
combined = {}
for rank, idx in enumerate(d_idx):
combined[idx] = combined.get(idx, 0) + 1.0 / (k_rrf + rank)
for rank, idx in enumerate(s_idx):
combined[idx] = combined.get(idx, 0) + 1.0 / (k_rrf + rank)
sorted_items = sorted(combined.items(), key=lambda x: x[1], reverse=True)
return [i[0] for i in sorted_items[:top_k]]
# ================================
# Rerank
# ================================
async def rerank(self, query: str, cand_idx: List[int], final_k: int = 10):
if not cand_idx:
return []
passages = [self.content_list[i] for i in cand_idx]
pairs = [[query, p] for p in passages]
scores = self.reranker.compute_score(pairs, normalize=True, max_length=512)
if isinstance(scores, float):
scores = [scores]
idx_score = list(zip(cand_idx, scores))
idx_score.sort(key=lambda x: x[1], reverse=True)
return idx_score[:final_k]
# ================================
# Main Search Function
# ================================
async def search_base(
self,
query: str,
topk_dense=50,
topk_sparse=50,
pre_rerank_k=50,
final_k=10,
):
d_idx, d_scores = await self.dense_retrieve(query, topk_dense)
s_idx, s_scores = await self.sparse_retrieve(query, topk_sparse)
cand_idx = await self.fuse(d_idx, d_scores, s_idx, s_scores, pre_rerank_k)
final_rank = await self.rerank(query, cand_idx, final_k)
# ===============================
# خروجی سریع و تمیز
# ===============================
return [
{
"id": self.ids_list[idx],
"content": self.content_list[idx],
"score": score,
}
for idx, score in final_rank
]
def load_orjson(path: str | Path):
path = Path(path)
with path.open("rb") as f: # باید باینری باز بشه برای orjson
return orjson.loads(f.read())
def save_orjson(path, data):
with open(path, "wb") as f:
f.write(
orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS)
)
WEB_LINK = "https://majles.tavasi.ir/entity/detail/view/qsection/"
# ref = f"[«{i}»](https://majles.tavasi.ir/entity/detail/view/qsection/{idx})"
def get_in_form(title: str, sections: list, max_len: int = 4000):
chunks = []
current = f"برای پرسش: {title}\n\n"
ref_text = "«منبع»"
for i, data in enumerate(sections, start=1):
sec_text = data.get("content", "")
idx = data.get("id")
# ساخت ref کامل
ref = f"[{ref_text}]({WEB_LINK}{idx})"
# متن کامل آیتم
block = f"{i}: {sec_text}\n{ref}\n\n"
# اگر با اضافه شدن این آیتم از حد مجاز عبور می‌کنیم → شروع چانک جدید
if len(current) + len(block) > max_len:
chunks.append(current.rstrip())
current = ""
current += block
# آخرین چانک را هم اضافه کن
if current.strip():
chunks.append(current.rstrip())
return chunks
def format_answer_bale(answer_text: str, sources: list, max_len: int = 4000):
"""
answer_text: متن خروجی مدل که داخلش عبارتهای مثل (منبع: qs2117427) وجود دارد
sources: مثل ['qs2117427']
"""
ref_text = "«منبع»"
def make_link(src):
return f"[{ref_text}]({WEB_LINK}{src})"
# الگو برای تشخیص هر پرانتز که شامل یک یا چند کد باشد
# مثلا: (qs123) یا (qs123, qs456, qs789)
pattern = r"\((?:منبع[: ]+)?([a-zA-Z0-9_, ]+)\)"
def replace_source(m):
content = m.group(1)
codes = [c.strip() for c in content.split(",")] # جداسازی چند کد
links = [make_link(code) for code in codes]
full_match = m.group(0)
# if "منبع" in full_match:
# print(f'Found explicit source(s): {links}')
# else:
# print(f'Found implicit source(s): {links}')
return ", ".join(links) # جایگزینی همه کدها با لینک‌هایشان
# جایگزینی در متن
answer_text = re.sub(pattern, replace_source, answer_text)
# اگر طول کمتر از max_len بود → تمام
if len(answer_text) <= max_len:
return [answer_text]
# تقسیم متن اگر طول زیاد شد
chunks = []
current = ""
sentences = answer_text.split(". ")
for sentence in sentences:
st = sentence.strip()
if not st.endswith("."):
st += "."
if len(current) + len(st) > max_len:
chunks.append(current.strip())
current = ""
current += st + " "
if current.strip():
chunks.append(current.strip())
return chunks
def get_user_prompt(query: str):
"""
get a query and prepare a prompt to generate title based on that
"""
title_prompt = f"برای متن {query} یک عنوان با معنا که بین 3 تا 6 کلمه داشته باشد، در قالب یک رشته متن ایجاد کن. سبک و لحن عنوان، حقوقی و کاملا رسمی باشد. عنوان تولید شده کاملا ساده و بدون هیچ مارک داون یا علائم افزوده ای باشد. غیر از عنوان، به هیچ وجه توضیح اضافه ای در قبل یا بعد آن اضافه نکن."
return title_prompt
def format_knowledge_block(knowledge):
lines = []
for item in knowledge:
_id = item.get("id", "unknown")
_content = item.get("content", "")
lines.append(f"- ({_id}) { _content }")
return "\n".join(lines)
def get_user_prompt2(obj):
query = obj.query
knowledge = obj.knowledge
prompt = f"""
شما باید تنها بر اساس اطلاعات ارائه شده پاسخ بدهید و هیچ دانشی خارج از آنها استفاده نکنید.
### پرسش:
{query}
### اسناد قابل استناد:
{format_knowledge_block(knowledge)}
### دستور تولید خروجی:
- پاسخی کاملاً دقیق، تحلیلی و مفهومی ایجاد کن
- لحن رسمی و حقوقی باشد
- اگر پاسخ نیاز به ترکیب چند سند دارد، آنها را ادغام کن
- اگر دادهها کافی نبود، این موضوع را شفاف اعلام کن اما اطلاعات مرتبط را همچنان ارائه بده
"""
return prompt
def get_user_prompt3(query, knowledge_json):
sys = f"""Answer the following based ONLY on the knowledge:
Query:
{query}
Knowledge:
{knowledge_json}"""
return sys
def load_faiss_index(index_path: str, metadata_path: str):
"""بارگذاری ایندکس FAISS و متادیتا (لیست جملات + عناوین)."""
index = faiss.read_index(index_path)
metadata = load_orjson(metadata_path)
metadata = [
{
"id": item["id"],
"content": item["content"],
"prefix": item["prefix"],
}
for item in metadata
]
return metadata, index

169
routers/rag_base.py Executable file
View File

@ -0,0 +1,169 @@
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
import time, os, traceback
from .base_model import Query, LLMOutput, LLMInput, Title
from .ai_data_parser import AsyncCore
from .chatbot_handler import (
InitHybridRetrieverReranker,
format_answer_bale,
get_user_prompt2,
get_user_prompt3,
load_faiss_index,
get_in_form,
)
from .static import (
EMBED_MODEL_PATH,
FAISS_INDEX_PATH,
FAISS_METADATA_PATH,
LLM_URL,
SYSTEM_PROMPT_FINALL,
RERANKER_MODEL_PATH,
LLM_ERROR,
MODEL_KEY,
MODEL_NAME,
OUTPUT_PATH_LLM,
REASONING_EFFORT,
TASK_NAME,
LLM_TIME_OUT,MAX_TOKEN, SYSTEM_PROPMT2
)
# ################################################## Global-params
router = APIRouter(tags=["ragchat"])
# # settings= get_settings()
METADATA_DICT, FAISS_INDEX = load_faiss_index(
index_path=FAISS_INDEX_PATH, metadata_path=FAISS_METADATA_PATH
)
RAG = InitHybridRetrieverReranker(
embeder_path=EMBED_MODEL_PATH,
reranker_path=RERANKER_MODEL_PATH,
dict_content=METADATA_DICT,
faiss_index=FAISS_INDEX,
dense_alpha=0.6,
device="cuda",
)
RUNNER_PROMPT = AsyncCore(
model_name=MODEL_NAME,
api_url=LLM_URL,
output_path=OUTPUT_PATH_LLM,
task_name=TASK_NAME,
output_schema=LLMOutput,
reasoning_effort=REASONING_EFFORT,
ai_code_version=MODEL_KEY,
request_timeout=LLM_TIME_OUT,
max_token=MAX_TOKEN,
save_number=1,
)
functions = [
{
"name": "legal_answer",
"description": "خروجی ساخت‌یافته از تحلیل حقوقی با ارجاع کامل به اسناد",
"parameters": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "متن کامل پاسخ شامل ارجاع (qsID)"
},
"source": {
"type": "array",
"items": {"type": "string"},
"description": "فهرست شناسه اسناد استفاده شده"
}
},
"required": ["text", "source"]
}
}
]
async def chat_bot_run(query):
try:
s = time.time()
sections_dict = await RAG.search_base(
query,
final_k=10,
topk_dense=100,
topk_sparse=100,
pre_rerank_k=100,
)
e = time.time()
input_data = LLMInput(query=query, knowledge=sections_dict)
# prompt = get_user_prompt2(input_data)
prompt = get_user_prompt3(query=query, knowledge_json=sections_dict)
llm_answer, _ = await RUNNER_PROMPT.single_simple_async_proccess_item(
item={"user_prompt": prompt, "system_prompt": SYSTEM_PROPMT2},
functions=functions,
function_name="legal_answer",
)
ee = time.time()
finall = format_answer_bale(
answer_text=llm_answer["text"], sources=llm_answer["source"]
)
eee = time.time()
print(
f'Rag = {e-s}',
f'llm_answer = {ee-e}',
f'Form = {eee-ee}',
sep='\n'
)
return finall
except:
traceback.print_exc()
async def rag_run(query):
try:
s = time.time()
sections_dict = await RAG.search_base(
query,
final_k=10,
topk_dense=100,
topk_sparse=100,
pre_rerank_k=100,
)
e = time.time()
finall = get_in_form(title=query, sections=sections_dict)
ee = time.time()
print(
f'Rag = {e-s}',
f'Form = {ee-e}',
sep='\n'
)
return finall
except:
traceback.print_exc()
@router.post("/run_chat")
async def run_chat(payload: Query, request: Request):
s = time.time()
try:
answer = await chat_bot_run(payload.query)
except:
print(f"chat_bot_run FAIL!")
answer = LLM_ERROR
e = time.time()
print(f"Total Time {e-s:.2f}'s")
return JSONResponse({"result": answer}, status_code=201)
@router.post("/run_rag")
async def run_chat(payload: Query, request: Request):
s = time.time()
try:
answer = await rag_run(payload.query)
except:
print(f"chat_bot_run FAIL!")
answer = LLM_ERROR
e = time.time()
print(f"Total Time {e-s:.2f}'s")
return JSONResponse({"result": answer}, status_code=201)

8
routers/readme.md Executable file
View File

@ -0,0 +1,8 @@
# "gpt-4o", "gpt-4o-mini", "deepseek-chat" , "gemini-2.0-flash", gemini-2.5-flash-lite
# gpt-4o : 500
# gpt-4o-mini : 34
# deepseek-chat: : 150
# gemini-2.0-flash : error
# cf.gemma-3-12b-it : 1
# gemini-2.5-flash-lite : 35 خیلی خوب

160
routers/static.py Executable file
View File

@ -0,0 +1,160 @@
from dotenv import load_dotenv
import os
LLM_URL = "http://localhost:8004/v1/" # "http://172.16.29.102:8001/v1/"
EMBED_MODEL_PATH = "/home2/MODELS/models--sentence-transformers--paraphrase-multilingual-MiniLM-L12-v2/snapshots/86741b4e3f5cb7765a600d3a3d55a0f6a6cb443d" # "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
RERANKER_MODEL_PATH = "/home2/MODELS/bge_reranker_m3_v2/bge-reranker-v2-m3" # "BAAI/bge-reranker-v2-m3"
FAISS_INDEX_PATH = "/home2/rag_qavanin_api/data/qavanin-faiss/faiss_index_qavanin_285k.index" # "/src/app/data/qavanin-faiss/faiss_index_qavanin_285k.index"
FAISS_METADATA_PATH = "/home2/rag_qavanin_api/data/qavanin-faiss/faiss_index_qavanin_285k_metadata.json" # "/src/app/data/qavanin-faiss/faiss_index_qavanin_285k_metadata.json"
PATH_LOG = "./data/llm-answer/"
load_dotenv()
RERANK_BATCH = int(os.environ.get("rerank_batch_size"))
API_KEY = os.getenv("api_key")
LLM_ERROR = "با عرض پوزش؛ ❌in ragمتاسفانه خطایی رخ داده است. لطفا لحظاتی دیگر دوباره تلاش نمائید"
MODEL_KEY = "oss-120-hamava"
MODEL_NAME="gpt-oss-20b" # "gpt-oss-120b"
OUTPUT_PATH_LLM="/home2/rag_qa_chat2/data/_temp"
TASK_NAME="bale-chat"
REASONING_EFFORT="low"
LLM_TIME_OUT=30
MAX_TOKEN=8192
SYSTEM_PROMPT_FINALL = """شما یک دستیار تحلیل‌گر حقوقی متخصص در استنتاج دقیق از اسناد قانونی هستید.
ورودی شما شامل:
- یک پرسش کاربر (query)
- مجموعهای از چند متن قانونی (knowledge)، که هر کدام شامل:
- id (شناسه سند)
- content (متن بند قانونی)
وظیفه شما:
1. پرسش را دقیق بخوانید و فقط بر اساس اطلاعات موجود در اسناد ارائه شده پاسخ دهید.
2. از خودتان هیچ اطلاعات جدید، تخمین، تفسیر شخصی، یا دانش خارج از اسناد وارد نکنید.
3. اگر یک پاسخ نیاز به ترکیب چند سند دارد، آنها را استخراج و در هم ادغام کنید و نتیجه را کاملاً روان و قابل فهم بنویسید.
4. پاسخ باید:
- تحلیلمحور
- شفاف
- فارسی استاندارد و حقوقی
- ساختاریافته و قابل ارائه باشد
5. هر جمله یا بند از پاسخ **حتماً باید به یک یا چند id سند مشخص وصل شود**.
- اگر برای جملهای منبعی پیدا نشد، صریحاً در متن ذکر کنید: "(هیچ منبع مرتبط موجود نیست)"
- از اضافه کردن idهای فرضی یا خارج از knowledge خودداری شود.
6. از تکرار مستقیم یا کپی کردن جملات خام اسناد اجتناب کنید. آنها را با بازنویسی تحلیلی به کار ببرید.
7. در پایان پاسخ:
- حتماً لیست تمام شناسههای سندهای استفادهشده را برگردانید.
- فقط id های اسنادی که واقعاً در پاسخ استفاده شدهاند ذکر شوند به صورت دقیقا: (qs2127)
- ترتیب اهمیت و ارتباط در لیست رعایت شود.
8. پاسخ نهایی باید دقیقاً در فرمت JSON زیر برگردد و هیچ متن دیگری خارج از آن اضافه نشود:
{
"text" : "متن کامل پاسخ تحلیلی و دقیق به پرسش، هر جمله یا بند با (id) سند مرتبط یا (هیچ منبع مرتبط موجود نیست) مشخص شود.",
"source": ["qs123", "qs545", ...]
}
ورودی نمونه:
{
query: "متن سوال",
knowledge: [
{"id": "qs01", "content": "..."},
{"id": "qs02", "content": "..."},
...
]
}
"""
SYSTEM_PROPMT2 = '''You are a legal reasoning model that MUST base the answer ONLY on the documents provided in `knowledge`.
STRICT RULES:
1. You have no knowledge outside the provided documents.
2. Before generating the answer you MUST:
A. Extract the list of all valid document IDs from `knowledge`.
B. Think through the answer sentence-by-sentence.
C. Every sentence MUST be directly supported by one or more document IDs.
3. Any sentence that is not directly supported by at least one `id` MUST be removed.
4. Document IDs must appear in the text as:
(qs123)
(qs1002)
etc.
5. The final answer MUST be returned strictly as:
{
"text": "...",
"source": ["qs001", "qs999"]
}
Where:
- `text` contains the final written response with citations inline.
- `source` contains ONLY the list of IDs actually used in the answer, no duplicates, order by relevance.
6. JSON MUST be valid. No comments, no trailing commas.
7. To the extent that there is even the slightest relevance to the question in the documentation, generate an answer from the documentation, indicating that a close answer to the user's question was not found.
8. Finally, if no document supports the question, return:
{
"text": "هیچ سند مرتبطی یافت نشد.",
"source": []
}
9. Length must NOT be shortened. Provide full analysis, fully detailed.
Before generating your answer:
Extract the list of VALID IDs from `knowledge`.
You MUST NOT invent IDs.
Any ID not in that list is forbidden.
'''
#############
"""
شما یک دستیار تحلیلگر حقوقی متخصص در استنتاج دقیق از اسناد قانونی هستید.
ورودی شما شامل:
- یک پرسش کاربر (query)
- مجموعهای از چند متن قانونی (knowledge)، که هر کدام شامل:
- id (شناسه سند)
- content (متن بند قانونی)
وظیفه شما:
1. پرسش را دقیق بخوانید و فقط بر اساس اطلاعات موجود در اسناد ارائه شده پاسخ دهید.
2. از خودتان هیچ اطلاعات جدید، تخمین، تفسیر شخصی، یا دانش خارج از اسناد وارد نکنید.
3. اگر یک پاسخ نیاز به ترکیب چند سند دارد، آنها را استخراج و در هم ادغام کنید و نتیجه را کاملاً روان و قابل فهم بنویسید.
4. پاسخ باید:
- تحلیلمحور
- شفاف
- فارسی استاندارد و حقوقی
- ساختاریافته و قابل ارائه باشد
5. از تکرار مستقیم یا کپی کردن جملات خام اسناد اجتناب کنید. آنها را با بازنویسی تحلیلی به کار ببرید.
6. اگر اطلاعات موجود برای پاسخ کامل کافی نبود:
- این موضوع را صریح اعلام کنید
- اما موارد مرتبط موجود را همچنان خلاصه و ارائه کنید
7. در پایان پاسخ:
- لیست شناسههای سندهای استفادهشده را برگردانید
- فقط id های اسنادی که واقعاً در پاسخ استفاده شدهاند ذکر شوند به صورت دقیقا : (qs2127)
- ترتیب اهمیت در لیست رعایت شود
8. پاسخ نهایی باید دقیقاً در فرمت زیر برگردد:
خروجی نمونه:
{
"text" : "متن کامل پاسخ تحلیلی و دقیق به پرسش",
"source": ["qs123", "qs545", ...]
}
بدون هیچ توضیح یا متن اضافه خارج از این قالب.
ورودی نمونه:
{
query: "متن سوال",
knowledge: [
{"id": "qs01", "content": "..."},
{"id": "qs02", "content": "..."},
...
]
}"""

View File

@ -1,3 +1,3 @@
docker stop qachat
docker rm qachat
docker run --name qachat -p 2425:80 --net qachat_net --gpus=all -v ./:/src/app/ -v ./qavanin-faiss/:/src/app/qavanin-faiss/ -v ./llm-answer/:/src/app/llm-answer/ -v ./../MODELS:/src/MODELS -v ./../cache:/root/.cache/huggingface/hub -it --restart unless-stopped docker.tavasi.ir/tavasi/qachat:1.0.0
docker stop qachat2
docker rm qachat2
docker run --name qachat2 -p 8009:80 --net qachat_net --gpus=all -v ./:/src/app/ -v ./data/:/src/app/data/ -v ./../MODELS:/src/MODELS -v ./../cache:/root/.cache/huggingface/hub -it --restart unless-stopped docker.tavasi.ir/tavasi/qachat2:1.0.0

3
run_env.bash Executable file
View File

@ -0,0 +1,3 @@
source /home2/.venv/bin/activate
uvicorn main:app --port=8009 --host=0.0.0.0

View File

@ -65,8 +65,8 @@ def create_faiss_index_from_json(json_file_path, faiss_index_path, metadata_file
if __name__ == '__main__':
# استفاده از متد
json_file_path = './majles-output/sections-vec-285k.json'
faiss_index_path = './qavanin-faiss/faiss_index_qavanin_285k.index'
metadata_file_path = './qavanin-faiss/faiss_index_qavanin_285k_metadata.json'
json_file_path = '../majles-output/sections-vec-285k.json'
faiss_index_path = '../data/qavanin-faiss/faiss_index_qavanin_285k.index'
metadata_file_path = '../data/qavanin-faiss/faiss_index_qavanin_285k_metadata.json'
create_faiss_index_from_json(json_file_path, faiss_index_path, metadata_file_path)

View File

@ -1,2 +1,2 @@
sudo docker build -t docker.tavasi.ir/tavasi/qachat_base:1.0.0 -f dockerfile_base .
sudo docker build -t docker.tavasi.ir/tavasi/qachat:1.0.0 .
sudo docker build -t docker.tavasi.ir/tavasi/qachat2:1.0.0 .

View File

@ -32,7 +32,7 @@ from sklearn.manifold import TSNE
from sklearn.metrics.pairwise import cosine_similarity
#from normalizer import cleaning
try:
from elastic_helper import ElasticHelper
from util.elastic_helper import ElasticHelper
except Exception as error:
eee = error
pass
@ -609,7 +609,7 @@ def main():
# output_dir = "output-speechs"
# input_file = "./majles/data/sections.json"
input_file = ""
output_dir = "majles-output"
output_dir = "./data/majles-output"
# Run the complete pipeline
analyzer.process_pipeline(input_file, output_dir)