LP_2-test / interface.py
DocUA's picture
Enhance provider management: normalize response keys, add availability checks, and improve Gradio interface for AI providers
a09e8cb
raw
history blame
44.8 kB
import gradio as gr
import asyncio
import json
import pandas as pd
from pathlib import Path
from typing import Tuple, Dict, Any, Optional
from config import (
ModelProvider, GenerationModelName, AnalysisModelName, get_settings,
DEFAULT_GENERATION_MODEL, DEFAULT_ANALYSIS_MODEL,
get_generation_models_by_provider, get_analysis_models_by_provider,
)
from utils import clean_text
from main import (
generate_legal_position,
search_with_ai_action,
analyze_action,
search_with_raw_text,
get_available_providers
)
from prompts import SYSTEM_PROMPT, LEGAL_POSITION_PROMPT, PRECEDENT_ANALYSIS_TEMPLATE
from src.session.manager import get_session_manager
from src.session.state import generate_session_id
# Load help content from HELP.md
def load_help_content() -> str:
"""Load help content from HELP.md file."""
try:
help_file = Path(__file__).parent / "HELP.md"
with open(help_file, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
return f"Помилка завантаження довідки: {str(e)}"
def get_available_provider_choices() -> list:
"""Get list of available AI providers based on API key availability."""
available = get_available_providers()
return [p.value for p in ModelProvider if available.get(p.value, False)]
def update_generation_model_choices(provider: str) -> gr.Dropdown:
"""Update generation model choices based on provider selection."""
if provider == ModelProvider.OPENAI.value:
return gr.Dropdown(
choices=[m.value for m in GenerationModelName if m.value.startswith("ft:") or m.value.startswith("gpt")],
value=GenerationModelName.GPT4_1.value,
label="Модель генерації"
)
if provider == ModelProvider.DEEPSEEK.value:
return gr.Dropdown(
choices=[m.value for m in GenerationModelName if m.value.startswith("deepseek")],
value=GenerationModelName.DEEPSEEK_CHAT.value,
label="Модель генерації"
)
elif provider == ModelProvider.ANTHROPIC.value:
return gr.Dropdown(
choices=[m.value for m in GenerationModelName if m.value.startswith("claude")],
value=GenerationModelName.CLAUDE_SONNET_4_5.value,
label="Модель генерації"
)
else: # GEMINI
return gr.Dropdown(
choices=[m.value for m in GenerationModelName if m.value.startswith("gemini")],
value=GenerationModelName.GEMINI_3_FLASH.value,
label="Модель генерації"
)
def update_thinking_visibility(provider: str):
"""Show/hide thinking controls based on provider."""
return gr.update(visible=(provider in [ModelProvider.GEMINI.value, ModelProvider.ANTHROPIC.value]))
def update_thinking_level_interactive(thinking_enabled: bool) -> tuple:
"""Enable/disable thinking controls based on checkbox."""
return (
gr.Dropdown(interactive=thinking_enabled),
gr.Slider(interactive=thinking_enabled)
)
# Session and prompt management functions
async def save_custom_prompts(
session_id: str,
system_prompt: str,
lp_prompt: str,
analysis_prompt: str
) -> Tuple[str, str]:
"""Save custom prompts to user session."""
try:
manager = get_session_manager()
session = await manager.get_session(session_id)
# Validate prompt lengths
max_length = 50000
if len(system_prompt) > max_length or len(lp_prompt) > max_length or len(analysis_prompt) > max_length:
return "❌ Помилка: Промпт занадто довгий (максимум 50000 символів)", session_id
# Save prompts
session.set_prompt('system', system_prompt)
session.set_prompt('legal_position', lp_prompt)
session.set_prompt('analysis', analysis_prompt)
await manager.update_session(session)
return "✅ Промпти успішно збережено для вашої сесії", session_id
except Exception as e:
return f"❌ Помилка при збереженні промптів: {str(e)}", session_id
async def reset_prompts_to_default(session_id: str) -> Tuple[str, str, str, str, str]:
"""Reset prompts to default values."""
try:
manager = get_session_manager()
session = await manager.get_session(session_id)
session.reset_prompts()
await manager.update_session(session)
return (
SYSTEM_PROMPT,
LEGAL_POSITION_PROMPT,
str(PRECEDENT_ANALYSIS_TEMPLATE.template),
"✅ Промпти скинуто до стандартних значень",
session_id
)
except Exception as e:
return (
SYSTEM_PROMPT,
LEGAL_POSITION_PROMPT,
str(PRECEDENT_ANALYSIS_TEMPLATE.template),
f"❌ Помилка: {str(e)}",
session_id
)
async def load_session_prompts(session_id: str) -> Tuple[str, str, str]:
"""Load prompts from user session."""
try:
manager = get_session_manager()
session = await manager.get_session(session_id)
system = session.get_prompt('system', SYSTEM_PROMPT)
legal_position = session.get_prompt('legal_position', LEGAL_POSITION_PROMPT)
analysis = session.get_prompt('analysis', str(PRECEDENT_ANALYSIS_TEMPLATE.template))
return system, legal_position, analysis
except Exception as e:
print(f"Error loading prompts: {e}")
return SYSTEM_PROMPT, LEGAL_POSITION_PROMPT, str(PRECEDENT_ANALYSIS_TEMPLATE.template)
def update_analysis_model_choices(provider: str) -> gr.Dropdown:
"""Update analysis model choices based on provider selection."""
if provider == ModelProvider.OPENAI.value:
return gr.Dropdown(
choices=[m.value for m in AnalysisModelName if m.value.startswith("gpt")],
value=AnalysisModelName.GPT4_1.value,
label="Модель аналізу"
)
elif provider == ModelProvider.DEEPSEEK.value:
return gr.Dropdown(
choices=[m.value for m in AnalysisModelName if m.value.startswith("deepseek")],
value=AnalysisModelName.DEEPSEEK_CHAT.value,
label="Модель аналізу"
)
elif provider == ModelProvider.ANTHROPIC.value:
return gr.Dropdown(
choices=[m.value for m in AnalysisModelName if m.value.startswith("claude")],
value=AnalysisModelName.CLAUDE_SONNET_4_5.value,
label="Модель аналізу"
)
else: # GEMINI
return gr.Dropdown(
choices=[m.value for m in AnalysisModelName if m.value.startswith("gemini")],
value=AnalysisModelName.GEMINI_3_FLASH.value,
label="Модель аналізу"
)
async def process_input(
text_input: str,
url_input: str,
file_input: gr.File,
comment_input: str,
input_method: str,
provider: str,
model_name: str,
thinking_enabled: bool = False,
thinking_level: str = "MEDIUM",
thinking_budget: int = 10000,
session_id: str = None
) -> Tuple[str, Optional[Dict[str, Any]], str]:
"""Process input and generate legal position."""
try:
input_type = "text"
input_text = ""
# Determine which input source has actual content
if input_method == "Завантаження файлу":
if not file_input:
return "❌ Помилка: Будь ласка, завантажте файл", None, session_id
try:
with open(file_input.name, 'r', encoding='utf-8') as file:
input_text = file.read()
except UnicodeDecodeError:
with open(file_input.name, 'r', encoding='cp1251') as file:
input_text = file.read()
elif input_method == "URL посилання":
input_type = "url"
input_text = url_input
else:
# Default to text input, but check if URL is provided instead
if url_input and url_input.strip():
input_type = "url"
input_text = url_input
else:
input_text = text_input
# Check if input is empty and provide specific error message
if not input_text or not input_text.strip():
if input_method == "URL посилання" or (url_input and url_input.strip()):
return "❌ Помилка: Будь ласка, введіть URL посилання на судове рішення", None, session_id
elif input_method == "Текстовий ввід":
return "❌ Помилка: Будь ласка, введіть текст судового рішення", None, session_id
else:
return "❌ Помилка: Текст не може бути порожнім", None, session_id
# Get custom prompts from session
manager = get_session_manager()
session = await manager.get_session(session_id)
custom_system_prompt = session.get_prompt('system', SYSTEM_PROMPT)
custom_lp_prompt = session.get_prompt('legal_position', LEGAL_POSITION_PROMPT)
# Don't clean here - let generate_legal_position handle it to avoid double cleaning
# input_text = clean_text(input_text)
# comment_input = clean_text(comment_input) if comment_input else ""
legal_position_json = generate_legal_position(
input_text,
input_type,
comment_input if comment_input else "",
provider,
model_name,
thinking_enabled,
thinking_level,
thinking_budget,
custom_system_prompt,
custom_lp_prompt
)
if isinstance(legal_position_json, dict) and all(
key in legal_position_json for key in ["title", "text", "proceeding", "category"]):
position_output_content = (
f"**Проект правової позиції суду (модель: {model_name}):**\n"
f"*{clean_text(legal_position_json['title'])}*\n\n"
f"{clean_text(legal_position_json['text'])}\n\n"
f"**Категорія:**\n"
f"{clean_text(legal_position_json['category'])} ({clean_text(legal_position_json['proceeding'])})\n\n"
)
# Store in session
session.legal_position_json = legal_position_json
await manager.update_session(session)
return position_output_content, legal_position_json, session_id
else:
return f"Помилка: Неправильний формат відповіді від моделі", None, session_id
except Exception as e:
return f"Помилка при генерації позиції: {str(e)}", None, session_id
async def process_raw_text_search(text, url, file, method, state_lp_json):
"""Process raw text search and update necessary states."""
try:
input_text = ""
# Determine which input source has actual content
if method == "Завантаження файлу":
if not file:
return "❌ Помилка: Будь ласка, завантажте файл", None, state_lp_json
try:
with open(file.name, 'r', encoding='utf-8') as f:
input_text = f.read()
except UnicodeDecodeError:
with open(file.name, 'r', encoding='cp1251') as f:
input_text = f.read()
elif method == "URL посилання":
input_text = url
else:
# Default to text input, but check if URL is provided instead
if url and url.strip():
input_text = url
else:
input_text = text
# Check if input is empty and provide specific error message
if not input_text or not input_text.strip():
if method == "URL посилання" or (url and url.strip()):
return "❌ Помилка: Будь ласка, введіть URL посилання на судове рішення", None, state_lp_json
elif method == "Текстовий ввід":
return "❌ Помилка: Будь ласка, введіть текст судового рішення", None, state_lp_json
else:
return "❌ Помилка: Порожній текст", None, state_lp_json
input_text = clean_text(input_text)
search_result, nodes = await search_with_raw_text(input_text)
if not state_lp_json:
state_lp_json = {
"title": "Пошук за текстом",
"text": input_text[:500] + "..." if len(input_text) > 500 else input_text,
"proceeding": "Не визначено",
"category": "Пошук за текстом"
}
if nodes is None:
return "Помилка: Не знайдено результатів", None, state_lp_json
return search_result, nodes, state_lp_json
except Exception as e:
return f"Помилка при пошуку: {str(e)}", None, state_lp_json
# Batch testing functions
async def load_csv_file(file) -> Tuple[str, Optional[pd.DataFrame]]:
"""Load CSV file and validate it has a 'text' column."""
try:
if file is None:
return "Помилка: Файл не вибрано", None
# Try to read CSV with different encodings
try:
df = pd.read_csv(file.name, encoding='utf-8')
except UnicodeDecodeError:
try:
df = pd.read_csv(file.name, encoding='cp1251')
except Exception as e:
return f"Помилка читання CSV: {str(e)}", None
# Validate 'text' column exists
if 'text' not in df.columns:
return f"Помилка: CSV файл повинен містити колонку 'text'. Знайдені колонки: {', '.join(df.columns)}", None
# Show preview
rows_count = len(df)
preview_msg = f"✅ Файл завантажено успішно!\n\n**Кількість рядків:** {rows_count}\n\n**Колонки:** {', '.join(df.columns)}\n\n**Перші 3 рядки (текст):**\n"
for idx, row in df.head(3).iterrows():
text_preview = str(row['text'])[:100] + "..." if len(str(row['text'])) > 100 else str(row['text'])
preview_msg += f"\n{idx + 1}. {text_preview}\n"
return preview_msg, df
except Exception as e:
return f"Помилка при завантаженні файлу: {str(e)}", None
async def process_batch_testing(
df: pd.DataFrame,
provider: str,
model_name: str,
delay_seconds: float = 1.0,
progress=gr.Progress()
) -> Tuple[str, Optional[str]]:
"""Process batch testing of legal position generation."""
try:
if df is None:
return "Помилка: Спочатку завантажте CSV файл", None
total_rows = len(df)
results = []
# Create column name based on model
result_column_name = model_name
progress(0, desc="Початок пакетної генерації...")
for idx, row in df.iterrows():
# Update progress
current_progress = (idx + 1) / total_rows
progress(current_progress, desc=f"Обробка рядка {idx + 1} з {total_rows}")
court_decision_text = str(row['text'])
# Generate legal position
try:
legal_position_json = generate_legal_position(
input_text=court_decision_text,
input_type="text",
comment_input="",
provider=provider,
model_name=model_name
)
# Store full JSON result
if isinstance(legal_position_json, dict):
# Convert dict to JSON string for CSV storage
result_text = json.dumps(legal_position_json, ensure_ascii=False)
else:
result_text = f"Помилка: {str(legal_position_json)}"
except Exception as e:
result_text = f"Помилка генерації: {str(e)}"
results.append(result_text)
# Add delay between requests (except for the last one)
if idx < total_rows - 1 and delay_seconds > 0:
await asyncio.sleep(delay_seconds)
# Add results to dataframe
df[result_column_name] = results
# Save to temporary file
output_dir = Path("test_results")
output_dir.mkdir(exist_ok=True)
timestamp = pd.Timestamp.now().strftime("%Y%m%d_%H%M%S")
output_filename = f"batch_test_results_{model_name}_{timestamp}.csv"
output_path = output_dir / output_filename
df.to_csv(output_path, index=False, encoding='utf-8')
success_msg = f"✅ **Пакетне тестування завершено!**\n\n"
success_msg += f"**Оброблено рядків:** {total_rows}\n"
success_msg += f"**Модель:** {model_name}\n"
success_msg += f"**Результати збережено в:** {output_path}\n\n"
success_msg += f"**Нова колонка:** {result_column_name}\n"
return success_msg, str(output_path)
except Exception as e:
return f"Помилка при пакетному тестуванні: {str(e)}", None
def create_gradio_interface() -> gr.Blocks:
"""Create and configure the Gradio interface."""
# Load theme and CSS from YAML config
try:
settings = get_settings(validate_api_keys=False)
gradio_cfg = settings.gradio
# Build theme from config
theme_map = {
"Soft": gr.themes.Soft,
"Default": gr.themes.Default,
"Glass": gr.themes.Glass,
"Monochrome": gr.themes.Monochrome,
"Base": gr.themes.Base,
}
theme_cls = theme_map.get(gradio_cfg.theme.base, gr.themes.Soft)
theme = theme_cls(
primary_hue=gradio_cfg.theme.primary_hue,
secondary_hue=gradio_cfg.theme.secondary_hue,
)
custom_css = gradio_cfg.css or ""
except Exception as e:
print(f"[WARNING] Could not load Gradio config from YAML: {e}, using defaults")
theme = gr.themes.Soft(primary_hue="blue", secondary_hue="indigo")
custom_css = """
.contain { display: flex; flex-direction: column; }
.tab-content { padding: 16px; border-radius: 8px; background: white; }
.header { margin-bottom: 24px; text-align: center; }
.tab-header { font-size: 1.2em; margin-bottom: 16px; color: #2563eb; }
"""
# Resolve default provider and models from YAML config
try:
_settings = get_settings(validate_api_keys=False)
_default_provider = _settings.models.default_provider # e.g. "anthropic"
except Exception:
_default_provider = "anthropic"
# Get available providers based on API key availability
_available_providers = get_available_provider_choices()
# If default provider is not available, use first available one
if _default_provider not in _available_providers:
if _available_providers:
_default_provider = _available_providers[0]
print(f"[WARNING] Default provider not available, using: {_default_provider}")
else:
print("[ERROR] No AI providers available! Please set at least one API key.")
_default_provider = "anthropic" # Fallback for UI rendering
# Get default generation model for the provider
_gen_models = get_generation_models_by_provider(_default_provider)
if DEFAULT_GENERATION_MODEL and DEFAULT_GENERATION_MODEL.value in _gen_models:
_default_gen_model = DEFAULT_GENERATION_MODEL.value
elif _gen_models:
_default_gen_model = _gen_models[0]
else:
_default_gen_model = None
# Get default analysis model for the provider
_ana_models = get_analysis_models_by_provider(_default_provider)
if DEFAULT_ANALYSIS_MODEL and DEFAULT_ANALYSIS_MODEL.value in _ana_models:
_default_ana_model = DEFAULT_ANALYSIS_MODEL.value
elif _ana_models:
_default_ana_model = _ana_models[0]
else:
_default_ana_model = None
print(f"[CONFIG] Default provider: {_default_provider}")
print(f"[CONFIG] Default generation model: {_default_gen_model}")
print(f"[CONFIG] Default analysis model: {_default_ana_model}")
with gr.Blocks(
title="AI Асистент LP 2.0",
) as app:
# Apply theme and css directly to the Blocks object
app.theme = theme
app.css = custom_css or """
.contain { display: flex; flex-direction: column; }
.tab-content { padding: 16px; border-radius: 8px; background: white; border: 1px solid #e5e7eb; }
.header-container {
text-align: center;
margin-bottom: 2rem;
padding: 1rem;
background: linear-gradient(to right, #f8fafc, #ffffff, #f8fafc);
border-bottom: 1px solid #e2e8f0;
}
.header-title {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.5rem;
}
.header-subtitle {
font-size: 1.25rem;
color: #475569;
font-weight: 400;
}
.tab-header {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: #334155;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 0.5rem;
}
.custom-btn-primary {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: none;
color: white;
}
"""
# New Header Design
gr.HTML(
"""
<div class="header-container">
<div class="header-title">⚖️ Legal Position AI</div>
<div class="header-subtitle">Інтелектуальний AI-Асистент для аналізу судової практики Верховного Суду</div>
</div>
"""
)
# Show provider availability status
_all_providers = {p.value for p in ModelProvider}
_unavailable = _all_providers - set(_available_providers)
if _unavailable:
unavailable_list = ", ".join(sorted(_unavailable))
gr.Info(
f"⚠️ Недоступні провайдери (відсутні API ключі): {unavailable_list}\n"
f"Додайте відповідні API ключі в налаштуваннях HF Space для активації.",
title="Інформація про провайдери",
duration=10
)
# Session state - generates unique ID for each browser session
session_id_state = gr.State(value=generate_session_id)
# Tracks current input method ("Текстовий ввід", "URL посилання", "Завантаження файлу")
# Initialize with "URL посилання" as it's the most common use case maybe? Or stick to input.
# Let's default to "URL посилання" as requested in similar contexts, or keep "Текстовий ввід".
# User screen showed "URL посилання", let's make that default if we want user friendly.
# But for now I'll stick to logic below.
input_method_state = gr.State(value="Текстовий ввід")
# Legacy states
state_lp_json = gr.State()
state_nodes = gr.State()
with gr.Tabs(selected=0) as tabs:
# Вкладка Генерація
with gr.Tab("💡 Генерація", id=0):
with gr.Row():
# Configuration Column
with gr.Column(scale=3, variant="panel"):
gr.Markdown("### 🤖 Налаштування моделі")
with gr.Row():
generation_provider_dropdown = gr.Dropdown(
choices=_available_providers,
value=_default_provider,
label="Провайдер AI",
container=False,
scale=1
)
generation_model_dropdown = gr.Dropdown(
choices=_gen_models,
value=_default_gen_model,
label="Модель генерації",
container=False,
scale=2
)
# Advanced Settings in Accordion to save space
with gr.Accordion("⚙️ Додаткові параметри (Thinking Mode)", open=False) as thinking_accordion:
thinking_enabled_checkbox = gr.Checkbox(
label="Увімкнути режим Thinking (глибокий аналіз)",
value=False,
info="Активує розширений ланцюг міркувань для моделей Gemini 3+ та Claude 4.5"
)
with gr.Row():
thinking_level_dropdown = gr.Dropdown(
choices=["Minimal", "Low", "Medium", "High"],
value="Medium",
label="Рівень Thinking (Gemini)",
interactive=False
)
thinking_budget_slider = gr.Slider(
minimum=1000,
maximum=20000,
value=10000,
step=1000,
label="Бюджет токенів (Claude)",
interactive=False
)
gr.Markdown("### 📄 Вхідні дані")
# New Tabs-based Input Selection
with gr.Tabs() as input_tabs:
with gr.TabItem("📝 Текст рішення", id="text_tab"):
text_input = gr.Textbox(
show_label=False,
placeholder="Вставте повний текст судового рішення сюди...",
lines=12,
max_lines=30
)
with gr.TabItem("🔗 URL посилання", id="url_tab"):
url_input = gr.Textbox(
show_label=False,
placeholder="https://reyestr.court.gov.ua/Review/...",
info="Підтримуються посилання на Єдиний державний реєстр судових рішень"
)
with gr.TabItem("📂 Завантаження файлу", id="file_tab"):
file_input = gr.File(
label="Перетягніть файл або натисніть для вибору",
file_types=[".txt", ".docx", ".pdf"], # Added docx/pdf just for UI (backend needs support)
file_count="single"
)
# Hidden grouping for thinking visibility
thinking_settings_group = gr.Group(visible=True) # Initially visible, visibility controlled by provider
with thinking_settings_group:
# This empty context is just to register the variable if I use it later,
# but actually thinking controls are ALREADY inside Accordion.
# The Accordion itself should be the thing I toggle?
# Or the Row with checkbox.
pass
with gr.Column(variant="panel"):
comment_input = gr.Textbox(
label="Коментар до генерації (опціонально)",
placeholder="Наприклад: 'Зробити акцент на процесуальних строках'...",
lines=2
)
generate_position_button = gr.Button(
"� Згенерувати правову позицію",
variant="primary",
size="lg"
)
position_output = gr.Markdown(
label="Результат",
elem_classes=["tab-content"]
)
# Вкладка Пошук
with gr.Tab("🔍 Пошук", id=1):
gr.Markdown("### Пошук схожих правових позицій", elem_classes=["tab-header"])
with gr.Row():
search_with_ai_button = gr.Button(
"🔎 Пошук на основі правової позиції",
variant="primary",
interactive=False
)
search_with_text_button = gr.Button(
"🔎 Пошук на основі вхідного тексту",
variant="primary",
interactive=True
)
search_output = gr.Markdown(
label="Результати пошуку",
elem_classes=["tab-content"]
)
# Вкладка Аналіз
with gr.Tab("⚖️ Аналіз", id=2):
gr.Markdown("### Порівняльний аналіз нової правової позиції із знайденими в результаті пошуку", elem_classes=["tab-header"])
with gr.Row():
analysis_provider_dropdown = gr.Dropdown(
choices=_available_providers,
value=_default_provider,
label="Провайдер AI",
scale=1
)
analysis_model_dropdown = gr.Dropdown(
choices=_ana_models,
value=_default_ana_model,
label="Модель аналізу",
scale=1
)
question_input = gr.Textbox(
label="Уточнююче питання для аналізу",
placeholder="Введіть питання для уточнення аналізу...",
lines=2
)
analyze_button = gr.Button(
"⚖️ Аналіз результатів пошуку",
variant="primary",
interactive=False
)
analysis_output = gr.Markdown(
label="Результати аналізу",
elem_classes=["tab-content"]
)
# Вкладка Налаштування (Settings)
with gr.Tab("⚙️ Налаштування", id=3):
gr.Markdown("### Редагування промптів", elem_classes=["tab-header"])
gr.Markdown("""
**Увага!** Налаштування промптів зберігаються тільки для вашої поточної сесії.
Кожен користувач має свої власні налаштування, які не впливають на інших користувачів.
""")
with gr.Column():
system_prompt_editor = gr.Textbox(
label="📋 Системний промпт",
value=SYSTEM_PROMPT,
lines=5,
max_lines=10,
placeholder="Введіть системний промпт...",
info="Визначає роль та базові інструкції для AI"
)
lp_prompt_editor = gr.Textbox(
label="⚖️ Промпт генерації правової позиції",
value=LEGAL_POSITION_PROMPT,
lines=15,
max_lines=30,
placeholder="Введіть промпт для генерації правової позиції...",
info="Шаблон для генерації правової позиції з судового рішення"
)
analysis_prompt_editor = gr.Textbox(
label="🔍 Промпт аналізу прецедентів",
value=str(PRECEDENT_ANALYSIS_TEMPLATE.template),
lines=15,
max_lines=30,
placeholder="Введіть промпт для аналізу прецедентів...",
info="Шаблон для порівняльного аналізу правових позицій"
)
with gr.Row():
save_prompts_button = gr.Button(
"💾 Зберегти промпти",
variant="primary",
scale=1
)
reset_prompts_button = gr.Button(
"🔄 Скинути до стандартних",
variant="secondary",
scale=1
)
prompts_status = gr.Markdown(
"",
elem_classes=["tab-content"]
)
# Вкладка Пакетне тестування (Batch Testing)
with gr.Tab("📊 Пакетне тестування", id=4):
gr.Markdown("### Пакетна генерація правових позицій з CSV файлу", elem_classes=["tab-header"])
gr.Markdown("""
**Інструкція:**
1. Виберіть провайдера AI та модель для генерації
2. Завантажте CSV файл, що містить колонку `text` з текстами судових рішень
3. Запустіть пакетне тестування
4. Завантажте результати у форматі CSV
**Формат CSV файлу:**
- Обов'язково повинна бути колонка `text` з текстами судових рішень
- Результати будуть збережені в новій колонці з назвою моделі
""")
with gr.Row():
batch_provider_dropdown = gr.Dropdown(
choices=_available_providers,
value=_default_provider,
label="Провайдер AI",
scale=1
)
batch_model_dropdown = gr.Dropdown(
choices=_gen_models,
value=_default_gen_model,
label="Модель генерації",
scale=1
)
delay_slider = gr.Slider(
minimum=0,
maximum=10,
value=1,
step=0.5,
label="⏱️ Пауза між запитами (секунди)",
info="Затримка між обробкою кожного рядка для уникнення перевантаження API"
)
csv_file_input = gr.File(
label="📁 Завантажте CSV файл з тестовими даними",
file_types=[".csv"],
type="filepath"
)
csv_preview_output = gr.Markdown(
label="Попередній перегляд файлу",
elem_classes=["tab-content"]
)
# State to store loaded dataframe
batch_df_state = gr.State()
load_csv_button = gr.Button(
"📂 Завантажити CSV файл",
variant="secondary",
scale=1
)
start_batch_button = gr.Button(
"▶️ Запустити пакетне тестування",
variant="primary",
scale=1,
interactive=False
)
batch_output = gr.Markdown(
label="Результати пакетного тестування",
elem_classes=["tab-content"]
)
download_results_file = gr.File(
label="📥 Завантажити результати",
visible=False
)
# Вкладка Допомога (Help)
with gr.Tab("📖 Допомога", id=5):
gr.Markdown("### Довідка по використанню AI Асистента", elem_classes=["tab-header"])
help_content = load_help_content()
gr.Markdown(
help_content,
elem_classes=["tab-content"]
)
# Event handlers
def update_input_state(evt: gr.SelectData):
# Map tab IDs to input method strings used by process_input
mapping = {
"text_tab": "Текстовий ввід",
"url_tab": "URL посилання",
"file_tab": "Завантаження файлу"
}
return mapping.get(evt.value, "Текстовий ввід")
def update_analyze_button_status(tab_id):
return gr.update(interactive=state_nodes is not None)
# Update input method state when tab changes
input_tabs.select(
fn=update_input_state,
inputs=None,
outputs=[input_method_state]
)
# provider dropdown changes
generation_provider_dropdown.change(
fn=update_generation_model_choices,
inputs=[generation_provider_dropdown],
outputs=[generation_model_dropdown]
)
analysis_provider_dropdown.change(
fn=update_analysis_model_choices,
inputs=[analysis_provider_dropdown],
outputs=[analysis_model_dropdown]
)
batch_provider_dropdown.change(
fn=update_generation_model_choices,
inputs=[batch_provider_dropdown],
outputs=[batch_model_dropdown]
)
# thinking mode settings
generation_provider_dropdown.change(
fn=update_thinking_visibility,
inputs=[generation_provider_dropdown],
outputs=[thinking_accordion]
)
thinking_enabled_checkbox.change(
fn=update_thinking_level_interactive,
inputs=[thinking_enabled_checkbox],
outputs=[thinking_level_dropdown, thinking_budget_slider]
)
# generation and analysis
generate_position_button.click(
fn=process_input,
inputs=[
text_input,
url_input,
file_input,
comment_input,
input_method_state,
generation_provider_dropdown,
generation_model_dropdown,
thinking_enabled_checkbox,
thinking_level_dropdown,
thinking_budget_slider,
session_id_state
],
outputs=[position_output, state_lp_json, session_id_state]
).then(
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]
).then(
fn=lambda: gr.update(interactive=True),
inputs=None,
outputs=analyze_button
)
search_with_text_button.click(
fn=process_raw_text_search,
inputs=[text_input, url_input, file_input, input_method_state, state_lp_json],
outputs=[search_output, state_nodes, state_lp_json]
).then(
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,
analysis_provider_dropdown,
analysis_model_dropdown
],
outputs=analysis_output
)
# Settings tab event handlers
save_prompts_button.click(
fn=save_custom_prompts,
inputs=[
session_id_state,
system_prompt_editor,
lp_prompt_editor,
analysis_prompt_editor
],
outputs=[prompts_status, session_id_state]
)
reset_prompts_button.click(
fn=reset_prompts_to_default,
inputs=[session_id_state],
outputs=[
system_prompt_editor,
lp_prompt_editor,
analysis_prompt_editor,
prompts_status,
session_id_state
]
)
# Batch testing tab event handlers
load_csv_button.click(
fn=load_csv_file,
inputs=[csv_file_input],
outputs=[csv_preview_output, batch_df_state]
).then(
fn=lambda df: gr.update(interactive=df is not None),
inputs=[batch_df_state],
outputs=[start_batch_button]
)
start_batch_button.click(
fn=process_batch_testing,
inputs=[
batch_df_state,
batch_provider_dropdown,
batch_model_dropdown,
delay_slider
],
outputs=[batch_output, download_results_file]
).then(
fn=lambda output_path: gr.update(visible=output_path is not None, value=output_path),
inputs=[download_results_file],
outputs=[download_results_file]
)
# Removed app.load call to avoid startup race condition with session state
# Prompts are already initialized with default values in the UI components
# and session is fresh on every reload anyway.
return app