DocUA commited on
Commit
bcdb6bd
·
1 Parent(s): 060f57e

refactoring 2

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