DocUA commited on
Commit
7e7c4ec
·
1 Parent(s): a51c2d5

Deploy main.py

Browse files
Files changed (1) hide show
  1. main.py +506 -0
main.py ADDED
@@ -0,0 +1,506 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import gradio as gr
4
+ import pandas as pd
5
+ import requests
6
+ import json
7
+ import faiss
8
+ import nest_asyncio
9
+ import sys
10
+ import boto3
11
+
12
+ from pathlib import Path
13
+ from bs4 import BeautifulSoup
14
+ from typing import Union, List
15
+ import asyncio
16
+ from llama_index.core import (
17
+ StorageContext,
18
+ ServiceContext,
19
+ VectorStoreIndex,
20
+ Settings,
21
+ load_index_from_storage
22
+ )
23
+ from llama_index.llms.openai import OpenAI
24
+ from llama_index.core.llms import ChatMessage
25
+ from llama_index.core.schema import IndexNode
26
+ from llama_index.core.storage.docstore import SimpleDocumentStore
27
+ from llama_index.retrievers.bm25 import BM25Retriever
28
+ from llama_index.embeddings.openai import OpenAIEmbedding
29
+ # from llama_index.vector_stores.faiss import FaissVectorStore
30
+ from llama_index.core.retrievers import QueryFusionRetriever
31
+ from llama_index.core.workflow import Event, Context, Workflow, StartEvent, StopEvent, step
32
+ from llama_index.core.schema import NodeWithScore
33
+ from llama_index.core.prompts import PromptTemplate
34
+ from llama_index.core.response_synthesizers import ResponseMode, get_response_synthesizer
35
+
36
+ from prompts import CITATION_QA_TEMPLATE, CITATION_REFINE_TEMPLATE
37
+
38
+
39
+ from dotenv import load_dotenv
40
+
41
+ load_dotenv()
42
+
43
+ aws_access_key_id = os.getenv("AWS_ACCESS_KEY_ID")
44
+ aws_secret_access_key = os.getenv("AWS_SECRET_ACCESS_KEY")
45
+ openai_api_key = os.getenv("OPENAI_API_KEY")
46
+
47
+ embed_model = OpenAIEmbedding(model_name="text-embedding-3-small")
48
+ Settings.embed_model = embed_model
49
+ Settings.context_window = 20000
50
+ Settings.chunk_size = 2048
51
+ Settings.similarity_top_k = 20
52
+
53
+
54
+ # Параметри S3
55
+ BUCKET_NAME = "legal-position"
56
+ PREFIX_RETRIEVER = "Save_Index/" # Префікс для всього вмісту, який потрібно завантажити
57
+ LOCAL_DIR = Path("Save_Index_Local") # Локальна директорія для збереження даних з S3
58
+
59
+
60
+ # Ініціалізація клієнта S3
61
+ s3_client = boto3.client(
62
+ "s3",
63
+ aws_access_key_id=aws_access_key_id,
64
+ aws_secret_access_key=aws_secret_access_key,
65
+ region_name="eu-north-1"
66
+ )
67
+
68
+ # # Ініціалізація клієнта S3
69
+ # s3_client = boto3.client(
70
+ # "s3",
71
+ # aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
72
+ # aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
73
+ # region_name="eu-north-1"
74
+ # )
75
+
76
+ # Створюємо локальну директорію, якщо вона не існує
77
+ LOCAL_DIR.mkdir(parents=True, exist_ok=True)
78
+
79
+ # Функція для завантаження файлу з S3
80
+ def download_s3_file(bucket_name, s3_key, local_path):
81
+ s3_client.download_file(bucket_name, s3_key, str(local_path))
82
+ print(f"Завантажено: {s3_key} -> {local_path}")
83
+
84
+ # Функція для завантаження всієї папки з S3 у локальну директорію
85
+ def download_s3_folder(bucket_name, prefix, local_dir):
86
+ response = s3_client.list_objects_v2(Bucket=bucket_name, Prefix=prefix)
87
+ if 'Contents' in response:
88
+ for obj in response['Contents']:
89
+ s3_key = obj['Key']
90
+ # Пропускаємо "папку" (кореневий префікс) у S3
91
+ if s3_key.endswith('/'):
92
+ continue
93
+ # Визначаємо локальний шлях, де буде збережений файл
94
+ local_file_path = local_dir / Path(s3_key).relative_to(prefix)
95
+ local_file_path.parent.mkdir(parents=True, exist_ok=True) # створення підкаталогів, якщо потрібно
96
+ # Завантажуємо файл
97
+ s3_client.download_file(bucket_name, s3_key, str(local_file_path))
98
+ print(f"Завантажено: {s3_key} -> {local_file_path}")
99
+
100
+ # Завантаження всього вмісту папки `Save_Index` з S3 у локальну директорію `Save_Index_Local`
101
+ download_s3_folder(BUCKET_NAME, PREFIX_RETRIEVER, LOCAL_DIR)
102
+
103
+
104
+
105
+ # PERSIST_DIR = "/home/docsa/Legal_Position/Save_index"
106
+
107
+ # Apply nest_asyncio to handle nested async calls
108
+ nest_asyncio.apply()
109
+
110
+ class RetrieverEvent(Event):
111
+ nodes: list[NodeWithScore]
112
+
113
+
114
+ state_lp_json = gr.State()
115
+ state_nodes = gr.State()
116
+
117
+
118
+ class CitationQueryEngineWorkflow(Workflow):
119
+ @step
120
+ async def retrieve(self, ctx: Context, ev: StartEvent) -> Union[RetrieverEvent, None]:
121
+ query = ev.get("query")
122
+ question = ev.get("question")
123
+ nodes = ev.get("nodes") # Отримуємо nodes з події
124
+
125
+ if not query:
126
+ return None
127
+
128
+ await ctx.set("query", query)
129
+ await ctx.set("question", question)
130
+
131
+ if nodes is not None:
132
+ # Використовуємо передані nodes
133
+ return RetrieverEvent(nodes=nodes)
134
+ else:
135
+ # Якщо nodes не передані, не виконуємо додатковий пошук
136
+ return None
137
+
138
+ @step
139
+ async def synthesize(self, ctx: Context, ev: RetrieverEvent) -> StopEvent:
140
+ query = await ctx.get("query", default=None)
141
+ question = await ctx.get("question", default=None)
142
+ llm_answer = OpenAI(model="gpt-4o-mini", temperature=0)
143
+
144
+ synthesizer = get_response_synthesizer(
145
+ llm=llm_answer,
146
+ text_qa_template=CITATION_QA_TEMPLATE,
147
+ refine_template=CITATION_REFINE_TEMPLATE,
148
+ response_mode=ResponseMode.COMPACT,
149
+ use_async=True,
150
+ )
151
+
152
+ response = await synthesizer.asynthesize(query=query, question=question, nodes=ev.nodes)
153
+ return StopEvent(result=response)
154
+
155
+
156
+ def parse_doc_ids(doc_ids):
157
+ if doc_ids is None:
158
+ return []
159
+ if isinstance(doc_ids, list):
160
+ return [str(id).strip('[]') for id in doc_ids]
161
+ if isinstance(doc_ids, str):
162
+ cleaned = doc_ids.strip('[]').replace(' ', '')
163
+ if cleaned:
164
+ return [id.strip() for id in cleaned.split(',')]
165
+ return []
166
+
167
+ def get_links_html(doc_ids):
168
+ parsed_ids = parse_doc_ids(doc_ids)
169
+ if not parsed_ids:
170
+ return ""
171
+ links = [f"[Рішення ВС: {doc_id}](https://reyestr.court.gov.ua/Review/{doc_id})"
172
+ for doc_id in parsed_ids]
173
+ return ", ".join(links)
174
+
175
+ def parse_lp_ids(lp_ids):
176
+ if lp_ids is None:
177
+ return []
178
+ if isinstance(lp_ids, (str, int)):
179
+ cleaned = str(lp_ids).strip('[]').replace(' ', '')
180
+ if cleaned:
181
+ return [cleaned]
182
+ return []
183
+
184
+ def get_links_html_lp(lp_ids):
185
+ parsed_ids = parse_lp_ids(lp_ids)
186
+ if not parsed_ids:
187
+ return ""
188
+ links = [f"[ПП ВС: {lp_id}](https://lpd.court.gov.ua/home/search/{lp_id})" for lp_id in parsed_ids]
189
+ return ", ".join(links)
190
+
191
+
192
+ def initialize_components():
193
+ try:
194
+ # Використовуємо папку `Save_Index_Local`, куди завантажено файли з S3
195
+ persist_path = Path("Save_Index_Local")
196
+
197
+ # Перевірка існування локальної директорії
198
+ if not persist_path.exists():
199
+ raise FileNotFoundError(f"Directory not found: {persist_path}")
200
+
201
+ # Перевірка наявності необхідних файлів і папок
202
+ required_files = ['docstore_es_filter.json', 'bm25_retriever_es']
203
+ missing_files = [f for f in required_files if not (persist_path / f).exists()]
204
+
205
+ if missing_files:
206
+ raise FileNotFoundError(f"Missing required files: {', '.join(missing_files)}")
207
+
208
+ # Ініціалізація компонентів
209
+ global retriever_bm25
210
+
211
+ # Ініціалізація `SimpleDocumentStore` з `docstore_es_filter.json`
212
+ docstore = SimpleDocumentStore.from_persist_path(str(persist_path / "docstore_es_filter.json"))
213
+
214
+ # Ініціалізація `BM25Retriever` з папки `bm25_retriever_es`
215
+ bm25_retriever = BM25Retriever.from_persist_dir(str(persist_path / "bm25_retriever_es"))
216
+
217
+ # Ініціалізація `QueryFusionRetriever` з налаштуваннями
218
+ retriever_bm25 = QueryFusionRetriever(
219
+ [
220
+ bm25_retriever,
221
+ ],
222
+ similarity_top_k=Settings.similarity_top_k,
223
+ num_queries=1,
224
+ use_async=True,
225
+ )
226
+ return True
227
+ except Exception as e:
228
+ print(f"Error initializing components: {str(e)}", file=sys.stderr)
229
+ return False
230
+
231
+
232
+ def extract_court_decision_text(url):
233
+ response = requests.get(url)
234
+ soup = BeautifulSoup(response.content, 'html.parser')
235
+
236
+ unwanted_texts = [
237
+ "Доступ до Реєстру здійснюється в тестовому (обмеженому) режимі.",
238
+ "З метою упередження перешкоджанню стабільній роботі Реєстру"
239
+ ]
240
+
241
+ decision_text = ""
242
+ for paragraph in soup.find_all('p'):
243
+ text = paragraph.get_text(separator="\n").strip()
244
+ if not any(unwanted_text in text for unwanted_text in unwanted_texts):
245
+ decision_text += text + "\n"
246
+ return decision_text.strip()
247
+
248
+
249
+ def generate_legal_position(court_decision_text, user_question):
250
+ # llm_lp = OpenAI(model="gpt-4o-mini", temperature=0)
251
+ # llm_lp = OpenAI(model="ft:gpt-4o-mini-2024-07-18:personal:legal-position-100:ASPFc3vF", temperature=0)
252
+ llm_lp = OpenAI(model="ft:gpt-4o-mini-2024-07-18:personal:legal-position-400:AT3wvKsU", temperature=0)
253
+
254
+ response_format = {
255
+ "type": "json_schema",
256
+ "json_schema": {
257
+ "name": "lp_schema",
258
+ "schema": {
259
+ "type": "object",
260
+ "properties": {
261
+ "title": {"type": "string", "description": "Title of the legal position"},
262
+ "text": {"type": "string", "description": "Text of the legal position"},
263
+ "proceeding": {"type": "string", "description": "Type of court proceedings"},
264
+ "category": {"type": "string", "description": "Category of the legal position"},
265
+ },
266
+ "required": ["title", "text", "proceeding", "category"],
267
+ "additionalProperties": False
268
+ },
269
+ "strict": True
270
+ }
271
+ }
272
+
273
+ system_prompt = """
274
+ Дій як кваліфікований юрист. :
275
+ """
276
+
277
+ prompt = f"""Дотримуйся цих інструкцій.
278
+
279
+ 1. Спочатку вам буде надано текст судового рішення:
280
+
281
+ <court_decision>
282
+ {court_decision_text}
283
+ </court_decision>
284
+
285
+ 2. Уважно прочитай та проаналізуй текст наданого судового рішення. Зверни увагу на:
286
+ - Юридичну суть рішення
287
+ - Основне правове обґрунтування
288
+ - Головні юридичні міркування
289
+
290
+ 3. На основі аналізу сформулюй короткий зміст позиції суду, дотримуючись таких вказівок:
291
+ - Будь чіткими, точними та обґрунтованими
292
+ - Використовуй відповідну юридичну термінологію
293
+ - Зберігай стислість, але повністю передай суть судового рішення
294
+ - Уникай додаткових пояснень чи коментарів
295
+ - Спробуй узагальнювати та уникати специфічної інформації (наприклад, імен або назв) під час подачі результатів
296
+ - Використовуйте лише українську мову
297
+
298
+ 4. Створи короткий заголовок, який відображає основну суть судового рішення та зазнач його категорію.
299
+
300
+ 5. Додатково визнач тип судочинства, до якої відноситься дане рішення.
301
+ Використовуй лише один із цих типів: 'Адміністративне судочинство', 'Кримінальне судочинство', 'Цивільне судочинство', 'Господарське судочинство'
302
+
303
+ 6. Відформатуй відповідь у форматі JSON:
304
+
305
+ {{
306
+ "title": "Заголовок судового рішення",
307
+ "text": "Текст короткого змісту позиції суду",
308
+ "proceeding": "Тип судочинства",
309
+ "category": "Категорія судового рішення"
310
+ }}
311
+
312
+ """
313
+
314
+ messages = [
315
+ ChatMessage(role="system", content=system_prompt),
316
+ ChatMessage(role="user", content=prompt),
317
+ ]
318
+
319
+ response = llm_lp.chat(messages, response_format=response_format)
320
+
321
+ try:
322
+ parsed_response = json.loads(response.message.content)
323
+ if "title" in parsed_response and "text" in parsed_response:
324
+ return parsed_response
325
+ else:
326
+ return {
327
+ "title": "Error: Missing required fields in response",
328
+ "text": response.message.content
329
+ }
330
+
331
+ except json.JSONDecodeError:
332
+ return {
333
+ "title": "Error parsing response",
334
+ "text": response.message.content
335
+ }
336
+
337
+
338
+ def create_gradio_interface():
339
+ with gr.Blocks() as app:
340
+ gr.Markdown("# Аналізатор судових рішень на основі правових позицій Верховного Суду")
341
+
342
+ with gr.Row():
343
+ url_input = gr.Textbox(label="URL судового рішення:")
344
+ question_input = gr.Textbox(label="Ваше питання:")
345
+
346
+ with gr.Row():
347
+ generate_position_button = gr.Button("Генерувати короткий зміст позиції суду")
348
+ search_with_ai_button = gr.Button("Пошук із ШІ", interactive=False)
349
+ search_without_ai_button = gr.Button("Пошук без ШІ")
350
+ analyze_button = gr.Button("Аналіз", interactive=False)
351
+
352
+ position_output = gr.Markdown(label="Короткий зміст позиції суду за введеним рішенням")
353
+ search_output = gr.Markdown(label="Результат пошуку")
354
+ analysis_output = gr.Markdown(label="Результат аналізу")
355
+
356
+ # Два об'єкти стану для зберігання legal_position_json та nodes
357
+ state_lp_json = gr.State()
358
+ state_nodes = gr.State()
359
+
360
+ async def generate_position_action(url):
361
+ try:
362
+ court_decision_text = extract_court_decision_text(url)
363
+ legal_position_json = generate_legal_position(court_decision_text, "")
364
+ 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"
365
+ return position_output_content, legal_position_json
366
+ except Exception as e:
367
+ return f"Error during position generation: {str(e)}", None
368
+
369
+ async def search_with_ai_action(legal_position_json):
370
+ try:
371
+ query_text = legal_position_json["title"] + ': ' + legal_position_json["text"] + ': ' + legal_position_json["proceeding"] + ': ' + legal_position_json["category"]
372
+ nodes = await retriever_bm25.aretrieve(query_text)
373
+
374
+ sources_output = "\n **Результати пошуку (наявні правові позиції ВСУ):** \n\n"
375
+ for index, node in enumerate(nodes, start=1):
376
+ source_title = node.node.metadata.get('title')
377
+ doc_ids = node.node.metadata.get('doc_id')
378
+ lp_ids = node.node.metadata.get('lp_id')
379
+ links = get_links_html(doc_ids)
380
+ links_lp = get_links_html_lp(lp_ids)
381
+ sources_output += f"\n[{index}] *{source_title}* {links_lp} 👉 Score: {node.score} {links}\n"
382
+
383
+ return sources_output, nodes
384
+ except Exception as e:
385
+ return f"Error during search: {str(e)}", None
386
+
387
+ async def search_without_ai_action(url):
388
+ try:
389
+ court_decision_text = extract_court_decision_text(url)
390
+ nodes = await retriever_bm25.aretrieve(court_decision_text)
391
+
392
+ search_output_content = f"**Результати пошуку (наявні правові позиції ВСУ):** \n\n"
393
+ for index, node in enumerate(nodes, start=1):
394
+ source_title = node.node.metadata.get('title', 'Невідомий заголовок')
395
+ doc_ids = node.node.metadata.get('doc_id')
396
+ links = get_links_html(doc_ids)
397
+ search_output_content += f"\n[{index}] *{source_title}* 👉 Score: {node.score} {links}\n"
398
+
399
+ return search_output_content, nodes
400
+ except Exception as e:
401
+ return f"Error during search: {str(e)}", None
402
+
403
+ import re
404
+
405
+ import re
406
+
407
+ async def analyze_action(legal_position_json, question, nodes):
408
+ try:
409
+ workflow = CitationQueryEngineWorkflow(timeout=600)
410
+ # Запускаємо workflow і отримуємо об'єкт Response
411
+ response = await workflow.run(
412
+ query=legal_position_json["title"] + ': ' + legal_position_json["text"] + ': ' +
413
+ legal_position_json["proceeding"] + ': ' + legal_position_json["category"],
414
+ question=question,
415
+ nodes=nodes # Передаємо nodes у workflow
416
+ )
417
+
418
+ # Отримуємо текст відповіді з об'єкта Response
419
+ response_text = str(response)
420
+
421
+ # Обробка цитат у тексті відповіді
422
+ citations = re.findall(r'\[(\d+)\]', response_text)
423
+ unique_citations = sorted(set(citations), key=int)
424
+
425
+ output = f"**Аналіз Штучного Інтелекту:**\n{response_text}\n\n"
426
+ output += "**Цитовані джерела існуючих правових позицій Верховного Суду:**\n"
427
+
428
+ # Перевіряємо наявність source_nodes в об'єкті Response
429
+ source_nodes = getattr(response, 'source_nodes', [])
430
+
431
+ # Проходимо по унікальних цитатах та зіставляємо з `lp_id` у source_nodes
432
+ for citation in unique_citations:
433
+ found = False # Змінна для відстеження, чи знайдено джерело для lp_id
434
+ for index, source_node_with_score in enumerate(source_nodes, start=1):
435
+ source_node = source_node_with_score.node
436
+ lp_id = source_node.metadata.get('lp_id') # Отримуємо lp_id із метаданих джерела
437
+
438
+ # Якщо lp_id збігається з цитатою
439
+ if str(lp_id) == citation:
440
+ found = True
441
+ source_title = source_node.metadata.get('title', 'Невідомий заголовок')
442
+ doc_ids = source_node.metadata.get('doc_id')
443
+ links = get_links_html(doc_ids)
444
+ links_lp = get_links_html_lp(lp_id)
445
+
446
+ # Використовуємо `index` як номер джерела на початку рядка
447
+ output += f"[{index}]: *{source_title}* {links_lp} 👉 Score: {source_node_with_score.score} {links}\n"
448
+ break # Вихід із циклу при знайденому відповідному джерелі
449
+
450
+ if not found:
451
+ output += f"[{citation}]: Немає відповідного джерела для lp_id {citation}\n"
452
+
453
+ return output
454
+ except Exception as e:
455
+ return f"Error during analysis: {str(e)}"
456
+
457
+ # Підключаємо функції до кнопок з оновленими входами та виходами
458
+ generate_position_button.click(
459
+ fn=generate_position_action,
460
+ inputs=url_input,
461
+ outputs=[position_output, state_lp_json]
462
+ )
463
+ generate_position_button.click(
464
+ fn=lambda: gr.update(interactive=True),
465
+ inputs=None,
466
+ outputs=search_with_ai_button
467
+ )
468
+
469
+ search_with_ai_button.click(
470
+ fn=search_with_ai_action,
471
+ inputs=state_lp_json,
472
+ outputs=[search_output, state_nodes]
473
+ )
474
+ search_with_ai_button.click(
475
+ fn=lambda: gr.update(interactive=True),
476
+ inputs=None,
477
+ outputs=analyze_button
478
+ )
479
+
480
+ search_without_ai_button.click(
481
+ fn=search_without_ai_action,
482
+ inputs=url_input,
483
+ outputs=[search_output, state_nodes]
484
+ )
485
+ search_without_ai_button.click(
486
+ fn=lambda: gr.update(interactive=True),
487
+ inputs=None,
488
+ outputs=analyze_button
489
+ )
490
+
491
+ analyze_button.click(
492
+ fn=analyze_action,
493
+ inputs=[state_lp_json, question_input, state_nodes],
494
+ outputs=analysis_output
495
+ )
496
+
497
+ return app
498
+
499
+ if __name__ == "__main__":
500
+ if initialize_components():
501
+ print("Components initialized successfully!")
502
+ app = create_gradio_interface()
503
+ app.launch(share=True)
504
+ else:
505
+ print("Failed to initialize components. Please check the paths and try again.", file=sys.stderr)
506
+ sys.exit(1)