Spaces:
Running
Running
import os | |
import re | |
import gradio as gr | |
import pandas as pd | |
import requests | |
import json | |
import faiss | |
import nest_asyncio | |
import sys | |
import boto3 | |
from pathlib import Path | |
from bs4 import BeautifulSoup | |
from typing import Union, List | |
import asyncio | |
from llama_index.core import ( | |
StorageContext, | |
ServiceContext, | |
VectorStoreIndex, | |
Settings, | |
load_index_from_storage | |
) | |
from llama_index.llms.openai import OpenAI | |
from llama_index.core.llms import ChatMessage | |
from llama_index.core.schema import IndexNode | |
from llama_index.core.storage.docstore import SimpleDocumentStore | |
from llama_index.retrievers.bm25 import BM25Retriever | |
from llama_index.embeddings.openai import OpenAIEmbedding | |
# from llama_index.vector_stores.faiss import FaissVectorStore | |
from llama_index.core.retrievers import QueryFusionRetriever | |
from llama_index.core.workflow import Event, Context, Workflow, StartEvent, StopEvent, step | |
from llama_index.core.schema import NodeWithScore | |
from llama_index.core.prompts import PromptTemplate | |
from llama_index.core.response_synthesizers import ResponseMode, get_response_synthesizer | |
from prompts import CITATION_QA_TEMPLATE, CITATION_REFINE_TEMPLATE | |
from dotenv import load_dotenv | |
load_dotenv() | |
aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID") | |
aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY") | |
openai_api_key = os.getenv("OPENAI_API_KEY") | |
embed_model = OpenAIEmbedding(model_name="text-embedding-3-small") | |
Settings.embed_model = embed_model | |
Settings.context_window = 20000 | |
Settings.chunk_size = 2048 | |
Settings.similarity_top_k = 20 | |
# Параметри S3 | |
BUCKET_NAME = "legal-position" | |
PREFIX_RETRIEVER = "Save_Index/" # Префікс для всього вмісту, який потрібно завантажити | |
LOCAL_DIR = Path("Save_Index_Local") # Локальна директорія для збереження даних з S3 | |
# Ініціалізація клієнта S3 | |
s3_client = boto3.client( | |
"s3", | |
aws_access_key_id=aws_access_key_id, | |
aws_secret_access_key=aws_secret_access_key, | |
region_name="eu-north-1" | |
) | |
# # Ініціалізація клієнта S3 | |
# s3_client = boto3.client( | |
# "s3", | |
# aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"), | |
# aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"), | |
# region_name="eu-north-1" | |
# ) | |
# Створюємо локальну директорію, якщо вона не існує | |
LOCAL_DIR.mkdir(parents=True, exist_ok=True) | |
# Функція для завантаження файлу з S3 | |
def download_s3_file(bucket_name, s3_key, local_path): | |
s3_client.download_file(bucket_name, s3_key, str(local_path)) | |
print(f"Завантажено: {s3_key} -> {local_path}") | |
# Функція для завантаження всієї папки з S3 у локальну директорію | |
def download_s3_folder(bucket_name, prefix, local_dir): | |
response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=prefix) | |
if 'Contents' in response: | |
for obj in response['Contents']: | |
s3_key = obj['Key'] | |
# Пропускаємо "папку" (кореневий префікс) у S3 | |
if s3_key.endswith('/'): | |
continue | |
# Визначаємо локальний шлях, де буде збережений файл | |
local_file_path = local_dir / Path(s3_key).relative_to(prefix) | |
local_file_path.parent.mkdir(parents=True, exist_ok=True) # створення підкаталогів, якщо потрібно | |
# Завантажуємо файл | |
s3_client.download_file(bucket_name, s3_key, str(local_file_path)) | |
print(f"Завантажено: {s3_key} -> {local_file_path}") | |
# Завантаження всього вмісту папки `Save_Index` з S3 у локальну директорію `Save_Index_Local` | |
download_s3_folder(BUCKET_NAME, PREFIX_RETRIEVER, LOCAL_DIR) | |
# PERSIST_DIR = "/home/docsa/Legal_Position/Save_index" | |
# Apply nest_asyncio to handle nested async calls | |
nest_asyncio.apply() | |
class RetrieverEvent(Event): | |
nodes: list[NodeWithScore] | |
state_lp_json = gr.State() | |
state_nodes = gr.State() | |
class CitationQueryEngineWorkflow(Workflow): | |
async def retrieve(self, ctx: Context, ev: StartEvent) -> Union[RetrieverEvent, None]: | |
query = ev.get("query") | |
question = ev.get("question") | |
nodes = ev.get("nodes") # Отримуємо nodes з події | |
if not query: | |
return None | |
await ctx.set("query", query) | |
await ctx.set("question", question) | |
if nodes is not None: | |
# Використовуємо передані nodes | |
return RetrieverEvent(nodes=nodes) | |
else: | |
# Якщо nodes не передані, не виконуємо додатковий пошук | |
return None | |
async def synthesize(self, ctx: Context, ev: RetrieverEvent) -> StopEvent: | |
query = await ctx.get("query", default=None) | |
question = await ctx.get("question", default=None) | |
llm_answer = OpenAI(model="gpt-4o-mini", temperature=0) | |
synthesizer = get_response_synthesizer( | |
llm=llm_answer, | |
text_qa_template=CITATION_QA_TEMPLATE, | |
refine_template=CITATION_REFINE_TEMPLATE, | |
response_mode=ResponseMode.COMPACT, | |
use_async=True, | |
) | |
response = await synthesizer.asynthesize(query=query, question=question, nodes=ev.nodes) | |
return StopEvent(result=response) | |
def parse_doc_ids(doc_ids): | |
if doc_ids is None: | |
return [] | |
if isinstance(doc_ids, list): | |
return [str(id).strip('[]') for id in doc_ids] | |
if isinstance(doc_ids, str): | |
cleaned = doc_ids.strip('[]').replace(' ', '') | |
if cleaned: | |
return [id.strip() for id in cleaned.split(',')] | |
return [] | |
def get_links_html(doc_ids): | |
parsed_ids = parse_doc_ids(doc_ids) | |
if not parsed_ids: | |
return "" | |
links = [f"[Рішення ВС: {doc_id}](https://reyestr.court.gov.ua/Review/{doc_id})" | |
for doc_id in parsed_ids] | |
return ", ".join(links) | |
def parse_lp_ids(lp_ids): | |
if lp_ids is None: | |
return [] | |
if isinstance(lp_ids, (str, int)): | |
cleaned = str(lp_ids).strip('[]').replace(' ', '') | |
if cleaned: | |
return [cleaned] | |
return [] | |
def get_links_html_lp(lp_ids): | |
parsed_ids = parse_lp_ids(lp_ids) | |
if not parsed_ids: | |
return "" | |
links = [f"[ПП ВС: {lp_id}](https://lpd.court.gov.ua/home/search/{lp_id})" for lp_id in parsed_ids] | |
return ", ".join(links) | |
def initialize_components(): | |
try: | |
# Використовуємо папку `Save_Index_Local`, куди завантажено файли з S3 | |
persist_path = Path("Save_Index_Local") | |
# Перевірка існування локальної директорії | |
if not persist_path.exists(): | |
raise FileNotFoundError(f"Directory not found: {persist_path}") | |
# Перевірка наявності необхідних файлів і папок | |
required_files = ['docstore_es_filter.json', 'bm25_retriever_es'] | |
missing_files = [f for f in required_files if not (persist_path / f).exists()] | |
if missing_files: | |
raise FileNotFoundError(f"Missing required files: {', '.join(missing_files)}") | |
# Ініціалізація компонентів | |
global retriever_bm25 | |
# Ініціалізація `SimpleDocumentStore` з `docstore_es_filter.json` | |
docstore = SimpleDocumentStore.from_persist_path(str(persist_path / "docstore_es_filter.json")) | |
# Ініціалізація `BM25Retriever` з папки `bm25_retriever_es` | |
bm25_retriever = BM25Retriever.from_persist_dir(str(persist_path / "bm25_retriever_es")) | |
# Ініціалізація `QueryFusionRetriever` з налаштуваннями | |
retriever_bm25 = QueryFusionRetriever( | |
[ | |
bm25_retriever, | |
], | |
similarity_top_k=Settings.similarity_top_k, | |
num_queries=1, | |
use_async=True, | |
) | |
return True | |
except Exception as e: | |
print(f"Error initializing components: {str(e)}", file=sys.stderr) | |
return False | |
def extract_court_decision_text(url): | |
response = requests.get(url) | |
soup = BeautifulSoup(response.content, 'html.parser') | |
unwanted_texts = [ | |
"Доступ до Реєстру здійснюється в тестовому (обмеженому) режимі.", | |
"З метою упередження перешкоджанню стабільній роботі Реєстру" | |
] | |
decision_text = "" | |
for paragraph in soup.find_all('p'): | |
text = paragraph.get_text(separator="\n").strip() | |
if not any(unwanted_text in text for unwanted_text in unwanted_texts): | |
decision_text += text + "\n" | |
return decision_text.strip() | |
def generate_legal_position(court_decision_text, user_question): | |
# llm_lp = OpenAI(model="gpt-4o-mini", temperature=0) | |
# llm_lp = OpenAI(model="ft:gpt-4o-mini-2024-07-18:personal:legal-position-100:ASPFc3vF", temperature=0) | |
llm_lp = OpenAI(model="ft:gpt-4o-mini-2024-07-18:personal:legal-position-400:AT3wvKsU", temperature=0) | |
response_format = { | |
"type": "json_schema", | |
"json_schema": { | |
"name": "lp_schema", | |
"schema": { | |
"type": "object", | |
"properties": { | |
"title": {"type": "string", "description": "Title of the legal position"}, | |
"text": {"type": "string", "description": "Text of the legal position"}, | |
"proceeding": {"type": "string", "description": "Type of court proceedings"}, | |
"category": {"type": "string", "description": "Category of the legal position"}, | |
}, | |
"required": ["title", "text", "proceeding", "category"], | |
"additionalProperties": False | |
}, | |
"strict": True | |
} | |
} | |
system_prompt = """ | |
Дій як кваліфікований юрист. : | |
""" | |
prompt = f"""Дотримуйся цих інструкцій. | |
1. Спочатку вам буде надано текст судового рішення: | |
<court_decision> | |
{court_decision_text} | |
</court_decision> | |
2. Уважно прочитай та проаналізуй текст наданого судового рішення. Зверни увагу на: | |
- Юридичну суть рішення | |
- Основне правове обґрунтування | |
- Головні юридичні міркування | |
3. На основі аналізу сформулюй короткий зміст позиції суду, дотримуючись таких вказівок: | |
- Будь чіткими, точними та обґрунтованими | |
- Використовуй відповідну юридичну термінологію | |
- Зберігай стислість, але повністю передай суть судового рішення | |
- Уникай додаткових пояснень чи коментарів | |
- Спробуй узагальнювати та уникати специфічної інформації (наприклад, імен або назв) під час подачі результатів | |
- Використовуйте лише українську мову | |
4. Створи короткий заголовок, який відображає основну суть судового рішення та зазнач його категорію. | |
5. Додатково визнач тип судочинства, до якої відноситься дане рішення. | |
Використовуй лише один із цих типів: 'Адміністративне судочинство', 'Кримінальне судочинство', 'Цивільне судочинство', 'Господарське судочинство' | |
6. Відформатуй відповідь у форматі JSON: | |
{{ | |
"title": "Заголовок судового рішення", | |
"text": "Текст короткого змісту позиції суду", | |
"proceeding": "Тип судочинства", | |
"category": "Категорія судового рішення" | |
}} | |
""" | |
messages = [ | |
ChatMessage(role="system", content=system_prompt), | |
ChatMessage(role="user", content=prompt), | |
] | |
response = llm_lp.chat(messages, response_format=response_format) | |
try: | |
parsed_response = json.loads(response.message.content) | |
if "title" in parsed_response and "text" in parsed_response: | |
return parsed_response | |
else: | |
return { | |
"title": "Error: Missing required fields in response", | |
"text": response.message.content | |
} | |
except json.JSONDecodeError: | |
return { | |
"title": "Error parsing response", | |
"text": response.message.content | |
} | |
def create_gradio_interface(): | |
with gr.Blocks() as app: | |
gr.Markdown("# Аналізатор судових рішень на основі правових позицій Верховного Суду") | |
with gr.Row(): | |
url_input = gr.Textbox(label="URL судового рішення:") | |
question_input = gr.Textbox(label="Ваше питання:") | |
with gr.Row(): | |
generate_position_button = gr.Button("Генерувати короткий зміст позиції суду") | |
search_with_ai_button = gr.Button("Пошук із ШІ", interactive=False) | |
search_without_ai_button = gr.Button("Пошук без ШІ") | |
analyze_button = gr.Button("Аналіз", interactive=False) | |
position_output = gr.Markdown(label="Короткий зміст позиції суду за введеним рішенням") | |
search_output = gr.Markdown(label="Результат пошуку") | |
analysis_output = gr.Markdown(label="Результат аналізу") | |
# Два об'єкти стану для зберігання legal_position_json та nodes | |
state_lp_json = gr.State() | |
state_nodes = gr.State() | |
async def generate_position_action(url): | |
try: | |
court_decision_text = extract_court_decision_text(url) | |
legal_position_json = generate_legal_position(court_decision_text, "") | |
position_output_content = f"**Короткий зміст позиції суду за введеним рішенням:**\n *{legal_position_json['title']}*: \n{legal_position_json['text']} **Категорія:** \n{legal_position_json['category']} ({legal_position_json['proceeding']})\n\n" | |
return position_output_content, legal_position_json | |
except Exception as e: | |
return f"Error during position generation: {str(e)}", None | |
async def search_with_ai_action(legal_position_json): | |
try: | |
query_text = legal_position_json["title"] + ': ' + legal_position_json["text"] + ': ' + legal_position_json["proceeding"] + ': ' + legal_position_json["category"] | |
nodes = await retriever_bm25.aretrieve(query_text) | |
sources_output = "\n **Результати пошуку (наявні правові позиції ВСУ):** \n\n" | |
for index, node in enumerate(nodes, start=1): | |
source_title = node.node.metadata.get('title') | |
doc_ids = node.node.metadata.get('doc_id') | |
lp_ids = node.node.metadata.get('lp_id') | |
links = get_links_html(doc_ids) | |
links_lp = get_links_html_lp(lp_ids) | |
sources_output += f"\n[{index}] *{source_title}* {links_lp} 👉 Score: {node.score} {links}\n" | |
return sources_output, nodes | |
except Exception as e: | |
return f"Error during search: {str(e)}", None | |
async def search_without_ai_action(url): | |
try: | |
court_decision_text = extract_court_decision_text(url) | |
nodes = await retriever_bm25.aretrieve(court_decision_text) | |
search_output_content = f"**Результати пошуку (наявні правові позиції ВСУ):** \n\n" | |
for index, node in enumerate(nodes, start=1): | |
source_title = node.node.metadata.get('title', 'Невідомий заголовок') | |
doc_ids = node.node.metadata.get('doc_id') | |
links = get_links_html(doc_ids) | |
search_output_content += f"\n[{index}] *{source_title}* 👉 Score: {node.score} {links}\n" | |
return search_output_content, nodes | |
except Exception as e: | |
return f"Error during search: {str(e)}", None | |
import re | |
import re | |
async def analyze_action(legal_position_json, question, nodes): | |
try: | |
workflow = CitationQueryEngineWorkflow(timeout=600) | |
# Запускаємо workflow і отримуємо об'єкт Response | |
response = await workflow.run( | |
query=legal_position_json["title"] + ': ' + legal_position_json["text"] + ': ' + | |
legal_position_json["proceeding"] + ': ' + legal_position_json["category"], | |
question=question, | |
nodes=nodes # Передаємо nodes у workflow | |
) | |
# Отримуємо текст відповіді з об'єкта Response | |
response_text = str(response) | |
# Обробка цитат у тексті відповіді | |
citations = re.findall(r'\[(\d+)\]', response_text) | |
unique_citations = sorted(set(citations), key=int) | |
output = f"**Аналіз Штучного Інтелекту:**\n{response_text}\n\n" | |
output += "**Цитовані джерела існуючих правових позицій Верховного Суду:**\n" | |
# Перевіряємо наявність source_nodes в об'єкті Response | |
source_nodes = getattr(response, 'source_nodes', []) | |
# Проходимо по унікальних цитатах та зіставляємо з `lp_id` у source_nodes | |
for citation in unique_citations: | |
found = False # Змінна для відстеження, чи знайдено джерело для lp_id | |
for index, source_node_with_score in enumerate(source_nodes, start=1): | |
source_node = source_node_with_score.node | |
lp_id = source_node.metadata.get('lp_id') # Отримуємо lp_id із метаданих джерела | |
# Якщо lp_id збігається з цитатою | |
if str(lp_id) == citation: | |
found = True | |
source_title = source_node.metadata.get('title', 'Невідомий заголовок') | |
doc_ids = source_node.metadata.get('doc_id') | |
links = get_links_html(doc_ids) | |
links_lp = get_links_html_lp(lp_id) | |
# Використовуємо `index` як номер джерела на початку рядка | |
output += f"[{index}]: *{source_title}* {links_lp} 👉 Score: {source_node_with_score.score} {links}\n" | |
break # Вихід із циклу при знайденому відповідному джерелі | |
if not found: | |
output += f"[{citation}]: Немає відповідного джерела для lp_id {citation}\n" | |
return output | |
except Exception as e: | |
return f"Error during analysis: {str(e)}" | |
# Підключаємо функції до кнопок з оновленими входами та виходами | |
generate_position_button.click( | |
fn=generate_position_action, | |
inputs=url_input, | |
outputs=[position_output, state_lp_json] | |
) | |
generate_position_button.click( | |
fn=lambda: gr.update(interactive=True), | |
inputs=None, | |
outputs=search_with_ai_button | |
) | |
search_with_ai_button.click( | |
fn=search_with_ai_action, | |
inputs=state_lp_json, | |
outputs=[search_output, state_nodes] | |
) | |
search_with_ai_button.click( | |
fn=lambda: gr.update(interactive=True), | |
inputs=None, | |
outputs=analyze_button | |
) | |
search_without_ai_button.click( | |
fn=search_without_ai_action, | |
inputs=url_input, | |
outputs=[search_output, state_nodes] | |
) | |
search_without_ai_button.click( | |
fn=lambda: gr.update(interactive=True), | |
inputs=None, | |
outputs=analyze_button | |
) | |
analyze_button.click( | |
fn=analyze_action, | |
inputs=[state_lp_json, question_input, state_nodes], | |
outputs=analysis_output | |
) | |
return app | |
if __name__ == "__main__": | |
if initialize_components(): | |
print("Components initialized successfully!") | |
app = create_gradio_interface() | |
app.launch(share=True) | |
else: | |
print("Failed to initialize components. Please check the paths and try again.", file=sys.stderr) | |
sys.exit(1) | |