| import os |
| import json |
| import re |
| import time |
| from concurrent.futures import ThreadPoolExecutor, as_completed |
| from dataclasses import dataclass, asdict |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| import gradio as gr |
| import pandas as pd |
| from huggingface_hub import InferenceClient |
|
|
| HF_TOKEN = os.getenv("HF_TOKEN", "").strip() |
|
|
| HF_MODELS = { |
| "Qwen2.5-32B-Instruct": "Qwen/Qwen2.5-32B-Instruct", |
| "DeepSeek-R1-Distill-Qwen-32B": "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B", |
| "DeepSeek-R1-Distill-Llama-8B": "deepseek-ai/DeepSeek-R1-Distill-Llama-8B", |
| "Llama-3.1-8B-Instruct": "meta-llama/Llama-3.1-8B-Instruct", |
| "Mistral-Small-24B-Instruct": "mistralai/Mistral-Small-3.1-24B-Instruct-2503", |
| } |
|
|
|
|
| @dataclass |
| class RequirementResult: |
| original_requirement: str |
| model_name: str |
| original_score: Optional[float] |
| refactored_requirement: str |
| found_issues: List[str] |
| refactoring_time_sec: float |
| status: str |
| raw_response: str |
|
|
|
|
| SYSTEM_PROMPT = """ |
| Ты — эксперт по инженерии требований и системному анализу. |
| |
| Твоя задача: |
| 1. Проанализировать исходное требование. |
| 2. Найти ошибки и проблемы качества в исходном требовании. |
| 3. Оценить качество исходного требования числом от 0 до 1: |
| - 0 = очень плохое требование |
| - 1 = качественное требование |
| 4. Выполнить рефакторинг требования, улучшив его. |
| |
| ВАЖНО: |
| - Отвечай только на русском языке. |
| - Верни только корректный JSON. |
| - Не добавляй markdown, пояснений или текста вне JSON. |
| |
| Формат ответа: |
| { |
| "original_score": 0.0, |
| "refactored_requirement": "строка", |
| "found_issues": ["ошибка 1", "ошибка 2"], |
| "comment": "краткое пояснение" |
| } |
| |
| Правила: |
| - original_score должен быть числом от 0 до 1. |
| - refactored_requirement должен быть одной улучшенной формулировкой. |
| - found_issues должен содержать список найденных проблем в исходном требовании. |
| - Все значения должны быть на русском языке. |
| """.strip() |
|
|
|
|
| def build_user_prompt(requirement: str) -> str: |
| return f""" |
| Проанализируй и отрефактори следующее требование: |
| |
| {requirement} |
| """.strip() |
|
|
|
|
| def make_hf_client() -> InferenceClient: |
| if HF_TOKEN: |
| return InferenceClient(token=HF_TOKEN) |
| return InferenceClient() |
|
|
|
|
| def safe_json_extract(text: str) -> Dict[str, Any]: |
| text = (text or "").strip() |
|
|
| try: |
| return json.loads(text) |
| except Exception: |
| pass |
|
|
| text = re.sub(r"^```(?:json)?", "", text, flags=re.IGNORECASE).strip() |
| text = re.sub(r"```$", "", text).strip() |
|
|
| match = re.search(r"\{.*\}", text, flags=re.DOTALL) |
| if match: |
| return json.loads(match.group(0)) |
|
|
| raise ValueError("Модель не вернула корректный JSON") |
|
|
|
|
| def normalize_score(value: Any) -> Optional[float]: |
| try: |
| score = float(value) |
| score = max(0.0, min(1.0, score)) |
| return round(score, 3) |
| except Exception: |
| return None |
|
|
|
|
| def normalize_result(data: Dict[str, Any]) -> Dict[str, Any]: |
| issues = data.get("found_issues", []) |
| if not isinstance(issues, list): |
| issues = [str(issues)] |
|
|
| issues = [str(x).strip() for x in issues if str(x).strip()] |
|
|
| return { |
| "original_score": normalize_score(data.get("original_score")), |
| "refactored_requirement": str(data.get("refactored_requirement", "")).strip(), |
| "found_issues": issues, |
| "comment": str(data.get("comment", "")).strip(), |
| } |
|
|
|
|
| def parse_requirements_text(raw_text: str) -> List[str]: |
| raw_text = (raw_text or "").strip() |
| if not raw_text: |
| return [] |
|
|
| lines = [line.strip() for line in raw_text.splitlines() if line.strip()] |
| cleaned = [] |
|
|
| for line in lines: |
| line = re.sub(r"^\d+[\).\s-]+", "", line).strip() |
| line = re.sub(r"^[-•*]\s*", "", line).strip() |
| if line: |
| cleaned.append(line) |
|
|
| return cleaned |
|
|
|
|
| def load_requirements_from_file(file_obj) -> List[str]: |
| if file_obj is None: |
| return [] |
|
|
| path = file_obj.name |
| ext = os.path.splitext(path)[1].lower() |
|
|
| if ext == ".csv": |
| df = pd.read_csv(path) |
|
|
| possible_columns = [ |
| "requirement", "requirements", "text", "description", |
| "требование", "требования", "текст", "описание" |
| ] |
|
|
| for col in possible_columns: |
| if col in df.columns: |
| return [str(x).strip() for x in df[col].dropna().tolist() if str(x).strip()] |
|
|
| first_col = df.columns[0] |
| return [str(x).strip() for x in df[first_col].dropna().tolist() if str(x).strip()] |
|
|
| if ext == ".txt": |
| with open(path, "r", encoding="utf-8") as f: |
| return parse_requirements_text(f.read()) |
|
|
| raise ValueError("Поддерживаются только .txt и .csv") |
|
|
|
|
| def call_model( |
| model_id: str, |
| requirement: str, |
| temperature: float, |
| max_tokens: int, |
| ) -> Tuple[str, Dict[str, Any]]: |
| client = make_hf_client() |
|
|
| response = client.chat.completions.create( |
| model=model_id, |
| messages=[ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| {"role": "user", "content": build_user_prompt(requirement)}, |
| ], |
| temperature=temperature, |
| max_tokens=max_tokens, |
| ) |
|
|
| text = response.choices[0].message.content |
| parsed = safe_json_extract(text) |
| return text, normalize_result(parsed) |
|
|
|
|
| def process_single_requirement( |
| requirement: str, |
| model_label: str, |
| temperature: float, |
| max_tokens: int, |
| ) -> RequirementResult: |
| start = time.perf_counter() |
|
|
| try: |
| if model_label not in HF_MODELS: |
| raise RuntimeError(f"Неизвестная модель: {model_label}") |
|
|
| raw_response, parsed = call_model( |
| model_id=HF_MODELS[model_label], |
| requirement=requirement, |
| temperature=temperature, |
| max_tokens=max_tokens, |
| ) |
|
|
| elapsed = round(time.perf_counter() - start, 3) |
|
|
| return RequirementResult( |
| original_requirement=requirement, |
| model_name=model_label, |
| original_score=parsed["original_score"], |
| refactored_requirement=parsed["refactored_requirement"], |
| found_issues=parsed["found_issues"], |
| refactoring_time_sec=elapsed, |
| status="ok", |
| raw_response=raw_response, |
| ) |
|
|
| except Exception as e: |
| elapsed = round(time.perf_counter() - start, 3) |
|
|
| return RequirementResult( |
| original_requirement=requirement, |
| model_name=model_label, |
| original_score=None, |
| refactored_requirement="", |
| found_issues=[], |
| refactoring_time_sec=elapsed, |
| status=f"error: {str(e)}", |
| raw_response="", |
| ) |
|
|
|
|
| def build_results_dataframe(results: List[RequirementResult]) -> pd.DataFrame: |
| rows = [] |
|
|
| for r in results: |
| rows.append({ |
| "Изначальное требование": r.original_requirement, |
| "Модель": r.model_name, |
| "Оценка изначального требования": r.original_score, |
| "Версия требования после рефакторинга": r.refactored_requirement, |
| "Найденные ошибки в изначальном требовании": "; ".join(r.found_issues), |
| "Время рефакторинга": r.refactoring_time_sec, |
| }) |
|
|
| return pd.DataFrame(rows) |
|
|
|
|
| def save_results(df: pd.DataFrame, raw_results: List[RequirementResult]) -> Tuple[str, str]: |
| csv_path = "results_table.csv" |
| json_path = "results_raw.json" |
|
|
| df.to_csv(csv_path, index=False, encoding="utf-8-sig") |
|
|
| with open(json_path, "w", encoding="utf-8") as f: |
| json.dump([asdict(x) for x in raw_results], f, ensure_ascii=False, indent=2) |
|
|
| return csv_path, json_path |
|
|
|
|
| def run_benchmark( |
| raw_requirements: str, |
| uploaded_file, |
| selected_models: List[str], |
| temperature: float, |
| max_tokens: int, |
| max_parallel_calls: int, |
| ): |
| requirements = [] |
|
|
| if raw_requirements.strip(): |
| requirements.extend(parse_requirements_text(raw_requirements)) |
|
|
| if uploaded_file is not None: |
| requirements.extend(load_requirements_from_file(uploaded_file)) |
|
|
| unique_requirements = [] |
| seen = set() |
| for req in requirements: |
| if req not in seen: |
| unique_requirements.append(req) |
| seen.add(req) |
|
|
| if not unique_requirements: |
| raise gr.Error("Добавь требования текстом или загрузи CSV/TXT файл.") |
|
|
| if not selected_models: |
| raise gr.Error("Выбери хотя бы одну модель.") |
|
|
| results: List[RequirementResult] = [] |
|
|
| futures = [] |
| with ThreadPoolExecutor(max_workers=max_parallel_calls) as executor: |
| for req in unique_requirements: |
| for model_name in selected_models: |
| futures.append( |
| executor.submit( |
| process_single_requirement, |
| requirement=req, |
| model_label=model_name, |
| temperature=temperature, |
| max_tokens=max_tokens, |
| ) |
| ) |
|
|
| for future in as_completed(futures): |
| results.append(future.result()) |
|
|
| results.sort(key=lambda x: (x.original_requirement, x.model_name)) |
|
|
| df = build_results_dataframe(results) |
| csv_path, json_path = save_results(df, results) |
|
|
| stats = ( |
| f"Обработано требований: {len(unique_requirements)}\n" |
| f"Выбрано моделей: {len(selected_models)}\n" |
| f"Всего запусков: {len(results)}" |
| ) |
|
|
| return stats, df, csv_path, json_path |
|
|
|
|
| def preview_requirements(raw_requirements: str, uploaded_file): |
| requirements = [] |
|
|
| if raw_requirements.strip(): |
| requirements.extend(parse_requirements_text(raw_requirements)) |
|
|
| if uploaded_file is not None: |
| try: |
| requirements.extend(load_requirements_from_file(uploaded_file)) |
| except Exception as e: |
| return f"Ошибка чтения файла: {e}" |
|
|
| unique_requirements = [] |
| seen = set() |
| for req in requirements: |
| if req not in seen: |
| unique_requirements.append(req) |
| seen.add(req) |
|
|
| if not unique_requirements: |
| return "Требования не найдены." |
|
|
| preview = "\n".join(f"{i+1}. {req}" for i, req in enumerate(unique_requirements[:20])) |
| if len(unique_requirements) > 20: |
| preview += f"\n... ещё {len(unique_requirements) - 20}" |
|
|
| return f"Найдено требований: {len(unique_requirements)}\n\n{preview}" |
|
|
|
|
| with gr.Blocks(title="Рефакторинг требований с помощью LLM") as demo: |
| gr.Markdown( |
| """ |
| # Сравнение моделей для анализа и рефакторинга требований |
| |
| Приложение принимает: |
| - список требований, где каждое требование идет с новой строки; |
| - или CSV-файл с требованиями. |
| |
| Для каждого требования каждая выбранная модель: |
| - оценивает исходное требование; |
| - делает рефакторинг; |
| - находит ошибки; |
| - измеряется время выполнения. |
| """ |
| ) |
|
|
| with gr.Row(): |
| with gr.Column(scale=2): |
| raw_requirements = gr.Textbox( |
| label="Список требований", |
| lines=14, |
| placeholder="Каждое новое требование — с новой строки" |
| ) |
|
|
| uploaded_file = gr.File( |
| label="Или загрузи TXT / CSV файл", |
| file_types=[".txt", ".csv"] |
| ) |
|
|
| with gr.Column(scale=1): |
| model_selector = gr.CheckboxGroup( |
| choices=list(HF_MODELS.keys()), |
| value=[ |
| "Qwen2.5-32B-Instruct", |
| "DeepSeek-R1-Distill-Qwen-32B", |
| "Llama-3.1-8B-Instruct", |
| ], |
| label="Модели" |
| ) |
|
|
| temperature = gr.Slider( |
| minimum=0.0, |
| maximum=1.0, |
| value=0.2, |
| step=0.1, |
| label="Температура" |
| ) |
|
|
| max_tokens = gr.Slider( |
| minimum=256, |
| maximum=2048, |
| value=1024, |
| step=128, |
| label="Максимум токенов" |
| ) |
|
|
| max_parallel_calls = gr.Slider( |
| minimum=1, |
| maximum=8, |
| value=3, |
| step=1, |
| label="Параллельных запросов" |
| ) |
|
|
| preview_btn = gr.Button("Проверить входные данные") |
| run_btn = gr.Button("Запустить", variant="primary") |
|
|
| preview_box = gr.Textbox( |
| label="Предпросмотр", |
| lines=10 |
| ) |
|
|
| preview_btn.click( |
| fn=preview_requirements, |
| inputs=[raw_requirements, uploaded_file], |
| outputs=preview_box, |
| ) |
|
|
| stats_box = gr.Textbox(label="Статистика") |
| results_table = gr.Dataframe(label="Результаты", wrap=True, interactive=False) |
|
|
| with gr.Row(): |
| results_csv = gr.File(label="Скачать CSV") |
| results_json = gr.File(label="Скачать JSON") |
|
|
| run_btn.click( |
| fn=run_benchmark, |
| inputs=[ |
| raw_requirements, |
| uploaded_file, |
| model_selector, |
| temperature, |
| max_tokens, |
| max_parallel_calls, |
| ], |
| outputs=[ |
| stats_box, |
| results_table, |
| results_csv, |
| results_json, |
| ], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
| |