diff --git a/core/base_model.py b/core/base_model.py new file mode 100644 index 0000000..39b3a8a --- /dev/null +++ b/core/base_model.py @@ -0,0 +1,16 @@ +import json +from pydantic import BaseModel, Field +from typing import List, Optional, Callable, Literal, Union, Dict, Any + + +class ChatLaw(BaseModel): + ref_ids: List + answer: str + answer_type: str + llm_reason: str = None + metadata: Dict + +class TitleRepeat(BaseModel): + title: str + id: str + score: int diff --git a/core/core.py b/core/core.py index f71cb18..e9bb9f6 100755 --- a/core/core.py +++ b/core/core.py @@ -1,32 +1,22 @@ ################# modularity ### import from external-package -from fastapi import FastAPI, Request, HTTPException -import requests, logging, asyncio, httpx, os, uuid, traceback, orjson, copy, uvicorn, time, re -from dotenv import load_dotenv +import unicodedata, requests, logging, asyncio, httpx, os, json, uuid, traceback, orjson, copy, uvicorn, time, re from pathlib import Path from time import sleep -from enum import Enum -from typing import Dict, List, Tuple from collections import defaultdict -from typing import Union, List +from typing import Dict, List, Tuple from elasticsearch import Elasticsearch, helpers -import requests, logging, asyncio, httpx, os, uuid, traceback, orjson, copy, uvicorn, time from pathlib import Path -from time import sleep -import re -import unicodedata -import httpx -import json ### import from internal-file from router.bale.base_model import * from router.bale.bale_buttons import * from router.bale.bale_massages import * from core.static import * +from core.base_model import * ############## Global-Params - DATA_DIR = os.path.join(".", "_data_json") if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR) @@ -52,6 +42,8 @@ class Formatter: ########################################################### def __init__(self, max_len: int = 4000): self.max_len = max_len + + self.dash = "--------------------------------------------------------------------" self._number_map = { "0": "0️⃣", "1": "1️⃣", @@ -136,17 +128,72 @@ class Formatter: # توابع فرمت و ساختار ########################################################### - def form_search_in_law(self, title: str, sections: List) -> List: + def form_search_in_law_rules(self, header: str, body: List[SemanticSearchP2P], footer: str = None) -> List[str]: + """ + گروه‌بندی بر اساس in_rule.rule_id و ساخت بلوک‌های مجزا برای هر گروه. + """ + if footer: + footer = '\n' + footer + + # گروه‌بندی بر اساس in_rule.rule_id + grouped = defaultdict(list) + for item in body: + key = item.in_rule.rule_id + grouped[key].append(item) + + print(f'form_search_in_law_rules -> {len(grouped)}') + + chunks = [] + current = header + + # برای هر گروه (یعنی یک in_rule.rule_id منحصربه‌فرد) + for group_id, items in grouped.items(): + # فرض: همه‌ی in_rule در یک گروه، rule_content یکسانی دارند — از اولی استفاده می‌کنیم + in_rule_content = items[0].in_rule.rule_content + block_lines = [in_rule_content] + + # لیست db_ruleها با شماره‌گذاری + for i, item in enumerate(items, start=1): + db_content = item.db_rule.rule_content + section_id = self.__make_link_qs(src=item.db_rule.section_id) + block_lines.append(f"{self.number(i)} گزاره: {db_content} در {section_id}") + + block = "\n".join(block_lines) + "\n\n" + + # بررسی سایز و تقسیم در صورت نیاز + if len(current) + len(block) > self.max_len: + if current.strip() != header.strip(): + chunks.append(current.rstrip()) + current = header + block # شروع چانک جدید با header دوباره (یا بدون header؟) + else: + current += block + + # اضافه کردن آخرین چانک + if current.strip() and current.strip() != header.strip(): + chunks.append(current.rstrip()) + + # footer + if footer and chunks: + last = chunks[-1] + if len(last) + len(footer) <= self.max_len: + chunks[-1] = last + footer + else: + chunks.append(footer) + + return chunks + + def form_search_in_law(self, header: str, sections: List[SingleSearchData], footer:str=None) -> List: """ خروجی به صورت چانک بدون دکمه هر خروجی لینک دارد برش امن لینک ها و اده ها """ + footer = '\n\n'+footer chunks = [] - current = f"برای پرسش: {title}\n\n" + current = header for i, data in enumerate(sections, start=1): - sec_text = data.get("content", "") - idx = data.get("id") + sec_text = data.content + idx = data.id # ساخت ref کامل ref = self.__make_link_qs(src=idx) @@ -166,6 +213,13 @@ class Formatter: if current.strip(): chunks.append(current.rstrip()) + if footer : + last = chunks[-1] + if len(last) + len(footer) <= self.max_len: + chunks[-1] = last + footer + else: + chunks.append(footer) + return chunks def form_law_chat(self, answer_text: str): @@ -217,27 +271,24 @@ class Formatter: return chunks - async def form_title_repeated(self, data: List[Dict[str, str]]): - if len(data) == 0: + async def form_title_repeated(self, _input: List[TitleRepeat]) -> List: + if len(_input) == 0: return ["هیچ عنوان تکراری و یا حتی مشابه یافت نشد."] chunks = [] current = "نزدیک‌ترین عناوین مشابه عنوان قانون موارد زیر می باشد:\n\n" - for i, item in enumerate(data, start=1): - title = item.get("title", "") - sec_id = item.get("id", "") - score = item.get("score", "") + for i, item in enumerate(_input, start=1): - if not title or not sec_id: + if not item.title or not item.id: continue - ref = self.__make_link_qq(src=sec_id) + ref = self.__make_link_qq(src=item.id) # بلوک کامل: عنوان + لینک — هر دو در یک بلوک غیرقابل تقسیم # block = f"{i}. {title}(وزن {score})\n{ref}\n" block = ( - f"{self.number(i)} {self.bold(title)}؛ میزان تشابه: %{score} ؛{ref}\n" + f"{self.number(i)} {self.bold(item.title)}؛ میزان تشابه: %{item.score} ؛{ref}\n" ) # اگر اضافه کردن این بلوک باعث overflow شود → چانک قبلی را ذخیره و current را ریست کن @@ -263,11 +314,12 @@ class Formatter: # print(f'Found implicit source(s): {links}') return ", ".join(links) # جایگزینی همه کدها با لینک‌هایشان - async def form_chat(self, llm_text: str, header: str): + async def form_chat(self, llm_text: str, header: str, footer: str=None): """ answer_text: متن خروجی مدل که داخلش عبارت‌های مثل (منبع: qs2117427) وجود دارد """ - + if footer: + footer = '\n\n'+footer # الگو برای تشخیص هر پرانتز که شامل یک یا چند کد باشد # مثلا: (qs123) یا (qs123, qs456, qs789) pattern = r"\((?:منبع[:: ]+)?([a-zA-Z0-9_, ]+)\)" @@ -298,18 +350,26 @@ class Formatter: if current.strip(): chunks.append(current.strip()) + if footer and chunks: + last = chunks[-1] + if len(last) + len(footer) <= self.max_len: + chunks[-1] = last + footer + else: + chunks.append(footer) + return chunks async def form_llm_answer_chat(self, _input, header): if len(_input) > 0: - return await self.form_chat(llm_text=_input["text"], header=header) + return await self.form_chat(llm_text=_input, header=header) # _input['source'] return ["هیچ ماده مرتبطی یافت نشد!"] - async def form_subject_unity(self, - _input:Union[List[RuleRelation], str], - header="نتایج اولیه مغایرت های احتمالی :\n" - ): + async def form_subject_unity( + self, + _input: Union[List[RuleRelation], str], + header="نتایج اولیه مغایرت های احتمالی :\n", + ): if isinstance(_input, str): _input = self.form_law_chat(_input) return _input, [], [] @@ -322,13 +382,16 @@ class Formatter: for item in _input: title = item.db_rule.qanon_title groups[title].add(item.db_rule.section_id) - + current = header for idx, (qanon_title, section_ids) in enumerate(groups.items(), start=1): block_lines = [f"{self.number(idx)} در قانون {self.bold(qanon_title)}"] sample_items_by_section = {} for item in _input: - if item.db_rule.qanon_title == qanon_title and item.db_rule.section_id in section_ids: + if ( + item.db_rule.qanon_title == qanon_title + and item.db_rule.section_id in section_ids + ): sid = item.db_rule.section_id if sid not in sample_items_by_section: sample_items_by_section[sid] = item @@ -367,18 +430,22 @@ class Formatter: # Button: add *once* per qanon_title if qanon_title and qanon_title not in seen_qanon_titles: seen_qanon_titles.add(qanon_title) - buttons.append([ - { - "text": f"بررسی مغایرت با {qanon_title}", - "callback_data": f"subject_unities:qq:{qanon_title}" - } - ]) + buttons.append( + [ + { + "text": f"بررسی مغایرت با {qanon_title}", + "callback_data": f"subject_unities:qq:{qanon_title}", + } + ] + ) # Final flush - if current.strip() and (len(chunks) == 0 or current.strip() != header.rstrip()): + if current.strip() and ( + len(chunks) == 0 or current.strip() != header.rstrip() + ): chunks.append(current.rstrip()) - - input_dict = {item.db_rule.section_id : item for item in _input} + + input_dict = {item.db_rule.section_id: item for item in _input} mapping_data = defaultdict(list) for k, v in groups.items(): for i in v: @@ -386,11 +453,13 @@ class Formatter: return chunks, buttons, mapping_data - async def form_rule_making( - self, _input, header="گزاره های حقوقی زیر استخراج شد:\n\n" + self, _input, header="گزاره های حقوقی زیر استخراج شد:\n\n", footer=None ): if len(_input) > 0: + if footer: + footer = '\n\n'+footer + chunks = [] current = header @@ -404,8 +473,15 @@ class Formatter: if current.strip(): chunks.append(current.rstrip()) + if footer and chunks: + last = chunks[-1] + if len(last) + len(footer) <= self.max_len: + chunks[-1] = last + footer + else: + chunks.append(footer) return chunks + return ["هیچ گزاره حقوقی یافت و استخراج نشد!"] def get_asl(self, _in: str): @@ -458,19 +534,42 @@ class Formatter: chunks.append(current.rstrip()) return chunks + + async def form_constitution_low(self, input: Dict, _id, _header='نتایچ بررسی مغایرت با اصول مهم قانون اساسی\n\n'): + """ """ + + chunks = [] + current = "" + + for k, v in input.items(): + block = self.get_in_form_single(asl=k, _in_dict=v, _id=_id) + + # اگر این بلاک جا نشد → چانک جدید + if len(current) + len(block) > self.max_len: + chunks.append(current.rstrip()) + current = _header + block + else: + current += block + + + # آخرین چانک + if current.strip(): + chunks.append(current.rstrip()) + + return ''.join(chunks) + + async def form_ss_rules(self, _input: List[Dict], header): - async def form_ss_rules(self, _input:List[Dict], header): - if len(_input) > 1: chunks = [] current = header _i = 0 - + # -------- 1. group by qanon_id / qanon_title groups = defaultdict(set) for item in _input: - key = item['db_rule']['qanon_title'] - groups[key].add(item['db_rule']['section_id']) + key = item["db_rule"]["qanon_title"] + groups[key].add(item["db_rule"]["section_id"]) for qanon_title, ids in groups.items(): _i += 1 @@ -492,63 +591,64 @@ class Formatter: return ["هیچ ماده مرتبطی یافت نشد!"] - - - async def form_conflict_detection(self, - _input:RuleRelation, header="نتیجه تشخیص مغایرت :\n" - ): + async def form_conflict_detection( + self, _input: RuleRelation, header="نتیجه تشخیص مغایرت :\n" + ): current = header - + # ساخت لینک # _link = self.__make_link_qs(src=_input.db_rule.section_id) current += f"به صورت خلاصه {_input.conflict_detection.has_confict}\n" current += f"توضیحات : {_input.conflict_detection.explanation_of_conflict}\n" return current - - async def form_conflict_type_detection(self, - _input:RuleRelation, header="نتیجه تشخیص نوع مغایرت :\n" - ): + + async def form_conflict_type_detection( + self, _input: RuleRelation, header="نتیجه تشخیص نوع مغایرت :\n" + ): current = header - + # ساخت لینک # _link = self.__make_link_qs(src=_input.db_rule.section_id) current += f"به صورت خلاصه {_input.conflict_type_detection.conflict_type}\n" current += f"توضیحات : {_input.conflict_type_detection.explanation_of_type}\n" return current - - async def form_relation_identification(self, - _input:RuleRelation, header="نتیجه رابطه مغایرت :\n" - ): + + async def form_relation_identification( + self, _input: RuleRelation, header="نتیجه رابطه مغایرت :\n" + ): current = header - + # ساخت لینک # _link = self.__make_link_qs(src=_input.db_rule.section_id) current += f"به صورت خلاصه {_input.relation_identification.relation_type}\n" current += f"توضیحات : {_input.relation_identification.reasoning}\n" return current - - async def form_evaluation(self, - _input:Evaluation, header="نتیجه نهایی بررسی مغایرت :\n" - ): + + async def form_evaluation( + self, _input: Evaluation, header="نتیجه نهایی بررسی مغایرت :\n" + ): current = header - + # ساخت لینک # _link = self.__make_link_qs(src=_input.db_rule.section_id) current += f"1. آیا ارزیابی وحدت موضوع صحیح است؟ {_input.is_subject_unity_assessment_correct}\n" current += f"2. آیا ارزیابی تشخیص نوع درست است ؟ {_input.is_conflict_detection_correct}\n" current += f"3. آیا ارزیابی نوع درست است ؟ {_input.is_conflict_type_detection_correct}\n" - current += f"4. رابطه مغایرت چطور؟ {_input.is_relation_type_detection_correct}\n" + current += ( + f"4. رابطه مغایرت چطور؟ {_input.is_relation_type_detection_correct}\n" + ) current += f"5. نوع رابطه ؟ {_input.valid_relation_type}\n" current += f"6.توضیح بیشتر: {_input.comments}\n" return current - async def from_law_writing_policy(self, _input_dict: Dict, header:str) -> List[str]: - f_list = [ - self.bold(header)] + async def from_law_writing_policy( + self, _input_dict: Dict, header: str + ) -> List[str]: + f_list = [self.bold(header)] _str = { "analyze": "گزارش تحلیلی بندبه‌بند", "strength": "بیان نقاط قوت", @@ -562,59 +662,57 @@ class Formatter: f_list += [_title] # f_list += ['\n'] f_list += [v] - f_list += ['\n'] + f_list += ["\n"] return ["\n".join(f_list)] - - - class RequestManager: - def __init__(self, - host_url:str, - url_time_out=1200, - step_time_out=600, - ): - self.host_url = host_url + def __init__( + self, + host_url: str, + url_time_out=1200, + step_time_out=600, + ): + if host_url.endswith('/'): + self.host_url = host_url + else: + self.host_url = host_url + '/' + self.url_time_out = url_time_out self.step_time_out = step_time_out - TASK_URL ={ + TASK_URL = { # stream - "":"/stream/chat_logical", - + "": "/stream/chat_logical", # none-stream - "":"/conflict/general_policy/qs_unity", - "":"/conflict/all_qanon/qs_unity", - "":"/conflict/general_policy/unity_eval", - "":"/conflict/law_writing_policy", - "":"/conflict/constitution", - "":"/rule_making", - "":"/chat", - "":"/talk", - "":"/semantic_search/chat_logical", - "":"/semantic_search/run_chat", - "":"/semantic_search/run_semantic_search", + "": "/conflict/general_policy/qs_unity", + "": "/conflict/all_qanon/qs_unity", + "": "/conflict/general_policy/unity_eval", + "": "/conflict/law_writing_policy", + "": "/conflict/constitution", + "": "/rule_making", + "": "/chat", + "": "/talk", + "": "/semantic_search/chat_logical", + "": "/semantic_search/run_semantic_search", + "": "/semantic_search/run_chat", } - async def get_result( self, payload, - url :str, - section_id:str='qs_10001', - mode_type='bale', - ): - - _url = self.host_url+url - print( - f'get_result _url {_url}' - ) + url: str, + section_id: str = "qs_10001", + mode_type="bale", + ): + if url.startswith('/'): + url = url[1:] + + _url = self.host_url + url + print(f"get_result _url {_url}") try: async with httpx.AsyncClient(timeout=self.url_time_out) as client: - response = await client.post( - url=_url, json=payload - ) + response = await client.post(url=_url, json=payload) response.raise_for_status() data = response.json() result = data.get("result", "❌ پاسخی دریافت نشد") @@ -625,11 +723,10 @@ class RequestManager: print(f"❌ خطای RAG:\n{str(e)}") return "❌ ارتباط با سرور قطع می‌باشد" - async def stream_result( self, - url :str, - payload : Dict, + url: str, + payload: Dict, ): """ هر مرحله شامل: @@ -638,16 +735,15 @@ class RequestManager: data : "داده در این مرحله" } """ + if url.startswith('/'): + url = url[1:] + timeout = httpx.Timeout(self.step_time_out, read=self.url_time_out) - _url = self.host_url+url + _url = self.host_url + url async with httpx.AsyncClient(timeout=timeout) as client: # ارسال درخواست به صورت Stream - async with client.stream( - "POST", - url=_url, - json=payload - ) as r: + async with client.stream("POST", url=_url, json=payload) as r: # بررسی وضعیت پاسخ if r.status_code != 200: print(f"Error: {r.status_code}") @@ -799,38 +895,6 @@ def make_link_qs(src, ref_text=REF_TEXT): return f"[{ref_text}]({QS_WEB_LINK}{src})" -def encode_uc(update: BaleUpdate) -> str: - if update.message: - user = update.message.from_user - chat = update.message.chat - - elif update.callback_query: - user = update.callback_query.from_user - chat = update.callback_query.message.chat - - else: - return "unknown" - - username = user.username or user.id - chat_id = chat.id # ✅ فقط chat_id - - return f"{username}:{chat_id}" - - -def decode_uc(uc_id: str) -> dict: - """ - ورودی: 'username:chat_id' یا 'user_id:chat_id' - خروجی: {'username': ..., 'chat_id': ...} - """ - - try: - username, chat_id = uc_id.split(":", 1) - - return (username, int(chat_id) if chat_id.isdigit() else chat_id) - - except ValueError: - raise ValueError(f"decode_uc") - async def get_from_gpl(in_dict: Dict) -> List[str]: f_list = [] @@ -861,17 +925,19 @@ def cer(ref: str, hyp: str) -> float: for j in range(1, n + 1): cur = dp[j] dp[j] = min( - dp[j] + 1, # deletion - dp[j - 1] + 1, # insertion - prev + (ref[i - 1] != hyp[j - 1]) # substitution + dp[j] + 1, # deletion + dp[j - 1] + 1, # insertion + prev + (ref[i - 1] != hyp[j - 1]), # substitution ) prev = cur - return (dp[n] / m) * 100 + return (dp[n] / m) * 100 + import nltk from nltk.metrics import edit_distance + def cer_ntlk(exist: str, new: str) -> float: """ این روش دقیق‌تر است، چون تعداد کاراکترهای اضافی یا کم در متن طولانی، @@ -879,7 +945,8 @@ def cer_ntlk(exist: str, new: str) -> float: """ # edit distance روی کلمات return round(float(1 - edit_distance(new, exist) / len(exist)) * 100, 2) - + + def wer_nltk(new: str, exist: str) -> float: new = new.split() exist = exist.split() @@ -887,43 +954,6 @@ def wer_nltk(new: str, exist: str) -> float: return round(float(1 - edit_distance(new, exist) / len(exist)) * 100, 2) -async def title_repeated( - qanontitle, search_range: int = 10, url=f"http://localhost:8010/v1/indices/qaqanon/search" -): - """ - - باید با سرویس از حاج آقا گرفته شود - Fetch similar titles from the custom Elasticsearch-like REST API. - """ - # "/majles/similar/title/qaqanon/0/10/none" - # headers["Authorization"]="GuestAccess" - headers = {"accept": "application/json", "Content-Type": "application/json"} - - body = { - "query": qanontitle, # - "from_": 0, - "size": search_range+10, - "track_total_hits": True, - } - - response = requests.request("POST", url, headers=headers, json=body, timeout=20) - - if response.status_code != 200: - print("ERROR:", response.status_code) - print(response.text) - else: - data = response.json() - ids = [] - # print('---------------------------------------> max_score', max_score) - # print(data["hits"]) - - for i in data["hits"]["hits"]: - title = i["_source"]["title"] - ids.append( - {"title": title, "id": i["_source"]["id"], "score" :wer_nltk(exist=title, new=qanontitle)} - # {"title": title, "id": i["_source"]["id"], "score" :cer_ntlk(exist=title, new=qanontitle)} - ) - - return sorted(ids, key=lambda x: x['score'], reverse=True)[:search_range] def normalize_persian(text: str) -> str: @@ -1126,7 +1156,7 @@ async def get_form_gp_old(_inputs: Union[List[RuleRelation], List]): ) if current.strip(): chunks.append(current.rstrip()) - + else: chunks = ["هیچ مغایرتی یافت نشد."] return chunks, _button @@ -1405,5 +1435,3 @@ def chunked_simple_text(answer_text): chunks.append(current.strip()) return chunks - - diff --git a/core/operation.py b/core/operation.py new file mode 100644 index 0000000..61b6ec4 --- /dev/null +++ b/core/operation.py @@ -0,0 +1,217 @@ +from core.core import RequestManager, wer_nltk +from core.base_model import * +from router.bale.base_model import * +import requests + +""" +روند هر مرحله در اینجا مشخص می شود و داده و خروجی و پردازش در اینجا انجام می شود +""" +__all__ = ["Operation"] + + +class Operation: + def __init__(self, request_manager: RequestManager): + self.request_manager = request_manager + + async def search_in_law( + self, query: str, limit: int, rerank_model: str, embed_model: str + ) -> BMNewSemanticSearchOutput: + """ + فقط منطق – بدون هیچ وابستگی به Bale یا User + """ + + result = await self.request_manager.get_result( + payload={ + "query": query, + "limit": limit, + "rerank_model": rerank_model, + "embed_model": embed_model, + }, + url="new/semantic_search", + ) + + return BMNewSemanticSearchOutput.parse_obj(result) + + async def stream_search_in_law( + self, query: str, limit: int, rerank_model: str, embed_model: str + ): + """ + فقط منطق – بدون هیچ وابستگی به Bale یا User + """ + async for data in self.request_manager.stream_result( + payload={ + "query": query, + "limit": limit, + "rerank_model": rerank_model, + "embed_model": embed_model, + }, + url="new/semantic_search", + ): + yield data + + async def stream_rule_making(self, query, llm_name, effort): + async for data in self.request_manager.stream_result( + payload={ + "query": query, + "query_id": "qs12357498", + "llm_effort": effort, + "llm_model_name": llm_name, + # "llm_api_url" + # "llm_api_key" + }, + url="/single/rule_making", + ): + + yield data + + async def stream_chat_in_law(self, query, limit, effort, mode_type="bale"): + async for data in self.request_manager.stream_result( + payload={ + "section_content": query, + "effort": effort, + "limit": limit, + "mode_type": mode_type, + }, + url="/single/semantic_search/run_chat", + ): + + yield data + + async def stream_rule_semantic_search( + self, + queries: List, + filter_qanon_ids: List, + limit_rerank: int, + embed_model="jinaai/jina-colbert-v2", + rerank_model="BAAI/bge-reranker-v2-m3", + metadata={}, + limit_cos=100, + ): + async for data in self.request_manager.stream_result( + payload={ + "queries": queries, + "filter_qanon_ids": filter_qanon_ids, + "embed_model": embed_model, + "rerank_model": rerank_model, + "metadata": metadata, + "limit_rerank": limit_rerank, + "limit_cos": limit_cos, + }, + url="/single/rule_semantic_search", + ): + + yield data + + async def chat_in_law(self, query, effort, limit, mode_type="bale") -> ChatLaw: + result = await self.request_manager.get_result( + payload={ + "section_content": query, + "effort": effort, + "limit": limit, + "mode_type": mode_type, + }, + url="/single/semantic_search/run_chat", + ) + print(f"chat_in_law {result}") + return ChatLaw.parse_obj(result) + + async def title_repeated( + self, + qanontitle, + search_range: int = 10, + # url=f"http://localhost:8010/v1/indices/qaqanon/search", + url=f"http://localhost/api/elp/v1/indices/qaqanon/search", + ) -> List[TitleRepeat]: + """ + - باید با سرویس از حاج آقا گرفته شود + Fetch similar titles from the custom Elasticsearch-like REST API. + """ + # "/majles/similar/title/qaqanon/0/10/none" + # headers["Authorization"]="GuestAccess" + headers = {"accept": "application/json", "Content-Type": "application/json"} + + body = { + "query": qanontitle, # + "from_": 0, + "size": search_range + 10, + "track_total_hits": True, + } + + response = requests.request("POST", url, headers=headers, json=body, timeout=20) + print(f"title_repeated -> {response}") + if response.status_code != 200: + print("ERROR:", response.status_code) + print(response.text) + else: + data = response.json() + ids = [] + # print('---------------------------------------> max_score', max_score) + # print(data["hits"]) + + for i in data["hits"]["hits"]: + title = i["_source"]["title"] + ids.append( + TitleRepeat( + title=title, + id=i["_source"]["id"], + score=wer_nltk(exist=title, new=qanontitle), + ) + ) + + return sorted(ids, key=lambda x: x.score, reverse=True)[:search_range] + + async def talk(self, query) -> str: + result = await self.request_manager.get_result( + payload={ + "user_input": query, + }, + url="/talk", + ) + return result + + async def conflict_qanon_asasi_low(self, query, effort, limit, mode_type="bale"): + async for data in self.request_manager.stream_result( + payload={ + "section_content": query, + "effort": effort, + "limit": limit, + "mode_type": mode_type, + }, + url="/new/conflict/constitution_low", + ): + yield data + + async def conflict_qanon_asasi_steps(self, query, effort, limit, mode_type="bale"): + _result = await self.request_manager.get_result( + payload={ + "section_content": query, + "effort": effort, + "limit": limit, + "mode_type": mode_type, + }, + url="/new/conflict/constitution", + ) + + return _result + + async def stream_logical_chat_in_law(self, query, effort, metadata, limit): + async for data in self.request_manager.stream_result( + payload={ + "section_content": query, + "effort": effort, + "metadata": metadata, + "limit":limit + }, + url="/new/stream/chat_logical", + ): + yield data + + async def conflict_law_writing_policy(self, query, effort): + _result = await self.request_manager.get_result( + payload={ + "section_content": query, + "effort": effort, + }, + url="/conflict/law_writing_policy", + ) + return _result diff --git a/main.py b/main.py index 28edb20..7b424ef 100755 --- a/main.py +++ b/main.py @@ -6,6 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware from router.bale.bale import router as bale_router from router.bale.bale import initialize_webhook from router.bale.bale_handle import BaleBot, UserManager +from core.operation import Operation from dotenv import load_dotenv import os @@ -67,8 +68,12 @@ async def lifespan(app: FastAPI): app.state.request_manager = RequestManager( host_url=BACK_END_URL, ) + app.state.operation = Operation( + request_manager=app.state.request_manager, + ) # بله بات bale_bot = BaleBot( + operation=app.state.operation, user_manager=app.state.user_manager, es_helper=app.state.es_helper, es_index_name=app.state.es_index_name, @@ -78,7 +83,7 @@ async def lifespan(app: FastAPI): request_manager = app.state.request_manager, ) app.state.bale_bot = bale_bot - print("=== Bale-Bot Initialized ===") + print("✅✅✅ Bale-Bot Initialized ✅✅✅") yield # برنامه در این حالت اجرا می‌شود diff --git a/proje_information.md b/proje_information.md new file mode 100644 index 0000000..8df9bc8 --- /dev/null +++ b/proje_information.md @@ -0,0 +1,22 @@ + +Operation (logic layer) +│ +├── handle_search_in_law(...) +│ ├── دریافت ورودی خام +│ ├── فراخوانی request_manager +│ ├── ساخت خروجی منطقی (data) +│ └── بدون وابستگی به بله +│ +└── return OperationResult + +BaleBot (delivery / interface layer) +│ +├── دریافت user +├── صدا زدن Operation +├── تبدیل خروجی به متن + دکمه +└── ارسال به بله + + +# Logic & Flow +Operation → منطق، پردازش، گرفتن دیتا، تصمیم‌گیری +BaleBot → ورودی/خروجی، ارتباط با بله، فرمت پیام، دکمه‌ها، مدیریت state کاربر \ No newline at end of file diff --git a/requierments.txt b/requierments.txt index 017b6be..6a52e29 100755 --- a/requierments.txt +++ b/requierments.txt @@ -1,3 +1,4 @@ elasticsearch==8.13.2 nltk pydantic +fast-api \ No newline at end of file diff --git a/router/bale/bale_handle.py b/router/bale/bale_handle.py index 6b186a4..13b443b 100755 --- a/router/bale/bale_handle.py +++ b/router/bale/bale_handle.py @@ -1,128 +1,58 @@ ################# modularity ### import from external-package -from fastapi import FastAPI, Request, HTTPException -import requests, logging, asyncio, httpx, os, uuid, traceback, orjson, copy, uvicorn, time, re -from dotenv import load_dotenv -from pathlib import Path -from time import sleep -from enum import Enum -import time -from typing import Dict, Optional -from dataclasses import dataclass +import requests, logging, asyncio, httpx, os, uuid, traceback, orjson, copy, uvicorn, time, re, random +from typing import Iterable, Any, List, Dict, Optional +from core.base_model import * ### import from internal-file from router.bale.base_model import * from router.bale.bale_buttons import * from router.bale.bale_massages import * +from router.bale.bale_manager import UserManager +from core.operation import Operation from core.core import * from core.static import * -from router.bale.config import STATE_CONFIG, STATE, BUSY_TEXT,MAIN_BUTTON, STATE_REGISTERY, build_buttons_form -from typing import Iterable, Any, List +from router.bale.config import * -def extract_user_info(update: BaleUpdate) -> dict: - uc_id = encode_uc(update) - user_id, chat_id = decode_uc(uc_id) - if update.message: - u = update.message.from_user - return { - "uc_id": str(uc_id), - "chat_id": int(chat_id), - "user_id": user_id, - "username": u.username, - "first_name": u.first_name, - "last_name": u.last_name or "", - "is_bot": u.is_bot, - "update": update, - } - - if update.callback_query: - u = update.callback_query.from_user - return { - "uc_id": str(uc_id), - "chat_id": int(chat_id), - "user_id": user_id, - "username": u.username, - "first_name": u.first_name, - "last_name": "", - "is_bot": u.is_bot, - "update": update, - } - - raise ValueError("No user info in update") - - -class UserManager: - def __init__(self): - self.users: Dict[str, BaleUser] = {} - - def get_or_create(self, update: BaleUpdate) -> BaleUser: - user_data = extract_user_info(update) - - uc_id = user_data["uc_id"] - - if uc_id not in self.users: - self.users[uc_id] = BaleUser( - **user_data, - ) - user = self.users[uc_id] - user.update = update - return user - - -class BaleBot: +class BaleBotGeneral: """ - input → set_state → render_user_state + کلاس اصلی ارتباط با Bale و متد های اصلی ارتباط """ def __init__( self, - user_manager: UserManager, - es_index_name: str, token: str, - es_helper: ElasticHelper, - formatter: Formatter, - back_end_url: str, - request_manager: RequestManager, + send_message_url: Optional[str] = None, + delete_message_url: Optional[str] = None, + update_message_url: Optional[str] = None, ): - self.formatter = formatter - self.request_manager = request_manager - self.es_helper = es_helper self.token = token - self.es_index_name = es_index_name - self.user_manager = user_manager - self.max_limit = 100 - self.back_end_url = back_end_url - self.update_message_url = ( - f"https://tapi.bale.ai/bot{self.token}/editMessageText" + + self.send_message_url = ( + send_message_url or f"https://tapi.bale.ai/bot{self.token}/sendMessage" + ) + self.delete_message_url = ( + delete_message_url or f"https://tapi.bale.ai/bot{self.token}/deleteMessage" + ) + self.update_message_url = ( + update_message_url + or f"https://tapi.bale.ai/bot{self.token}/editMessageText" ) - self.send_message_url = f"https://tapi.bale.ai/bot{self.token}/sendMessage" - # self.user_state = self.make_state() - async def render_user_state(self, user: BaleUser, run_internal=None): - """Buse سیستم""" + async def delete_bale_message(self, chat_id, message_id): + payload = { + "chat_id": chat_id, + "message_id": message_id, + } + try: - state_detail = user.state_detail + r = requests.post(self.delete_message_url, json=payload) + print(f"Delete Message Status code:", r.status_code) - if user.input_query == "": - await self.send_message_helper( - user=user, - text=state_detail.message, - end_buttons=state_detail.end_buttons, - ) - return {"ok": True} - # if user.input_query != "" and not run_internal and - - if run_internal == "subject_unities": - await self.handle_advanced_check_conflict(user) - - elif not run_internal and state_detail.handler: - handler = getattr(self, state_detail.handler) - await handler(user) - except: - await self.handle_chat_in_law(user) - - return {"ok": True} + except Exception: + print("ERROR in Delete Message Status code:") + traceback.print_exc() async def update_message_to_bale( self, @@ -134,7 +64,7 @@ class BaleBot: payload = { "chat_id": user.chat_id, - "message_id": user.last_message_id, + "message_id": user.active_message_id, "text": text, "reply_markup": { # "keyboard": reply_markup, @@ -150,14 +80,11 @@ class BaleBot: r = requests.post(self.update_message_url, json=payload) if not r.ok: - print("Bale API Error:", r.status_code, r.text) - - r_ = r.json() - print(f"Send:", r.status_code) - # user.last_message_id = int(r_["result"]["message_id"]) + print("ERROR in Update Message Bale API Error:", r.status_code, r.text) + print(f"Update Message Status code:", r.status_code) except Exception: - print("ERROR in update_message_to_bale:") + print("ERROR in Update Message Status code:") traceback.print_exc() async def send_message_helper( @@ -169,6 +96,7 @@ class BaleBot: structed_output: List = None, end_buttons: List = [], reply_markup: List = BUTTON_TEXT_TO_CALLBACK_LIST, + is_save_active_message_id=False, ): _i = 0 @@ -184,7 +112,7 @@ class BaleBot: ) if update_message == True: - if user.last_message_id == 0: + if user.active_message_id == 0: await self.send_message_helper( user=user, update_message=False, @@ -193,6 +121,7 @@ class BaleBot: chunked_text=chunked_text, structed_output=structed_output, reply_markup=reply_markup, + is_save_active_message_id=is_save_active_message_id, ) else: if text: @@ -208,6 +137,7 @@ class BaleBot: buttons=end_buttons, reply_markup=reply_markup, ) + else: if structed_output: for i, item in enumerate(structed_output, start=1): @@ -220,6 +150,7 @@ class BaleBot: text=item[0], buttons=item[1], reply_markup=reply_markup, + is_save_active_message_id=is_save_active_message_id, ) if chunked_text: @@ -234,7 +165,9 @@ class BaleBot: text=item, buttons=_buttons, reply_markup=reply_markup, + is_save_active_message_id=is_save_active_message_id, ) + await asyncio.sleep(0.05) if text: await self.send_message_to_bale( @@ -242,6 +175,7 @@ class BaleBot: text=text, buttons=end_buttons, reply_markup=reply_markup, + is_save_active_message_id=is_save_active_message_id, ) async def send_message_to_bale( @@ -250,10 +184,9 @@ class BaleBot: text: str, buttons: List = [], reply_markup: List = BUTTON_TEXT_TO_CALLBACK_LIST, + is_save_active_message_id=False, ): - print(f"send_message_to_bale--- {text}") - print(f"send_message_to_bale buttons--- {buttons}") payload = { "chat_id": user.chat_id, "text": text, @@ -266,20 +199,115 @@ class BaleBot: if buttons: payload["reply_markup"]["inline_keyboard"] = buttons + try: r = requests.post(self.send_message_url, json=payload) if not r.ok: - print("Bale API Error:", r.status_code, r.text) + print("ERROR in Send Message Bale API Error:", r.status_code, r.text) r_ = r.json() - print(f"Send:", r.status_code) - user.last_message_id = int(r_["result"]["message_id"]) + print(f"Send Message Status code:", r.status_code) + + if is_save_active_message_id: + user.active_message_id = int(r_["result"]["message_id"]) except Exception: - print("ERROR in send_message_to_bale:") + print("ERROR in Send Message Status code:") traceback.print_exc() + +class BaleBot(BaleBotGeneral): + """ + input → set_state → render_user_state + """ + + def __init__( + self, + user_manager: UserManager, + operation: Operation, + es_index_name: str, + token: str, + es_helper: ElasticHelper, + formatter: Formatter, + back_end_url: str, + request_manager: RequestManager, + ): + super().__init__( + token=token, + ) + self.ui_handler = UIState( + delay_time=0.4, + token=token, + ) + self.freeze_limit = 3 + self.operator = operation + self.formatter = formatter + self.request_manager = request_manager + self.es_helper = es_helper + self.token = token + self.es_index_name = es_index_name + self.user_manager = user_manager + self.max_limit = 100 + self.back_end_url = back_end_url + + """ + deleteMessage + message_id + chat_id + """ + # self.user_state = self.make_state() + + # ------------------------------------------------------------------ + # توابعی که در والد BaleBotGeneral هستند + # ------------------------------------------------------------------ + async def delete_bale_message(self, **kwargs): + await super().delete_bale_message(**kwargs) + + async def update_message_to_bale(self, *args, **kwargs): + await super().update_message_to_bale(*args, **kwargs) + + async def send_message_helper(self, *args, **kwargs): + await super().send_message_helper(*args, **kwargs) + + async def send_message_to_bale(self, *args, **kwargs): + await super().send_message_to_bale(*args, **kwargs) + + + # ------------------------------------------------------------------ + # توابع همین کلاس + # ------------------------------------------------------------------ + + async def render_user_state(self, user: BaleUser): + """Bus سیستم""" + try: + + print(f"user.input_query.render_user_state >{user.input_query}<") + print(f"user.state >{user.state_detail.state}<") + if user.input_query == "": + await self.send_message_helper( + user=user, + text=user.state_detail.message, + end_buttons=user.state_detail.end_buttons, + ) + return {"ok": True} + # if user.input_query != "" and not run_internal and + # and user.last_input_query == "" + + # if user.last_input_query != "" and user.input_query != "" and "subject_unities": + # await self.handle_advanced_check_conflict(user) + + if user.state_detail.handler: + user.is_processing_lock = True + handler = getattr(self, user.state_detail.handler) + await handler(user) + + except Exception as e: + print("ERROR in handle_chat:", str(traceback.print_exc())) + await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) + + return {"ok": True} + async def render_update(self, update: BaleUpdate): """ مهمترین تابع ربات بله @@ -291,54 +319,62 @@ class BaleBot: # 2️⃣ ساخت یا بازیابی user user = self.user_manager.get_or_create(update) - print(f"render_update user.state_detail.state -> {user.state_detail}") + print(f"render_update user -> {user.uc_id} user.is_processing_lock {user.is_processing_lock }") if user.is_processing_lock == False: if update.message: - print(f"render_message") + user.message_limit = 0 user.input_query = str(user.update.message.text or "").strip() # async def render_message(self, user: BaleUser): if user.input_query in ("/start", "main", "بازگشت به خانه"): - await self.handle_main(user) user.input_query = "" + await self.handle_main(user) return {"ok": True} - if user.input_query == "جستجو": + elif user.input_query == "جستجو": user.state_detail = STATE_CONFIG["search_in_law"] user.input_query = "" - if user.input_query == "گفتگو": + elif user.input_query == "گفتگو": user.state_detail = STATE_CONFIG["chat_in_law"] user.input_query = "" - # --- WorkFlow Logic by State await self.render_user_state(user) return {"ok": True} - + if update.callback_query: """ اینجا فقط باید مرحله و state عوض شود و داده ای وارد نمیشود!!! برای سوال پرسیدن از کاربر و ... """ - print("render_callback") + + user.message_limit = 0 user.is_call_back_query = True user.call_back_query = user.update.callback_query.data # user.update.callback_query.data run_internal = None if user.call_back_query == "main": - user.input_query = "" await self.handle_main(user) return {"ok": True} # Dynamic Change Options if user.call_back_query == "more_limit": user.limit += 10 + user.input_query = user.last_input_query + if user.state_detail == STATE_CONFIG["logical_chat_in_law"]: + user.state_detail = STATE_CONFIG["chat_in_law"] # Dynamic Change Options elif user.call_back_query == "more_effort": user.effort = "medium" + user.input_query = user.last_input_query + + elif user.call_back_query == "logical_chat_in_law": + # user.effort = "medium" + user.input_query = user.last_input_query + user.state_detail = STATE_CONFIG["logical_chat_in_law"] elif user.call_back_query.startswith("subject_unities:"): run_internal = "subject_unities" @@ -348,12 +384,21 @@ class BaleBot: else: user.state_detail = STATE_CONFIG[user.call_back_query] - await self.render_user_state(user, run_internal) + await self.render_user_state(user) return {"ok": True} - else: + if user.call_back_query == "main": + await self.handle_main(user) + return {"ok": True} + # beta-mode - await self.send_message_helper(user=user, text=BUSY_TEXT) + if user.message_limit < self.freeze_limit: + await self.send_message_helper( + user=user, + text=BUSY_TEXT, + end_buttons=[[{"text":"توقف عملیات در حال اجراء", "callback_data":"main"}]] + ) + user.message_limit += 1 return {"ok": True} @@ -362,9 +407,15 @@ class BaleBot: pass async def handle_main(self, user: BaleUser): + # if user.input_query != "": + # "اگر کاربر پیام داد در حالت عادی به چت منتقل شود" + # user.state_detail = STATE_CONFIG["chat_in_law"] + # await self.render_user_state(user) + # return {"ok": True} + """ + اگر کاربر پیامی نداده بود صفحه اصلی نمایش داده شود و داده ها پاک شود + """ # ریست کردن پشته‌ها و نتایج - """ """ - ################################### SAVE2ELASTIC ################################### user.last_query = "" user.last_result = "" user.stack.clear() @@ -372,11 +423,10 @@ class BaleBot: user.sub_state = "" user.limit = 10 user.effort = "low" - user.last_message_id = 0 - - message = """👋 سلام دوست عزیز! 🤗 -به دستیار هوشمند قانون یار خوش آمدید! -فقط کافیه به من بگید چه کمکی از دستم برمیاد!""" + user.active_message_id = 0 + user.message_limit = 0 + + message = "من ربات تست ام" await self.send_message_helper( user=user, @@ -384,42 +434,210 @@ class BaleBot: end_buttons=MAIN_BUTTON, ) + async def choice_button(self, user: BaleUser, choice_buttons): + # handle_conflict_law_writing_policy + # handle_conflict_qanon_asasi + # handle_conflict_general_policy + # handle_conflict_all_qavanin + result = await self.send_message_helper( + user=user, + text="یک مورد را انتخاب کنید:", + end_buttons=choice_buttons, + ) + + """ + -> Response from render message -> return + """ + return None + async def handle_search_in_law(self, user: BaleUser): user.is_processing_lock = True - user.last_query = user.input_query + print(f"handle_search_in_law -> {user.input_query}") + self.ui_handler.create(user=user, main_text="در حال جستجو") + _buttons = [[HOME_BUTTON]] try: - result = await self.request_manager.get_result( - payload={ - "section_content": user.input_query, - "limit": user.limit, - "effort": user.effort, - }, - url="/semantic_search/run_semantic_search", - ) + async for result in self.operator.stream_search_in_law( + query=user.input_query, + limit=user.limit, + rerank_model="BAAI/bge-reranker-v2-m3", + embed_model="BAAI/bge-m3", + ): + result = BMNewSemanticSearchOutput.model_validate(result["result"]) + if isinstance(result, BMNewSemanticSearchOutput): + self.ui_handler.cancel(user) - # print(f'result rag {result} {type(result["ss_answer"])}') - chunked_text_ = self.formatter.form_search_in_law( - title=user.input_query, - sections=result["answer"], - ) + chunked_text_ = self.formatter.form_search_in_law( + header=f"برای پرسش: {user.input_query}\n\n", + footer=f"مدت زمان بردارسازی {result.embed_model_time} مدت زمان تشابه یابی {result.cosine_similarity_time} مدت زمان تشابه یابی دقیق {result.rerank_time}", + sections=result.result, + ) - # print(f'chunked_text_ rag {chunked_text_}') - _buttons = [[HOME_BUTTON]] - if user.limit < self.max_limit: - _buttons.insert(0, [MORE_LIMIT_BUTTON]) + if user.limit < self.max_limit: + _buttons.insert(0, [MORE_LIMIT_BUTTON]) + + await self.send_message_helper( + user=user, + chunked_text=chunked_text_, + end_buttons=_buttons, + ) + + # else: + # heartbeat_task - await self.send_message_helper( - user=user, chunked_text=chunked_text_, end_buttons=_buttons - ) except Exception as e: print("ERROR in handle_chat:", str(traceback.print_exc())) await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) - finally: + data = { + "state": "search_in_law", + "contents": [i.content for i in result.result], + "scores": [i.score for i in result.result], + "ref_ids": [i.id for i in result.result], + "user_input": user.input_query, + "metadata": { + "embed_model_time": result.embed_model_time, + "cosine_similarity_time": result.cosine_similarity_time, + "rerank_time": result.rerank_time, + }, + } + data["metadata"].update(result.metadata) + + await self.save_to_db(user, data=data) + user.is_processing_lock = False + user.last_input_query = user.input_query user.input_query = "" + self.ui_handler.cancel(user) + + return {"ok": True} + + async def handle_search_in_law_rules(self, user: BaleUser): + + user.is_processing_lock = True + _status_show_map = { + "total_process": "تعداد کل موارد مورد بررسی", + "total_processed": "تعداد موارد بررسی شده", + } + total_process = len(user.last_result) * user.limit + es_data = None + es_metadata = None + print( + f"user.limit {user.limit}", + f"len(user.last_result) {len(user.last_result)}", + f"total_process*****s {total_process}", + ) + try: + if user.last_result: + print(f"/////////////user.last_result", len(user.last_result)) + await self.send_message_helper( + user=user, + text="در حال جستجو ...", + update_message=True, + is_save_active_message_id=True, + ) + + _buttons = [ + [HOME_BUTTON], + [{"text": "بررسی مغایرت", "callback_data": "not_yet"}], + ] + filter_qanon_ids = [] + # filter_qanon_ids باید از کاربر پرسیده شود + async for step_data in self.operator.stream_rule_semantic_search( + queries=[i.model_dump() for i in user.last_result], + filter_qanon_ids=filter_qanon_ids, + limit_rerank=user.limit, + ): + status = step_data.get("status") + data = step_data.get("data") + metadata = step_data.get("metadata") + # time + + if status == "end": + # print(f'*****data {data}') + es_data = data[status] + es_metadata = metadata + data = [ + SemanticSearchP2P.model_validate(i) for i in data[status] + ] + _res = self.formatter.form_search_in_law_rules( + header="نتایج جستجوی اجزاء در قانون\n", + body=data, + ) + print(f"len(_res) {len(_res) }") + if user.limit < self.max_limit: + _buttons.insert(0, [MORE_LIMIT_BUTTON]) + + if len(_res) == 1: + await self.send_message_helper( + user=user, + chunked_text=_res, + update_message=True, + end_buttons=_buttons, + ) + else: + for i, item in enumerate(_res): + print(i) + if i == 0: + print("start-batch") + await self.send_message_helper( + user=user, + text=item, + update_message=True, + ) + elif i == len(_res) - 1: + print("end-batch") + await self.send_message_helper( + user=user, + text=item, + end_buttons=_buttons, + ) + else: + print("middle-batch") + await self.send_message_helper( + user=user, + text=item, + ) + + user.active_message_id = 0 + + else: + # print( + # f"status {status}\nText {type(_status_show_map[status])}\ndata type {type(data[status])}" + # ) + if status == "total_processed": + _result = ( + _status_show_map[status] + + f": {data['total_processed']}/{total_process}" + ) + elif status == "total_process": + _result = ( + _status_show_map[status] + f": {data['total_process']}" + ) + + await self.send_message_helper( + user=user, + text=_result, + update_message=True, + is_save_active_message_id=True, + ) + else: + print("-----------------------No Found Any Last_data") + + if es_data: + _data = { + "state": "search_in_law_rules", + "result": es_data, + "metadata": {}, + } + _data["metadata"].update(es_metadata) + + await self.save_to_db(user, data=_data) + except Exception as e: + print("ERROR in handle_chat:", str(traceback.print_exc())) + await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) + finally: user.is_processing_lock = False return {"ok": True} @@ -427,195 +645,420 @@ class BaleBot: async def handle_rule_making(self, user: BaleUser): user.is_processing_lock = True - user.effort = "medium" + self.ui_handler.create(user=user, main_text="در حال بررسی") + + _buttons = [ + [ + { + "text": "مشابهت یابی در قانون", + "callback_data": "not_yet", # "search_in_law_rules", + } + ], + [HOME_BUTTON], + ] + _max_token = None try: - result = await self.request_manager.get_result( - payload={ - "section_content": user.input_query, - "limit": user.limit, - "effort": user.effort, - }, - url="/rule_making", - ) - print(f"handle_rule_making {result}") - - res_ = await self.formatter.form_rule_making(_input=result) - - _buttons = [[HOME_BUTTON]] - if user.effort != "medium": - _buttons.append([MORE_EFFORT_BUTTON]) - await self.send_message_helper( - user=user, chunked_text=res_, end_buttons=_buttons - ) - + f_data = None + f_metadata = None + async for step_data in self.operator.stream_rule_making( + query=user.input_query, llm_name="gpt-oss-20b", effort=user.effort + ): + status = step_data.get("status") + data = step_data.get("data") + metadata = step_data.get("metadata") + # time + + if status == "end": + self.ui_handler.cancel(user) + + f_data = data[status] + f_metadata = metadata + res_ = await self.formatter.form_rule_making( + _input=data[status], + footer=f'تعداد توکن های مصرف شده {_max_token}', + ) + + if user.effort != "medium": + _buttons.insert(0, [MORE_EFFORT_BUTTON]) + + await self.send_message_helper( + user=user, + chunked_text=res_, + end_buttons=_buttons, + ) + + user.last_result = [ + InputRule.model_validate(i) for i in data[status] + ] + if user.is_vip: + _result2 = await self.formatter.form_chat( + header=" *reasoning* :\n", + llm_text=metadata["llm_reason"], + + ) + + await self.send_message_helper( + user=user, + chunked_text=_result2, + ) + + else: + _max_token = data["max_token"] + if _max_token: + self.ui_handler.main_text = [ + f'تعداد توکن های مصرف شده {_max_token}', + ] except Exception as e: print("ERROR in handle_chat:", str(traceback.print_exc())) await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) finally: + if f_data: + + _data = { + "state": "rule_making", + "result": f_data, + "llm_reason": metadata["llm_reason"], + "metadata": { + "llm_token":_max_token + }, + } + print( + f'_data {_data}' + ) + _data["metadata"].update(f_metadata) + + await self.save_to_db(user, data=_data) + user.last_input_query = user.input_query user.input_query = "" user.is_processing_lock = False + self.ui_handler.cancel(user) return {"ok": True} + async def handle_chat_in_law(self, user: BaleUser): + + user.is_processing_lock = True + self.ui_handler.create(user=user, main_text=[ + "در حال تحلیل", + "در حال طبقه بندی", + "در حال تفکر", + "ساختار بندی", + ], + ) + # print('asyncio') + # sleep(80) + # print('wait') + _buttons = [[HOME_BUTTON]] + + try: + async for _result in self.operator.stream_chat_in_law( + query=user.input_query, effort=user.effort, limit=user.limit + ): + print(f"handle_chat_in_law **--**---*--*-*-*-***-**-*-*-*-** ") + self.ui_handler.cancel(user) + result = ChatLaw.model_validate(_result["result"]) + user.last_result = result + text_result = self.formatter.form_law_chat(result.answer) + + if result.answer_type == "legal_question": + _b = [ + { + "text": "بررسی عمیق تر", + "callback_data": "logical_chat_in_law", + } + ] + if user.limit < self.max_limit: + _b.append( + { + "text": "بررسی گسترده تر", + "callback_data": "more_limit", + } + ) + _buttons.insert(0, _b) + + await self.send_message_helper( + user=user, chunked_text=text_result, end_buttons=_buttons + ) + if user.is_vip: + print(f"user.is_vip {user.is_vip}") + reason_result = await self.formatter.form_chat( + header="model-reason", llm_text=result.llm_reason + ) + await self.send_message_helper( + user=user, + chunked_text=reason_result, + ) + except Exception as e: + print("ERROR in handle_chat:", str(traceback.print_exc())) + await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) + finally: + + print(f"result {type(result)}") + print(f"result.answer {type(result.answer)}") + print(f"result.ref_ids {type(result.ref_ids)}") + print(f"result.answer_type {type(result.answer_type)}") + _data = { + "state": "chat_in_law", + "result": result.answer, + "llm_reason": result.llm_reason, + "metadata": { + "ref_ids": result.ref_ids, + "answer_type": result.answer_type, + }, + } + + await self.save_to_db(user=user, data=_data) + + user.last_input_query = user.input_query + user.input_query = "" + user.is_processing_lock = False + self.ui_handler.cancel(user) + async def handle_qanon_title_repeat(self, user: BaleUser): user.is_processing_lock = True try: - result_ = await title_repeated(qanontitle=user.input_query) - res_ = await self.formatter.form_title_repeated(data=result_) - _buttons = [[HOME_BUTTON]] - await self.send_message_helper( - user=user, chunked_text=res_, end_buttons=_buttons + _result: List[TitleRepeat] = await self.operator.title_repeated( + qanontitle=user.input_query ) + print(f"*/*/*/*/*/*/*/*/*/*/*/result_ {_result}") + _res = await self.formatter.form_title_repeated(_input=_result) + + _buttons = [[HOME_BUTTON]] + + await self.send_message_helper( + user=user, chunked_text=_res, end_buttons=_buttons + ) + except Exception as e: print("ERROR in handle_chat:", str(traceback.print_exc())) await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) + finally: user.is_processing_lock = False - user.input_query = "" return {"ok": True} async def handle_talk(self, user: BaleUser): user.is_processing_lock = True try: - result = await self.request_manager.get_result( - payload={ - "user_input": user.input_query, - }, - url="/talk", - ) - # answer - # answer_type + _result = await self.operator.talk(query=user.input_query) await self.send_message_helper( - user=user, chunked_text=result['answer'], end_buttons=MAIN_BUTTON + user=user, text=_result, end_buttons=MAIN_BUTTON ) except Exception as e: - print("ERROR in handle_chat:", str(traceback.print_exc())) + print("ERROR in handle_talk:", str(traceback.print_exc())) await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) finally: user.is_processing_lock = False return {"ok": True} + async def handle_logical_chat_in_law(self, user: BaleUser): + user.is_processing_lock = True + + self.ui_handler.create(user=user, main_text=["در حال استخراج اجزاء"]) + + f_data = "" + _button = [[HOME_BUTTON]] + len_rules = 0 + try: + metadata = user.last_result["metadata"] + except: + metadata = {} + + try: + async for _data in self.operator.stream_logical_chat_in_law( + query=user.input_query, effort=user.effort, metadata=metadata, limit=20 + ): + status = _data.get("status") + data = _data.get("data") + # _metadata = _data.get("metadata") + print( + f"status {status}", + ) + + if status == "rules": + len_rules = data + self.ui_handler.main_text = [f"تعداد اجزاء استخراج شده {data}"] + + elif status == "semantic": + self.ui_handler.main_text = [ + f"مشابهت‌ یابی اجزاء {user.limit*len_rules}/{data}" + ] + + elif status == "llm": + self.ui_handler.main_text = ["ساخت پاسخ نهایی"] + self.ui_handler.cancel(user) + + _header = f"📝 پاسخ نهایی:\n" + f_data = data["text"] + + response = await self.formatter.form_llm_answer_chat( + data["text"], header=_header + ) + + await self.send_message_helper( + user, + chunked_text=response, + end_buttons=_button, + ) + if user.is_vip: + response2 = await self.formatter.form_chat( + data["llm_reason"], header="دلیل مدل برای خروجی" + ) + + await self.send_message_helper( + user, + chunked_text=response2, + ) + # break + + except Exception as e: + print("ERROR in handle_chat:", str(traceback.print_exc())) + await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) + + finally: + user.is_processing_lock = False + user.input_query = "" + self.ui_handler.cancel(user) + _data = { + "state": "logical_chat_in_law", + "result": f_data, + "llm_reason": data["llm_reason"], + "metadata": {}, + } + # _data['metadata'].update(_metadata) + await self.save_to_db(user=user, data=_data) + + + return {"ok": True} async def handle_conflict_qanon_asasi(self, user: BaleUser): user.is_processing_lock = True - user.last_message_id = 0 + user.active_message_id = 0 + try: print(f"effort=user.effort {user.effort}") - async for step_data in self.request_manager.stream_result( - payload={ - "section_content": user.input_query, - "effort": user.effort, - "limit": user.limit, - "mode_type": "bale", - }, - url="/conflict/constitution", - ): - step = step_data.get("step") - data = step_data.get("data") - is_first = step_data.get("is_first", False) - is_last = step_data.get("is_last", False) - print(f"is_first {is_first} is_last {is_last}") - # is_last = step_data.get("is_last") - _button = [[HOME_BUTTON]] - print(f"==== handle_constitution ====") - if step == "low": - print(f"low {user.effort}") - chunked_response = await self.formatter.form_constitution(data) - # await self.send_message_helper(user, chunked_text=response) - # if is_last: - if is_first is True: + _buttons = [[HOME_BUTTON]] + + if user.effort == "low": + current_text = "*نتیجه بررسی مغایرت با اصول مهم قانون اساسی*:\n\n" + + seen_k = set() + _buttons.insert(0, [MORE_EFFORT_BUTTON]) + async for _data in self.operator.conflict_qanon_asasi_low( + user.input_query, user.effort, user.limit + ): + status = _data.get("status") + data = _data.get("data") + + print(f"status {type(status)}-{status}") + data = {k: v for k, v in data.items() if k not in seen_k} + print(f"data {len(data)} seen_k {len(list(seen_k))}") + text = await self.formatter.form_constitution_low(data, status) + text = text + "\n\n" + seen_k.update(data.keys()) + + if status == 11: await self.send_message_helper( - user=user, - update_message=False, - chunked_text=chunked_response, - ) - elif is_last is True: - _button.append([MORE_EFFORT_BUTTON]) - await self.send_message_helper( - user=user, + user, + text=current_text, + end_buttons=_buttons, update_message=True, - chunked_text=chunked_response, - end_buttons=_button, + is_save_active_message_id=True, ) else: - await self.send_message_helper( - user=user, - update_message=True, - chunked_text=chunked_response, - ) - elif step == "rule_making": - print(f"rule_making {user.effort}") - chunked_response = await self.formatter.form_rule_making( - _input=data - ) - await self.send_message_helper( - user=user, - update_message=False, - chunked_text=chunked_response, - ) + text = [text[i : i + 50] for i in range(0, len(text), 50)] + for word in text: + current_text += word + await self.send_message_helper( + user, + text=current_text, + update_message=True, + is_save_active_message_id=True, + ) + text = "" - elif step == "semantich_search": - print(f"semantich_search {user.effort}") - _header = f"🔍 جستجوی معنایی انجام شد و مستندات مرتبط یافت شدند:\n" - response = await self.formatter.form_ss_rules(data, header=_header) - await self.send_message_helper(user, chunked_text=response) + elif user.effort == "medium": + print(f"==== handle_constitution ====") + # chunked_response = await self.formatter.form_rule_making( + # _input=data + # ) + await self.send_message_helper( + user=user, + text="در دست توسعه...", + ) + # async for _data in self.operator.conflict_qanon_asasi_steps( + # user.input_query, user.effort, user.limit + # ): + # status = _data.get("status") + # data = _data.get("data") - elif step == "subject_unities": - print(f"subject_unities {user.effort}") - _header = "نتایج اولیه مغایرت های احتمالی :\n" - print(f"data type {type(data)}") - try: - print(f"data -> RuleRelation") - _data = [RuleRelation.parse_obj(i) for i in data] - except: - print(f"data -> String") - _data = data + # print(f"status {type(status)}-{status}") - chunked_text, _button, mapping_qs = ( - await self.formatter.form_subject_unity(_data, header=_header) - ) - await self.send_message_helper( - user=user, - chunked_text=chunked_text, - end_buttons=_button, - ) - user.subject_unities = mapping_qs + # print(f"semantich_search {user.effort}") + # _header = f"🔍 جستجوی معنایی انجام شد و مستندات مرتبط یافت شدند:\n" + # response = await self.formatter.form_ss_rules(data, header=_header) + # await self.send_message_helper(user, chunked_text=response) + + # print(f"subject_unities {user.effort}") + # _header = "نتایج اولیه مغایرت های احتمالی :\n" + # print(f"data type {type(data)}") + # try: + # print(f"data -> RuleRelation") + # _data = [RuleRelation.model_validate(i) for i in data] + # except: + # print(f"data -> String") + # _data = data + + # chunked_text, _button, mapping_qs = ( + # await self.formatter.form_subject_unity(_data, header=_header) + # ) + # await self.send_message_helper( + # user=user, + # chunked_text=chunked_text, + # end_buttons=_button, + # ) + # user.subject_unities = mapping_qs except Exception as e: print("ERROR in handle_chat:", str(traceback.print_exc())) await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) finally: - user.is_processing_lock = False + user.last_input_query = user.input_query user.input_query = "" + user.is_processing_lock = False + return {"ok": True} async def handle_conflict_law_writing_policy(self, user: BaleUser): user.is_processing_lock = True - try: - user.last_query = user.input_query + _buttons = [[HOME_BUTTON]] + self.ui_handler.create(user=user, main_text=[ + "در حال تحلیل", + "در حال طبقه بندی", + "در حال تفکر", + "ساختار بندی", + ], + ) - _result = await self.request_manager.get_result( - payload={ - "section_content": user.input_query, - "effort": user.effort, - }, - url="/conflict/law_writing_policy", + try: + + _result = await self.operator.conflict_law_writing_policy( + query=user.input_query, effort=user.effort ) result = await self.formatter.from_law_writing_policy( _input_dict=_result, header="نتیجه بررسی با سیاست های قانون گذاری" ) - _buttons = [[HOME_BUTTON]] if user.effort != "medium": _buttons.insert(0, [MORE_EFFORT_BUTTON]) - + self.ui_handler.cancel(user) await self.send_message_helper( user=user, chunked_text=result, end_buttons=_buttons ) @@ -625,8 +1068,10 @@ class BaleBot: await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) finally: - # user.input_query = "" + user.last_input_query = user.input_query + user.input_query = "" user.is_processing_lock = False + return {"ok": True} async def handle_conflict_all_qavanin(self, user: BaleUser): @@ -671,7 +1116,7 @@ class BaleBot: print(f"data type {type(data)}") try: print(f"data -> RuleRelation") - _data = [RuleRelation.parse_obj(i) for i in data] + _data = [RuleRelation.model_validate(i) for i in data] except: print(f"data -> String") _data = data @@ -737,7 +1182,7 @@ class BaleBot: print(f"data type {type(data)}") try: print(f"data -> RuleRelation") - _data = [RuleRelation.parse_obj(i) for i in data] + _data = [RuleRelation.model_validate(i) for i in data] except: print(f"data -> String") _data = data @@ -766,17 +1211,17 @@ class BaleBot: user.is_processing_lock = True print(f"handle_advanced_check_conflict======================================") try: - print(f'user.call_back_query {user.call_back_query}') + print(f"user.call_back_query {user.call_back_query}") step, _type, qq_title = user.call_back_query.split(":") if _type == "qq": - print('A') + print("A") groups = {} for qanon_title, item in user.subject_unities.items(): if qanon_title == qq_title: for i in item: groups[qanon_title] = i.db_rule.section_id - print('B') + print("B") if len(groups) > 1: _button = [] for i, (k, v) in enumerate(groups.items(), start=1): @@ -790,10 +1235,10 @@ class BaleBot: ) else: user.call_back_query = f"subject_unities:qs:{groups[qq_title]}" - print('ccccc') + print("ccccc") elif _type == "qs": - print('AAA@@@') + print("AAA@@@") content = None for k, v in user.subject_unities.items(): if v.db_rule.section_id == qq_title: @@ -818,14 +1263,14 @@ class BaleBot: if step == "step2": print(f"*********************Step2 {user.effort}") _header = "نتایج اولیه مغایرت های احتمالی :\n" - _data = RuleRelation.parse_obj(data) + _data = RuleRelation.model_validate(data) chunked_text = await self.formatter.form_conflict_detection( _data, header=_header ) await self.send_message_helper(user=user, text=chunked_text) elif step == "step3": print(f"*********************Step3") - _data = RuleRelation.parse_obj(data) + _data = RuleRelation.model_validate(data) chunked_text = ( await self.formatter.form_conflict_type_detection( _data, header=_header @@ -835,7 +1280,7 @@ class BaleBot: elif step == "step4": print(f"*********************Step4") - _data = RuleRelation.parse_obj(data) + _data = RuleRelation.model_validate(data) chunked_text = ( await self.formatter.form_relation_identification( _data, header=_header @@ -844,7 +1289,7 @@ class BaleBot: await self.send_message_helper(user=user, text=chunked_text) elif step == "step5": print(f"*********************Step5") - _data = Evaluation.parse_obj(data) + _data = Evaluation.model_validate(data) chunked_text = await self.formatter.form_evaluation( _data, header=_header ) @@ -856,55 +1301,12 @@ class BaleBot: print("ERROR in handle_chat:", str(traceback.print_exc())) await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) finally: - # user.input_query = "" + user.last_input_query = user.input_query + user.input_query = "" user.is_processing_lock = False return {"ok": True} - async def handle_chat_in_law( - self, user: BaleUser - ) -> List[BalePayload]: # -> List[List[str, dict]] - - user.is_processing_lock = True - user.last_query = user.input_query - try: - # اگر runtime تغییر کرده یا نتیجه قبلی وجود ندارد → درخواست جدید - result = await self.request_manager.get_result( - payload={ - "section_content": user.input_query, - "effort": user.effort, - "limit": user.limit, - "mode_type": "bale", - }, - url="/semantic_search/run_chat", - ) - print(f"===================={result}") - text_result = self.formatter.form_law_chat( - result["answer"] # , llm_answer["source"] - ) - _buttons = [[HOME_BUTTON]] - _b = [] - if result['answer_type'] == 'legal_question': - if user.limit < self.max_limit: - _b += [MORE_LIMIT_BUTTON] - - if user.effort != "medium": - _b += [MORE_EFFORT_BUTTON] - - if len(_b) > 0: - _buttons.insert(0, _b) - - await self.send_message_helper( - user=user, chunked_text=text_result, end_buttons=_buttons - ) - - except Exception as e: - print("ERROR in handle_chat:", str(traceback.print_exc())) - await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) - finally: - user.input_query = "" - user.is_processing_lock = False - async def handle_stream_chat(self, user: BaleUser): """ stream_talk() @@ -953,55 +1355,6 @@ class BaleBot: ) self.update_message_to_bale(user, payload) - async def handle_logical_chat_in_law(self, user: BaleUser): - user.is_processing_lock = True - - try: - async for step_data in self.request_manager.stream_result( - payload={ - "section_content": user.input_query, - "effort": user.effort, - }, - url="/stream/chat_logical", - ): - step = step_data.get("step") - data = step_data.get("data") - - if step == "rule_making": - _header = f"✅ مرحله استخراج اجزاء حقوقی متن انجام شد.\nتعداد جزء ها: {len(data)}\n اجزاء حقوقی:\n" - response = await self.formatter.form_rule_making( - data, header=_header - ) - await self.send_message_helper(user, chunked_text=response) - - elif step == "semantic_search": - _header = f"🔍 جستجوی معنایی انجام شد و مستندات مرتبط یافت شدند:\n" - response = await self.formatter.form_ss_rules(data, header=_header) - await self.send_message_helper(user, chunked_text=response) - - elif step == "llm_answer": - _button = [[HOME_BUTTON]] - if user.effort != "medium": - _button.insert(0, [MORE_EFFORT_BUTTON]) - _header = f"📝 پاسخ نهایی:\n" - response = await self.formatter.form_llm_answer_chat( - data, header=_header - ) - print(f"response {response}") - await self.send_message_helper( - user, chunked_text=response, end_buttons=_button - ) - - except Exception as e: - print("ERROR in handle_chat:", str(traceback.print_exc())) - await self.send_message_helper(user=user, text=ERROR_IN_PROCESS) - - finally: - user.input_query = "" - user.is_processing_lock = False - - return {"ok": True} - async def handle_beta(self, user: BaleUser): """ state: @@ -1037,16 +1390,144 @@ class BaleBot: except Exception as e: print("ERROR in handl_beta:", str(traceback.print_exc())) - async def save_to_es(self, data: QaChat): - # print("save_to_es data rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr") + async def save_to_db(self, user: BaleUser, data: Dict): + time_now = int(time.time()) + _id = f"ble_{user.chat_id}_{time_now}" + + data.update( + { + "id": _id, + "uc_id": user.uc_id, + "message_id": user.active_message_id, + "username": user.username, + "is_bot": user.is_bot, + "first_name": user.first_name, + "last_name": user.last_name, + "effort": user.effort, + "limit": user.limit, + "input_query": user.input_query, + "timestamp": time_now, + } + ) try: es_res = self.es_helper.update_index_doc( is_update_state=False, index_name_o=self.es_index_name, - eid=data.id, - data=data.model_dump(), + eid=_id, + data=data, ) # type_name, payload, request - print(f"Saved {es_res}") + print(f"-- ES-Saved {es_res}") except Exception as e: - print("save_to_es ", str(traceback.print_exc())) + print("++ failed save_to_Es ", str(traceback.print_exc())) + + +class UIState(BaleBotGeneral): + """ + مسئول نمایش «در حال تحلیل…» و پاک‌کردن پیام اصلی + در زمان لغو یا پایان کار است. + برای سرگرم کردن کاربر + یک روند کاملا مجزا برای نشان دادن پیام و حذف در انتها + """ + + def __init__( + self, + token:str, + delay_time: float = 0.4, + ): + super().__init__( + token=token, + ) + """ + :param bot: نمونهٔ BaleBot که متدهای API را در اختیار دارد + :param user: کاربری که پیامش در حال پردازش است + """ + self.delay_time = delay_time + self.done = False + self._heartbeat_task: Dict[str, asyncio.Task] = {} + + + # ------------------------------------------------------------------ + # توابعی که در والد BaleBotGeneral هستند + # ------------------------------------------------------------------ + async def delete_bale_message(self, *args, **kwargs): + await super().delete_bale_message(*args, **kwargs) + + async def send_message_helper(self, *args, **kwargs): + await super().send_message_helper(*args, **kwargs) + + + # ------------------------------------------------------------------ + # توابع خود کلاس + # ------------------------------------------------------------------ + async def active(self, user: BaleUser,): + await self.send_message_helper( + user=user, + text=self.main_text[0], + update_message=False, + is_save_active_message_id=True, + ) + + # ------------------------------------------------------------------ + # متدهای عمومی + # ------------------------------------------------------------------ + def cancel(self, user: BaleUser,): + """ + متوقف کردن حلقهٔ heartbeat و پاک‌کردن پیام اصلی + """ + self.done = True + _user_task = self._heartbeat_task.get(user.uc_id, None) + if _user_task: + self._heartbeat_task[user.uc_id].cancel() # اگر هنوز در حال اجرا باشد + # پاک‌کردن پیام اصلی + asyncio.create_task( + self.delete_bale_message( + chat_id=user.chat_id, + message_id=user.active_message_id, + ) + ) + self.main_text = ["در حال تحلیل"] + + + def create(self, user: BaleUser, main_text: str | list = ["در حال تحلیل"], waiting_list:list=[]): + """ + شروع حلقهٔ heartbeat + """ + if isinstance(main_text, str): + self.main_text = [main_text] + elif isinstance(main_text, list): + self.main_text = main_text + + print( + f'create create' + ) + self._heartbeat_task[user.uc_id] = asyncio.create_task(self.heartbeat_loop(user=user, waiting_list=waiting_list)) + + # ------------------------------------------------------------------ + # حلقهٔ heartbeat + # ------------------------------------------------------------------ + async def heartbeat_loop(self, user: BaleUser, waiting_list=[] ): + """ + هر 0.2 ثانیه متن «در حال تحلیل…» را به‌روزرسانی می‌کند. + """ + await self.active(user=user) + + if len(waiting_list) == 0: + waiting_list = [".", "..", "...", "...."] + + while not self.done: + text = random.choice(self.main_text) + + for j in waiting_list: + # ویرایش پیام + _text = f"{j} {text}" + await self.send_message_helper( + user=user, + text=_text, + update_message=True, + is_save_active_message_id=True, + ) + await asyncio.sleep(self.delay_time) + + # صبر 5 ثانیه + # await asyncio.sleep(0.2) diff --git a/router/bale/bale_manager.py b/router/bale/bale_manager.py new file mode 100644 index 0000000..7b536d4 --- /dev/null +++ b/router/bale/bale_manager.py @@ -0,0 +1,92 @@ + +from router.bale.base_model import * +from core.core import * + +def encode_uc(update: BaleUpdate) -> str: + if update.message: + user = update.message.from_user + chat = update.message.chat + + elif update.callback_query: + user = update.callback_query.from_user + chat = update.callback_query.message.chat + + else: + return "unknown" + + username = user.username or user.id + chat_id = chat.id # ✅ فقط chat_id + + return f"{username}:{chat_id}" + + +def decode_uc(uc_id: str) -> dict: + """ + ورودی: 'username:chat_id' یا 'user_id:chat_id' + خروجی: {'username': ..., 'chat_id': ...} + """ + + try: + username, chat_id = uc_id.split(":", 1) + + return (username, int(chat_id) if chat_id.isdigit() else chat_id) + + except ValueError: + raise ValueError(f"decode_uc") + + +def extract_user_info(update: BaleUpdate) -> Dict: + uc_id = encode_uc(update) + if update.message: + u = update.message.from_user + return { + "uc_id": str(uc_id), + "chat_id": update.message.chat.id, + "user_id": u.id, + "username": u.username, + "first_name": u.first_name, + "last_name": u.last_name or "", + "is_bot": u.is_bot, + "update": update, + } + + if update.callback_query: + u = update.callback_query.from_user + return { + "uc_id": str(uc_id), + "chat_id": update.callback_query.message.chat.id, + "user_id": u.id, + "username": u.username, + "first_name": u.first_name, + "last_name": "", + "is_bot": u.is_bot, + "update": update, + } + + raise ValueError("No user info in update") + + +class UserManager: + def __init__(self): + self.users: Dict[str, BaleUser] = {} + self.list_vip_username = load_orjson( + "/home/sabr/back_new/mj_bale_chat_test/mj_bale_chat/vip_username.json" + ) + self.temporary_data = load_orjson( + "/home/sabr/back_new/mj_bale_chat_test/mj_bale_chat/temp.json" + ) + + def get_or_create(self, update: BaleUpdate) -> BaleUser: + user_data = extract_user_info(update) + uc_id = user_data["uc_id"] + + if user_data["username"] in self.list_vip_username: + user_data["is_vip"] = True + + if uc_id not in self.users: + self.users[uc_id] = BaleUser( + **user_data, + ) + user = self.users[uc_id] + user.update = update + return user diff --git a/router/bale/base_model.py b/router/bale/base_model.py index 9a50e7f..c2abd5c 100755 --- a/router/bale/base_model.py +++ b/router/bale/base_model.py @@ -6,7 +6,20 @@ from typing import Optional, Callable, List, Any from pydantic import BaseModel +class SingleSearchData(BaseModel): + score:float + id:str + content:str +class BMNewSemanticSearchOutput(BaseModel): + query: str + result : List[SingleSearchData] + metadata : Dict + embed_model_time : float + cosine_similarity_time : float + rerank_time : float + + class DbRule(BaseModel): rule_id: str rule_content: str @@ -241,13 +254,14 @@ class StateDetail(BaseModel): class BaleUser(BaseModel): uc_id: str chat_id: int - user_id: str + user_id: int update: BaleUpdate - username: str + username: str = None is_bot: bool = False + is_vip: bool = False first_name: str = "" last_name: str = "" - + message_limit:int = 0 rule_relation: RuleRelation | None = None subject_unities:Dict = {} @@ -260,9 +274,10 @@ class BaleUser(BaseModel): is_processing_lock : bool = False is_call_back_query : bool = False state_detail : StateDetail = None - last_message_id : int = 0 + active_message_id : int = 0 input_query: str = "" # ورودی کاربر + last_input_query: str = "" # ورودی کاربر call_back_query: str = "" # ورودی کاربر _query_type: str = "" # ورودی کاربر sub_state: str = "" # برای روندی ها diff --git a/router/bale/config.py b/router/bale/config.py index 57c028b..aacbe70 100644 --- a/router/bale/config.py +++ b/router/bale/config.py @@ -3,7 +3,7 @@ from router.bale.base_model import StateDetail -BUSY_TEXT = ("""⏳ درخواست قبلی شما در حال پردازش هست، لطفا تا اتمام آن منتظر بمانید ⏳""",) +BUSY_TEXT = "⏳ درخواست قبلی شما در حال پردازش هست، لطفا تا اتمام آن منتظر بمانید ⏳" class StateRegistry: @@ -26,6 +26,13 @@ STATE = [ message="""متن حقوقی برای جستجو در قوانین را وارد نمایید""", handler="handle_search_in_law", ), + StateDetail( + state="search_in_law_rules", + button_text="جستجو در اجزاء 🔎", + end_buttons=[], + message="""متن حقوقی برای جستجو در اجزاء قانونی را وارد نمایید""", + handler="handle_search_in_law_rules", + ), StateDetail( state="chat_in_law", button_text="گفتگو طبق قوانین کشور", @@ -94,6 +101,11 @@ STATE = [ button_text="تماس با ما ☎️", message="""لطفا برای ارتباط با ما از طریق مرکز فناوری مجلس شورای اسلامی ایران اقدام فرمایید""", ), + StateDetail( + state="not_yet", + button_text="در دست توسعه", + message="""این قسمت در دست توسعه قرار دارد.""", + ), StateDetail( state="about_us", button_text="درباره ما ⚡", @@ -134,7 +146,6 @@ def build_buttons_form(button_form): main_button_form = [ ["chat_in_law"], ["search_in_law"], - ["logical_chat_in_law"], ["rule_making"], ["qanon_title_repeat"], ["conflict_law_writing_policy"], diff --git a/temp.json b/temp.json new file mode 100644 index 0000000..1610ea1 --- /dev/null +++ b/temp.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/vip_username.json b/vip_username.json new file mode 100644 index 0000000..3600fcc --- /dev/null +++ b/vip_username.json @@ -0,0 +1,3 @@ +[ + "init_mahdi", "hsafabale", "mmpouya" +] \ No newline at end of file