Spaces:
Running
Running
| 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 | |
| ) | |
| 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 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 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> | |
| """ | |
| ) | |
| # 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=[p.value for p in ModelProvider], | |
| 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=[p.value for p in ModelProvider], | |
| 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=[p.value for p in ModelProvider], | |
| 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 |