DocUA commited on
Commit
3879124
·
1 Parent(s): 1624a3f

Додано підтримку кількох провайдерів API (OpenRouter та Anthropic) в аналізаторі НПА. Оновлено інтерфейс для вибору провайдера та моделі, а також перевірки наявності API ключів. Внесено зміни в логіку аналізу та форматування результатів. Додано нові функції для зміни провайдера та моделі.

Browse files
app.py CHANGED
@@ -3,20 +3,23 @@ import sys
3
  from src.interface import create_interface
4
 
5
  if __name__ == "__main__":
6
- # Перевірка наявності API ключа
7
- if not os.getenv('ANTHROPIC_API_KEY'):
8
- print("Помилка: Змінна середовища ANTHROPIC_API_KEY не встановлена.")
9
- print("Будь ласка, встановіть її перед запуском.")
10
- print("Наприклад: export ANTHROPIC_API_KEY=your-api-key")
 
11
  sys.exit(1)
12
 
13
  try:
14
- print("Запуск аналізатора НПА з режимом 'extended thinking'")
15
  print("=======================================================")
16
  print("Доступні налаштування в інтерфейсі:")
17
- print("1. Увімкнути/вимкнути режим роздумів")
18
- print("2. Налаштувати бюджет токенів для роздумів (1024-8000)")
19
- print("3. Вибрати чи відображати роздуми моделі в інтерфейсі")
 
 
20
  print("=======================================================")
21
 
22
  # Створення та запуск інтерфейсу
 
3
  from src.interface import create_interface
4
 
5
  if __name__ == "__main__":
6
+ # Перевірка наявності API ключа (OpenRouter або Anthropic)
7
+ if not os.getenv('OPENROUTER_API_KEY') and not os.getenv('ANTHROPIC_API_KEY'):
8
+ print("Помилка: Жоден з API ключів не встановлено.")
9
+ print("Будь ласка, встановіть хоча б один ключ перед запуском:")
10
+ print("Наприклад: export OPENROUTER_API_KEY=your-api-key")
11
+ print("Або: export ANTHROPIC_API_KEY=your-api-key")
12
  sys.exit(1)
13
 
14
  try:
15
+ print("Запуск аналізатора НПА")
16
  print("=======================================================")
17
  print("Доступні налаштування в інтерфейсі:")
18
+ print("1. Вибір провайдера (OpenRouter, Anthropic)")
19
+ print("2. Вибір моделі в межах провайдера")
20
+ print("3. Увімкнення режиму роздумів (якщо підтримується)")
21
+ print("4. Налаштування бюджету токенів для роздумів (1024-8000)")
22
+ print("5. Вибір чи відображати роздуми моделі в інтерфейсі")
23
  print("=======================================================")
24
 
25
  # Створення та запуск інтерфейсу
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  anthropic==0.47.1
2
  gradio>=4.44.1
3
- python-dotenv>=1.0.0
 
 
1
  anthropic==0.47.1
2
  gradio>=4.44.1
3
+ python-dotenv>=1.0.0
4
+ openai>=1.0.0
src/__pycache__/analyzer.cpython-310.pyc CHANGED
Binary files a/src/__pycache__/analyzer.cpython-310.pyc and b/src/__pycache__/analyzer.cpython-310.pyc differ
 
src/__pycache__/interface.cpython-310.pyc CHANGED
Binary files a/src/__pycache__/interface.cpython-310.pyc and b/src/__pycache__/interface.cpython-310.pyc differ
 
src/analyzer.py CHANGED
@@ -1,30 +1,32 @@
1
  import os
2
- import anthropic
3
- import pkg_resources
4
- from .prompts import SYSTEM_PROMPT, get_analysis_prompt
5
 
6
 
7
  class NPAAnalyzer:
8
- def __init__(self):
9
  self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10
  self.methodology_path = os.path.join(self.base_dir, 'data', 'methodology.txt')
11
 
12
- api_key = os.getenv('ANTHROPIC_API_KEY')
13
- if not api_key:
14
- raise ValueError("API key not found in environment variables")
15
-
16
- # Створення клієнта Anthropic
17
- self.client = anthropic.Anthropic(api_key=api_key)
18
-
19
- # Отримання версії бібліотеки
20
  try:
21
- anthropic_version = pkg_resources.get_distribution("anthropic").version
22
- print(f"Використовується Anthropic SDK версії: {anthropic_version}")
23
- # Ми вважаємо, що версія 0.47.1 або вище підтримує thinking
24
- self.supports_thinking = True
25
- except:
26
- print("Не вдалося визначити версію Anthropic SDK")
27
- self.supports_thinking = True # Припускаємо, що підтримується, бо SDK оновлено
 
 
 
 
 
 
 
 
 
 
28
 
29
  self.methodology = self._load_methodology()
30
  print(f"Методологія завантажена, розмір: {len(self.methodology)} символів")
@@ -46,11 +48,10 @@ class NPAAnalyzer:
46
  def analyze_npa(self, npa_text: str, enable_thinking=False, thinking_budget=2000) -> dict:
47
  """
48
  Аналізує нормативно-правовий акт і повертає словник з текстом відповіді та роздумами.
49
- Використовує потокову передачу даних для довгих запитів.
50
 
51
  Args:
52
  npa_text (str): Текст НПА для аналізу
53
- enable_thinking (bool): Чи включати режим "thinking" (за замовчуванням True)
54
  thinking_budget (int): Бюджет токенів для режиму "thinking" (за замовчуванням 2000)
55
 
56
  Returns:
@@ -60,95 +61,58 @@ class NPAAnalyzer:
60
  return {"response": "Будь ласка, введіть текст НПА для аналізу.", "thinking": None}
61
 
62
  try:
63
- # Підготовка повідомлення користувача
64
- user_message = {
65
- "role": "user",
66
- "content": [
67
- {
68
- "type": "text",
69
- "text": get_analysis_prompt(self.methodology, npa_text)
70
- }
71
- ]
72
- }
73
-
74
- # Спільні параметри запиту (без stream, оскільки він вже є в messages.stream())
75
- params = {
76
- "model": "claude-3-7-sonnet-20250219",
77
- "max_tokens": 16000,
78
- "system": "Ти - експерт з антикорупційної експертизи нормативно-правових актів.",
79
- "messages": [user_message]
80
- }
81
-
82
- # Додавання параметра thinking, якщо дозволено і підтримується
83
- if enable_thinking and self.supports_thinking:
84
- print(f"Використовуємо параметр thinking при запиті з бюджетом: {thinking_budget} токенів")
85
- params["thinking"] = {
86
- "type": "enabled",
87
- "budget_tokens": thinking_budget
88
- }
89
- else:
90
- print("Режим thinking вимкнено або не підтримується")
91
-
92
- # Підготовка результату
93
- result = {
94
- "response": "", # Основна відповідь (text блок)
95
- "thinking": "" # Блок "роздумів" (thinking блок)
96
- }
97
 
98
- # Виконання запиту з потоковою передачею (без додаткового параметра stream)
99
- with self.client.messages.stream(**params) as stream:
100
- for event in stream:
101
- if event.type == "content_block_delta":
102
- if event.delta.type == "text_delta":
103
- # Додаємо частину тексту до відповіді
104
- result["response"] += event.delta.text
105
- elif event.delta.type == "thinking_delta":
106
- # Додаємо частину мислення до блоку thinking
107
- result["thinking"] += event.delta.thinking
108
 
109
- # Якщо відповідь порожня, але є "роздуми"
110
- if not result["response"] and result["thinking"]:
111
- result["response"] = "Не вдалося отримати текстову відповідь, але доступні роздуми моделі."
112
- elif not result["response"] and not result["thinking"]:
113
- result["response"] = "Не вдалося отримати відповідь від моделі."
114
-
115
  return result
116
 
117
  except Exception as e:
118
  error_message = str(e)
119
  print(f"Помилка при аналізі: {error_message}")
120
-
121
- # Якщо помилка пов'язана зі streaming, спробуємо без нього
122
- if "streaming" in error_message.lower() or "stream" in error_message.lower():
123
- print("Спроба виконати запит без потокової передачі")
124
- try:
125
- # Виконання запиту без streaming
126
- response = self.client.messages.create(**params)
127
-
128
- # Підготовка результату
129
- result = {
130
- "response": None, # Основна відповідь (text блок)
131
- "thinking": None # Блок "роздумів" (thinking блок)
132
- }
133
-
134
- # Обробка відповіді
135
- if hasattr(response, 'content') and isinstance(response.content, list):
136
- for content_block in response.content:
137
- if hasattr(content_block, 'type') and content_block.type == 'text':
138
- if hasattr(content_block, 'text'):
139
- result["response"] = content_block.text
140
- elif hasattr(content_block, 'type') and content_block.type == 'thinking':
141
- if hasattr(content_block, 'thinking'):
142
- if result["thinking"] is not None:
143
- result["thinking"] += "\n\n" + content_block.thinking
144
- else:
145
- result["thinking"] = content_block.thinking
146
-
147
- if not result["response"]:
148
- result["response"] = "Не вдалося отримати текстову відповідь від моделі."
149
-
150
- return result
151
- except Exception as e2:
152
- return {"response": f"Помилка при аналізі: {str(e2)}", "thinking": None}
153
-
154
- return {"response": f"Помилка при аналізі: {error_message}", "thinking": None}
 
 
 
 
 
1
  import os
2
+ from .llm_providers import get_provider, get_available_providers
3
+ from .prompts import get_analysis_prompt
 
4
 
5
 
6
  class NPAAnalyzer:
7
+ def __init__(self, provider_name="openrouter", model=None):
8
  self.base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9
  self.methodology_path = os.path.join(self.base_dir, 'data', 'methodology.txt')
10
 
11
+ # Ініціалізація провайдера
 
 
 
 
 
 
 
12
  try:
13
+ self.provider = get_provider(provider_name, model=model)
14
+ print(f"Використовується провайдер: {self.provider.name}, модель: {model or 'за замовчуванням'}")
15
+ except ValueError as e:
16
+ print(f"Помилка при ініціалізації провайдера: {str(e)}")
17
+ print("Спроба ініціалізації OpenRouter...")
18
+
19
+ try:
20
+ # Спроба OpenRouter
21
+ self.provider = get_provider("openrouter")
22
+ print(f"Успішно ініціалізовано OpenRouter")
23
+ except ValueError as e:
24
+ print(f"Помилка при ініціалізації OpenRouter: {str(e)}")
25
+ print("Спроба ініціалізації Anthropic...")
26
+
27
+ # Спроба Anthropic
28
+ self.provider = get_provider("anthropic")
29
+ print(f"Успішно ініціалізовано Anthropic")
30
 
31
  self.methodology = self._load_methodology()
32
  print(f"Методологія завантажена, розмір: {len(self.methodology)} символів")
 
48
  def analyze_npa(self, npa_text: str, enable_thinking=False, thinking_budget=2000) -> dict:
49
  """
50
  Аналізує нормативно-правовий акт і повертає словник з текстом відповіді та роздумами.
 
51
 
52
  Args:
53
  npa_text (str): Текст НПА для аналізу
54
+ enable_thinking (bool): Чи включати режим "thinking" (за замовчуванням False)
55
  thinking_budget (int): Бюджет токенів для режиму "thinking" (за замовчуванням 2000)
56
 
57
  Returns:
 
61
  return {"response": "Будь ласка, введіть текст НПА для аналізу.", "thinking": None}
62
 
63
  try:
64
+ # Підготовка промпту
65
+ analysis_prompt = get_analysis_prompt(self.methodology, npa_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ # Використовуємо відповідний провайдер для аналізу
68
+ # Якщо провайдер не підтримує thinking, але enable_thinking=True, то це буде проігноровано
69
+ result = self.provider.analyze(
70
+ prompt=analysis_prompt,
71
+ enable_thinking=enable_thinking and self.provider.supports_thinking,
72
+ thinking_budget=thinking_budget
73
+ )
 
 
 
74
 
 
 
 
 
 
 
75
  return result
76
 
77
  except Exception as e:
78
  error_message = str(e)
79
  print(f"Помилка при аналізі: {error_message}")
80
+ return {"response": f"Помилка при аналізі: {error_message}", "thinking": None}
81
+
82
+ def get_available_models(self):
83
+ """Повертає список доступних моделей для поточного провайдера"""
84
+ return self.provider.available_models
85
+
86
+ def get_provider_name(self):
87
+ """Повертає назву поточного провайдера"""
88
+ return self.provider.name
89
+
90
+ def supports_thinking(self):
91
+ """Повертає True, якщо поточний провайдер підтримує режим роздумів"""
92
+ return self.provider.supports_thinking
93
+
94
+ @staticmethod
95
+ def get_available_providers():
96
+ """Повертає список доступних провайдерів"""
97
+ return get_available_providers()
98
+
99
+ def change_provider(self, provider_name, model=None):
100
+ """Змінює поточного провайдера"""
101
+ try:
102
+ self.provider = get_provider(provider_name, model=model)
103
+ print(f"Провайдера змінено на: {self.provider.name}, модель: {model or 'за замовчуванням'}")
104
+ return True
105
+ except Exception as e:
106
+ print(f"Помилка при зміні провайдера: {str(e)}")
107
+ return False
108
+
109
+ def change_model(self, model):
110
+ """Змінює модель для поточного провайдера"""
111
+ try:
112
+ current_provider_name = self.provider.name
113
+ self.provider = get_provider(current_provider_name, model=model)
114
+ print(f"Модель змінено на: {model}")
115
+ return True
116
+ except Exception as e:
117
+ print(f"Помилка при зміні моделі: {str(e)}")
118
+ return False
src/interface.py CHANGED
@@ -1,5 +1,6 @@
1
  import gradio as gr
2
  import time
 
3
  from .analyzer import NPAAnalyzer
4
 
5
  # Кольори сайту НАЗК
@@ -13,62 +14,112 @@ NAZK_DARK_TEXT = "#333333"
13
 
14
  def create_interface():
15
  try:
16
- analyzer = NPAAnalyzer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  # Функція для форматування результату в Markdown
19
- def analyze_and_format(npa_text, enable_thinking, thinking_budget, show_thinking=False, progress=gr.Progress()):
20
  if not npa_text.strip():
21
  return "### Помилка\nПоле для тексту не може бути порожнім. Введіть текст для аналізу.", "", "Перевірте введені дані"
22
 
 
 
 
 
 
 
 
 
23
  # Перевірка та конвертація бюджету токенів
24
  try:
25
  thinking_budget = int(thinking_budget)
26
  if thinking_budget < 1024:
27
- thinking_budget = 1024 # Мінімальний бюджет за документацією
28
  elif thinking_budget > 8000:
29
- thinking_budget = 8000 # Рекомендований максимум
30
  except (ValueError, TypeError):
31
  thinking_budget = 2000 # Значення за замовчуванням
32
 
33
- start_time = time.time() # Початок відліку часу
34
 
35
- # Повідомлення про початок аналізу
36
  progress(0, desc="Ініціалізація аналізу...")
37
- time.sleep(0.5) # Невелика затримка для відображення прогресу
38
 
39
  progress(0.1, desc="Підготовка тексту НПА...")
40
  time.sleep(0.5)
41
 
42
- progress(0.2, desc="Відправка запиту до моделі...")
43
 
44
- # Тепер analyze_npa приймає параме��ри enable_thinking та thinking_budget
 
45
  result = analyzer.analyze_npa(
46
  npa_text,
47
- enable_thinking=enable_thinking,
48
  thinking_budget=thinking_budget
49
  )
50
 
51
  progress(0.9, desc="Форматування результатів...")
52
 
53
- end_time = time.time() # Кінець відліку часу
54
  elapsed_time = end_time - start_time
55
 
56
  # Форматування основної відповіді
57
  formatted_response = f"## Результат аналізу:\n{result['response']}\n\n_Час аналізу: {elapsed_time:.2f} секунд._"
58
 
59
- # Форматування роздумів моделі (якщо є)
60
  thinking_output = ""
61
- if show_thinking and result['thinking']:
62
- thinking_output = f"## Хід роздумів моделі\n```\n{result['thinking']}\n```"
63
- elif show_thinking and not result['thinking']:
64
- thinking_output = "## Роздуми моделі недоступні\nРоздуми моделі відсутні або режим думок вимкнено."
65
-
66
- # Додаємо інформацію про налаштування
67
- settings_info = f"\n\n_Налаштування: режим роздумів {'увімкнено' if enable_thinking else 'вимкнено'}"
68
- if enable_thinking:
69
- settings_info += f", бюджет токенів: {thinking_budget}_"
70
  else:
71
- settings_info += "_"
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  formatted_response += settings_info
74
 
@@ -141,7 +192,7 @@ def create_interface():
141
  """
142
 
143
  with gr.Blocks(css=custom_css) as iface:
144
- # Хедер із логотипом (можна замінити на справжній логотип)
145
  gr.HTML(f"""
146
  <div class="header">
147
  <h2 style="margin: 0; color: white; display: flex; justify-content: center; align-items: center;">
@@ -156,10 +207,11 @@ def create_interface():
156
 
157
  ### Інструкція:
158
  1. Введіть текст проекту нормативно-правового акту в поле нижче.
159
- 2. За бажанням, налаштуйте параметри режиму "роздумів" моделі.
160
- 3. Натисніть кнопку "Аналіз" для запуску аналізу.
161
- 4. Ознайомтесь із висновком.
162
- 5. Перегляньте процес "роздумів" моделі на відповідній вкладці.
 
163
  """)
164
 
165
  with gr.Row():
@@ -169,6 +221,23 @@ def create_interface():
169
  placeholder="Вставте текст проекту нормативно-правового акту для аналізу...",
170
  )
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  with gr.Row():
173
  with gr.Column(scale=2):
174
  analyze_button = gr.Button("Аналіз", variant="primary")
@@ -176,7 +245,8 @@ def create_interface():
176
  enable_thinking_checkbox = gr.Checkbox(
177
  label="Увімкнути режим роздумів",
178
  value=False,
179
- info="Дозволяє моделі виконувати покрокові роздуми перед відповіддю"
 
180
  )
181
  with gr.Column(scale=1):
182
  thinking_budget_slider = gr.Slider(
@@ -186,13 +256,7 @@ def create_interface():
186
  step=500,
187
  label="Бюджет токенів для роздумів",
188
  info="Рекомендований діапазон: 1024-8000",
189
- interactive=False # Вимкнено, оскільки режим роздумів вимкнено за замовчуванням
190
- )
191
- with gr.Column(scale=1):
192
- show_thinking_checkbox = gr.Checkbox(
193
- label="Показати роздуми моделі",
194
- value=False,
195
- info="Відображає процес мислення AI при аналізі"
196
  )
197
 
198
  with gr.Row():
@@ -205,14 +269,28 @@ def create_interface():
205
  with gr.TabItem("Роздуми моделі", id="thinking_tab"):
206
  thinking_box = gr.Markdown(label="Хід роздумів моделі")
207
 
208
- # Логіка кнопки
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  analyze_button.click(
210
  analyze_and_format,
211
- inputs=[input_box, enable_thinking_checkbox, thinking_budget_slider, show_thinking_checkbox],
212
  outputs=[output_box, thinking_box, status_box]
213
  )
214
 
215
- # Оновлення інтерфейсу при зміні стану чекбоксу "Увімкнути режим роздумів"
216
  enable_thinking_checkbox.change(
217
  lambda x: gr.update(interactive=x),
218
  inputs=[enable_thinking_checkbox],
@@ -230,6 +308,26 @@ def create_interface():
230
  - Зменшіть бюджет токенів режиму роздумів
231
  - Скоротіть текст документа або розділіть його на частини
232
  - Ви��кніть режим роздумів для пришвидшення аналізу
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  """)
234
 
235
  # Футер
 
1
  import gradio as gr
2
  import time
3
+ import os
4
  from .analyzer import NPAAnalyzer
5
 
6
  # Кольори сайту НАЗК
 
14
 
15
  def create_interface():
16
  try:
17
+ # Ініціалізація аналізатора з OpenRouter за замовчуванням
18
+ analyzer = NPAAnalyzer(provider_name="openrouter")
19
+
20
+ # Отримання доступних провайдерів та моделей
21
+ available_providers = analyzer.get_available_providers()
22
+ available_models = analyzer.get_available_models()
23
+
24
+ # Перевірка наявності API ключів
25
+ has_anthropic_key = bool(os.getenv('ANTHROPIC_API_KEY'))
26
+ has_openrouter_key = bool(os.getenv('OPENROUTER_API_KEY'))
27
+
28
+ if not has_anthropic_key and not has_openrouter_key:
29
+ raise ValueError("Жоден з API ключів (ANTHROPIC_API_KEY, OPENROUTER_API_KEY) не встановлено")
30
+
31
+ if not has_openrouter_key and analyzer.get_provider_name() == "OpenRouter":
32
+ # Використовуємо Anthropic, якщо OpenRouter не доступний
33
+ if has_anthropic_key:
34
+ analyzer.change_provider("anthropic")
35
+ print("Ключ OpenRouter відсутній, використовується Anthropic")
36
+ else:
37
+ raise ValueError("Відсутній ключ OpenRouter, а Anthropic недоступний")
38
+
39
+ # Функція для оновлення доступних моделей при зміні провайдера
40
+ def update_models(provider_name):
41
+ analyzer.change_provider(provider_name)
42
+ new_models = analyzer.get_available_models()
43
+ supports_thinking = analyzer.supports_thinking()
44
+
45
+ # Оновлення елементів інтерфейсу
46
+ return gr.Dropdown(choices=new_models, value=new_models[0]), gr.update(interactive=supports_thinking), gr.update(interactive=supports_thinking)
47
+
48
+ # Функція для оновлення моделі
49
+ def update_model(model_name):
50
+ analyzer.change_model(model_name)
51
+ return f"Модель змінено на: {model_name}"
52
 
53
  # Функція для форматування результату в Markdown
54
+ def analyze_and_format(npa_text, selected_provider, selected_model, enable_thinking, thinking_budget, progress=gr.Progress()):
55
  if not npa_text.strip():
56
  return "### Помилка\nПоле для тексту не може бути порожнім. Введіть текст для аналізу.", "", "Перевірте введені дані"
57
 
58
+ # Переконуємось, що провайдер та модель встановлені правильно
59
+ if analyzer.get_provider_name() != selected_provider:
60
+ analyzer.change_provider(selected_provider)
61
+
62
+ current_models = analyzer.get_available_models()
63
+ if selected_model in current_models:
64
+ analyzer.change_model(selected_model)
65
+
66
  # Перевірка та конвертація бюджету токенів
67
  try:
68
  thinking_budget = int(thinking_budget)
69
  if thinking_budget < 1024:
70
+ thinking_budget = 1024 # Мінімальний бюджет
71
  elif thinking_budget > 8000:
72
+ thinking_budget = 8000 # Рекомендований максимум
73
  except (ValueError, TypeError):
74
  thinking_budget = 2000 # Значення за замовчуванням
75
 
76
+ start_time = time.time()
77
 
78
+ # Індикатори прогресу
79
  progress(0, desc="Ініціалізація аналізу...")
80
+ time.sleep(0.5)
81
 
82
  progress(0.1, desc="Підготовка тексту НПА...")
83
  time.sleep(0.5)
84
 
85
+ progress(0.2, desc=f"Відправка запиту до {selected_provider}...")
86
 
87
+ # Аналіз з поточними налаштуваннями
88
+ supports_thinking = analyzer.supports_thinking()
89
  result = analyzer.analyze_npa(
90
  npa_text,
91
+ enable_thinking=(enable_thinking and supports_thinking),
92
  thinking_budget=thinking_budget
93
  )
94
 
95
  progress(0.9, desc="Форматування результатів...")
96
 
97
+ end_time = time.time()
98
  elapsed_time = end_time - start_time
99
 
100
  # Форматування основної відповіді
101
  formatted_response = f"## Результат аналізу:\n{result['response']}\n\n_Час аналізу: {elapsed_time:.2f} секунд._"
102
 
103
+ # Форматування роздумів моделі - показуємо автоматично якщо режим роздумів увімкнено
104
  thinking_output = ""
105
+ if enable_thinking and supports_thinking:
106
+ if result['thinking']:
107
+ thinking_output = f"## Хід роздумів моделі\n```\n{result['thinking']}\n```"
108
+ else:
109
+ thinking_output = "## Роздуми моделі недоступні\nРоздуми моделі відсутні або виникла помилка при їх отриманні."
 
 
 
 
110
  else:
111
+ thinking_output = f"## Роздуми моделі недоступні\n{'Режим роздумів вимкнено.' if supports_thinking else f'Провайдер {selected_provider} не підтримує режим роздумів.'}"
112
+
113
+ # Додавання інформації про налаштування
114
+ settings_info = f"\n\n_Провайдер: {selected_provider}, Модель: {selected_model}, "
115
+ if supports_thinking:
116
+ settings_info += f"режим роздумів {'увімкнено' if enable_thinking else 'вимкнено'}"
117
+ if enable_thinking:
118
+ settings_info += f", бюджет токенів: {thinking_budget}_"
119
+ else:
120
+ settings_info += "_"
121
+ else:
122
+ settings_info += "режим роздумів не підтримується_"
123
 
124
  formatted_response += settings_info
125
 
 
192
  """
193
 
194
  with gr.Blocks(css=custom_css) as iface:
195
+ # Хедер із логотипом
196
  gr.HTML(f"""
197
  <div class="header">
198
  <h2 style="margin: 0; color: white; display: flex; justify-content: center; align-items: center;">
 
207
 
208
  ### Інструкція:
209
  1. Введіть текст проекту нормативно-правового акту в поле нижче.
210
+ 2. Виберіть провайдера та модель для аналізу.
211
+ 3. За бажанням, увімкніть режим "роздумів" моделі (якщо підтримується).
212
+ 4. Натисніть кнопку "Аналіз" для запуску аналізу.
213
+ 5. Ознайомтесь із висновком.
214
+ 6. Перегляньте процес "роздумів" моделі на відповідній вкладці (якщо увімкнено режим роздумів).
215
  """)
216
 
217
  with gr.Row():
 
221
  placeholder="Вставте текст проекту нормативно-правового акту для аналізу...",
222
  )
223
 
224
+ with gr.Row():
225
+ with gr.Column(scale=1):
226
+ provider_dropdown = gr.Dropdown(
227
+ choices=available_providers,
228
+ value=analyzer.get_provider_name(),
229
+ label="Провайдер",
230
+ info="Виберіть провайдера для аналізу"
231
+ )
232
+
233
+ with gr.Column(scale=1):
234
+ model_dropdown = gr.Dropdown(
235
+ choices=available_models,
236
+ value=available_models[0] if available_models else None,
237
+ label="Модель",
238
+ info="Виберіть модель для аналізу"
239
+ )
240
+
241
  with gr.Row():
242
  with gr.Column(scale=2):
243
  analyze_button = gr.Button("Аналіз", variant="primary")
 
245
  enable_thinking_checkbox = gr.Checkbox(
246
  label="Увімкнути режим роздумів",
247
  value=False,
248
+ info="Дозволяє моделі виконувати покрокові роздуми перед відповіддю та показує їх у відповідній вкладці",
249
+ interactive=analyzer.supports_thinking()
250
  )
251
  with gr.Column(scale=1):
252
  thinking_budget_slider = gr.Slider(
 
256
  step=500,
257
  label="Бюджет токенів для роздумів",
258
  info="Рекомендований діапазон: 1024-8000",
259
+ interactive=False # Буде активовано при увімкненні режиму роздумів
 
 
 
 
 
 
260
  )
261
 
262
  with gr.Row():
 
269
  with gr.TabItem("Роздуми моделі", id="thinking_tab"):
270
  thinking_box = gr.Markdown(label="Хід роздумів моделі")
271
 
272
+ # Оновлення моделей при зміні провайдера
273
+ provider_dropdown.change(
274
+ update_models,
275
+ inputs=[provider_dropdown],
276
+ outputs=[model_dropdown, enable_thinking_checkbox, thinking_budget_slider]
277
+ )
278
+
279
+ # Оновлення моделі
280
+ model_dropdown.change(
281
+ update_model,
282
+ inputs=[model_dropdown],
283
+ outputs=[status_box]
284
+ )
285
+
286
+ # Логіка кнопки аналізу
287
  analyze_button.click(
288
  analyze_and_format,
289
+ inputs=[input_box, provider_dropdown, model_dropdown, enable_thinking_checkbox, thinking_budget_slider],
290
  outputs=[output_box, thinking_box, status_box]
291
  )
292
 
293
+ # Логіка чекбоксу "Увімкнути режим роздумів"
294
  enable_thinking_checkbox.change(
295
  lambda x: gr.update(interactive=x),
296
  inputs=[enable_thinking_checkbox],
 
308
  - Зменшіть бюджет токенів режиму роздумів
309
  - Скоротіть текст документа або розділіть його на частини
310
  - Ви��кніть режим роздумів для пришвидшення аналізу
311
+ - Спробуйте інший провайдер або модель
312
+ """)
313
+
314
+ # Інформація про API ключі
315
+ with gr.Accordion("Налаштування API ключів", open=False):
316
+ gr.Markdown(f"""
317
+ ### Статус API ключів
318
+
319
+ - OpenRouter API Key: {'✅ Встановлено' if has_openrouter_key else '❌ Відсутній'}
320
+ - Anthropic API Key: {'✅ Встановлено' if has_anthropic_key else '❌ Відсутній'}
321
+
322
+ Для роботи з різними провайдерами необхідно встановити відповідні API ключі через змінні середовища:
323
+
324
+ ```bash
325
+ # Для OpenRouter
326
+ export OPENROUTER_API_KEY=your-api-key
327
+
328
+ # Для Anthropic
329
+ export ANTHROPIC_API_KEY=your-api-key
330
+ ```
331
  """)
332
 
333
  # Футер
src/llm_providers.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import anthropic
3
+ from abc import ABC, abstractmethod
4
+ from openai import OpenAI
5
+ import requests
6
+ import traceback
7
+ from .prompts import SYSTEM_PROMPT
8
+
9
+ class LLMProvider(ABC):
10
+ """Абстрактний базовий клас для LLM провайдерів"""
11
+
12
+ @abstractmethod
13
+ def analyze(self, prompt: str, enable_thinking: bool = False, thinking_budget: int = 2000) -> dict:
14
+ """
15
+ Аналізує текст використовуючи LLM провайдера
16
+
17
+ Args:
18
+ prompt (str): Текст промпту для аналізу
19
+ enable_thinking (bool): Чи включати режим "думок" якщо підтримується
20
+ thinking_budget (int): Бюджет токенів для режиму "думок" якщо підтримується
21
+
22
+ Returns:
23
+ dict: Словник з ключами 'response' та 'thinking'
24
+ """
25
+ pass
26
+
27
+ @property
28
+ @abstractmethod
29
+ def name(self) -> str:
30
+ """Повертає назву провайдера"""
31
+ pass
32
+
33
+ @property
34
+ @abstractmethod
35
+ def supports_thinking(self) -> bool:
36
+ """Повертає чи підтримує провайдер режим думок"""
37
+ pass
38
+
39
+ @property
40
+ @abstractmethod
41
+ def available_models(self) -> list:
42
+ """Повертає список доступних моделей для цього провайдера"""
43
+ pass
44
+
45
+ @abstractmethod
46
+ def test_connection(self) -> bool:
47
+ """Перевіряє з'єднання з провайдером"""
48
+ pass
49
+
50
+
51
+ class AnthropicProvider(LLMProvider):
52
+ """Реалізація провайдера Anthropic"""
53
+
54
+ def __init__(self, api_key=None, model="claude-3-7-sonnet-latest"):
55
+ """Ініціалізація провайдера Anthropic"""
56
+ self._api_key = api_key or os.getenv('ANTHROPIC_API_KEY')
57
+ if not self._api_key:
58
+ raise ValueError("ANTHROPIC_API_KEY не знайдено в змінних середовища")
59
+
60
+ self.client = anthropic.Anthropic(api_key=self._api_key)
61
+ self._model = model
62
+
63
+ @property
64
+ def name(self) -> str:
65
+ return "Anthropic"
66
+
67
+ @property
68
+ def supports_thinking(self) -> bool:
69
+ return True
70
+
71
+ @property
72
+ def available_models(self) -> list:
73
+ return [
74
+ "claude-3-7-sonnet-latest",
75
+ "claude-3-5-haiku-latest"
76
+ ]
77
+
78
+ def test_connection(self) -> bool:
79
+ """Перевіряє з'єднання з Anthropic"""
80
+ try:
81
+ # Виконуємо простий тестовий запит
82
+ response = self.client.messages.create(
83
+ model=self._model,
84
+ max_tokens=200,
85
+ messages=[
86
+ {
87
+ "role": "user",
88
+ "content": "Привіт"
89
+ }
90
+ ]
91
+ )
92
+ return True
93
+ except Exception as e:
94
+ print(f"Помилка з'єднання з Anthropic: {str(e)}")
95
+ return False
96
+
97
+ def analyze(self, prompt: str, enable_thinking: bool = False, thinking_budget: int = 2000) -> dict:
98
+ """Аналізує текст використовуючи Anthropic Claude"""
99
+ if not prompt:
100
+ return {"response": "Будь ласка, введіть текст для аналізу.", "thinking": None}
101
+
102
+ try:
103
+ # Підготовка повідомлення користувача
104
+ user_message = {
105
+ "role": "user",
106
+ "content": [
107
+ {
108
+ "type": "text",
109
+ "text": prompt
110
+ }
111
+ ]
112
+ }
113
+
114
+ # Спільні параметри запиту
115
+ params = {
116
+ "model": self._model,
117
+ "max_tokens": 16000,
118
+ "system": SYSTEM_PROMPT,
119
+ "messages": [user_message]
120
+ }
121
+
122
+ # Додавання параметра thinking, якщо дозволено
123
+ if enable_thinking:
124
+ params["thinking"] = {
125
+ "type": "enabled",
126
+ "budget_tokens": thinking_budget
127
+ }
128
+
129
+ result = {
130
+ "response": "",
131
+ "thinking": ""
132
+ }
133
+
134
+ # Виконання запиту з потоковою передачею
135
+ with self.client.messages.stream(**params) as stream:
136
+ for event in stream:
137
+ if event.type == "content_block_delta":
138
+ if event.delta.type == "text_delta":
139
+ result["response"] += event.delta.text
140
+ elif event.delta.type == "thinking_delta":
141
+ result["thinking"] += event.delta.thinking
142
+
143
+ # Обробка порожньої відповіді
144
+ if not result["response"] and result["thinking"]:
145
+ result["response"] = "Не вдалося отримати текстову відповідь, але доступні роздуми моделі."
146
+ elif not result["response"] and not result["thinking"]:
147
+ result["response"] = "Не вдалося отримати відповідь від моделі."
148
+
149
+ return result
150
+
151
+ except Exception as e:
152
+ error_message = str(e)
153
+ print(f"Помилка при аналізі (Anthropic): {error_message}")
154
+ print(f"Деталі помилки: {traceback.format_exc()}")
155
+
156
+ # Якщо помилка пов'язана зі streaming, спробуємо без нього
157
+ if "streaming" in error_message.lower() or "stream" in error_message.lower():
158
+ try:
159
+ # Виконання запиту без streaming
160
+ response = self.client.messages.create(**params)
161
+
162
+ result = {
163
+ "response": None,
164
+ "thinking": None
165
+ }
166
+
167
+ # Обробка відповіді
168
+ if hasattr(response, 'content') and isinstance(response.content, list):
169
+ for content_block in response.content:
170
+ if hasattr(content_block, 'type') and content_block.type == 'text':
171
+ if hasattr(content_block, 'text'):
172
+ result["response"] = content_block.text
173
+ elif hasattr(content_block, 'type') and content_block.type == 'thinking':
174
+ if hasattr(content_block, 'thinking'):
175
+ if result["thinking"] is not None:
176
+ result["thinking"] += "\n\n" + content_block.thinking
177
+ else:
178
+ result["thinking"] = content_block.thinking
179
+
180
+ if not result["response"]:
181
+ result["response"] = "Не вдалося отримати текстову відповідь від моделі."
182
+
183
+ return result
184
+ except Exception as e2:
185
+ print(f"Помилка при спробі без streaming: {str(e2)}")
186
+ print(f"Деталі помилки: {traceback.format_exc()}")
187
+ return {"response": f"Помилка при аналізі: {str(e2)}", "thinking": None}
188
+
189
+ return {"response": f"Помилка при аналізі: {error_message}", "thinking": None}
190
+
191
+
192
+ class OpenRouterProvider(LLMProvider):
193
+ """Реалізація провайдера OpenRouter"""
194
+
195
+ def __init__(self, api_key=None, model="openrouter/quasar-alpha"):
196
+ """Ініціалізація провайдера OpenRouter"""
197
+ self._api_key = api_key or os.getenv('OPENROUTER_API_KEY')
198
+ if not self._api_key:
199
+ raise ValueError("OPENROUTER_API_KEY не знайдено в змінних середовища")
200
+
201
+ self.client = OpenAI(
202
+ base_url="https://openrouter.ai/api/v1",
203
+ api_key=self._api_key
204
+ )
205
+ self._model = model
206
+
207
+ @property
208
+ def name(self) -> str:
209
+ return "OpenRouter"
210
+
211
+ @property
212
+ def supports_thinking(self) -> bool:
213
+ return False
214
+
215
+ @property
216
+ def available_models(self) -> list:
217
+ return [
218
+ "openrouter/quasar-alpha",
219
+ # "google/gemini-2.5-pro-preview-03-25",
220
+ # "deepseek/deepseek-v3-base:free",
221
+ # "mistralai/mistral-large",
222
+ # "meta-llama/llama-3-70b-instruct"
223
+ ]
224
+
225
+ def test_connection(self) -> bool:
226
+ """Перевіряє з'єднання з провайдером"""
227
+ try:
228
+ # Виконуємо простий тестовий запит
229
+ completion = self.client.chat.completions.create(
230
+ extra_headers={
231
+ "HTTP-Referer": "https://npa-analyzer.com",
232
+ "X-Title": "NPA Analysis Assistant",
233
+ },
234
+ model=self._model,
235
+ messages=[
236
+ {
237
+ "role": "user",
238
+ "content": "Привіт"
239
+ }
240
+ ],
241
+ max_tokens=200
242
+ )
243
+ return True
244
+ except Exception as e:
245
+ print(f"Помилка з'єднання з OpenRouter: {str(e)}")
246
+ return False
247
+
248
+ def analyze(self, prompt: str, enable_thinking: bool = False, thinking_budget: int = 2000) -> dict:
249
+ """Аналізує текст використовуючи OpenRouter"""
250
+ if not prompt:
251
+ return {"response": "Будь ласка, введіть текст для аналізу.", "thinking": None}
252
+
253
+ try:
254
+ # Create completion
255
+ completion = self.client.chat.completions.create(
256
+ extra_headers={
257
+ "HTTP-Referer": "https://npa-analyzer.com",
258
+ "X-Title": "NPA Analysis Assistant",
259
+ },
260
+ extra_body={}, # Додаємо підтримку extra_body
261
+ model=self._model,
262
+ messages=[
263
+ {
264
+ "role": "system",
265
+ "content": SYSTEM_PROMPT
266
+ },
267
+ {
268
+ "role": "user",
269
+ "content": prompt
270
+ }
271
+ ],
272
+ temperature=0.5, # Додаємо контроль над температурою для кращих результатів
273
+ max_tokens=16000, # Збільшуємо ліміт токенів для аналізу довгих НПА
274
+ timeout=120 # Збільшуємо таймаут для довгих запитів
275
+ )
276
+
277
+ # Extract response content with improved error handling
278
+ if completion and hasattr(completion, 'choices') and completion.choices:
279
+ if hasattr(completion.choices[0], 'message') and hasattr(completion.choices[0].message, 'content'):
280
+ response_content = completion.choices[0].message.content
281
+ else:
282
+ response_content = "Отримано відповідь від API, але структура відповіді нестандартна"
283
+ print(f"Нестандартна структура відповіді: {str(completion)}")
284
+ else:
285
+ response_content = "Не вдалося отримати відповідь від OpenRouter API"
286
+ print(f"Структура відповіді: {str(completion)}")
287
+
288
+ return {
289
+ "response": response_content,
290
+ "thinking": None # OpenRouter не підтримує режим роздумів
291
+ }
292
+
293
+ except Exception as e:
294
+ error_message = str(e)
295
+ print(f"Помилка при аналізі (OpenRouter): {error_message}")
296
+ print(f"Деталі помилки: {traceback.format_exc()}")
297
+
298
+ return {"response": f"Помилка при аналізі: {error_message}", "thinking": None}
299
+
300
+
301
+ def get_provider(provider_name, api_key=None, model=None):
302
+ """Функція-фабрика для отримання екземпляра провайдера"""
303
+ providers = {
304
+ "anthropic": lambda: AnthropicProvider(api_key=api_key, model=model) if model else AnthropicProvider(api_key=api_key),
305
+ "openrouter": lambda: OpenRouterProvider(api_key=api_key, model=model) if model else OpenRouterProvider(api_key=api_key)
306
+ }
307
+
308
+ if provider_name.lower() not in providers:
309
+ raise ValueError(f"Невідомий провайдер: {provider_name}")
310
+
311
+ return providers[provider_name.lower()]()
312
+
313
+
314
+ def get_available_providers():
315
+ """Повертає список доступних провайдерів"""
316
+ return ["OpenRouter", "Anthropic"]
317
+
318
+
319
+ def get_available_openrouter_models(api_key=None):
320
+ """
321
+ Отримує список доступних моделей з API OpenRouter
322
+ Примітка: цю функцію можна використовувати для динамічного оновлення списку моделей
323
+ """
324
+ try:
325
+ api_key = api_key or os.getenv('OPENROUTER_API_KEY')
326
+ if not api_key:
327
+ return []
328
+
329
+ # Запит на отримання доступних моделей
330
+ response = requests.get(
331
+ "https://openrouter.ai/api/v1/models",
332
+ headers={"Authorization": f"Bearer {api_key}"}
333
+ )
334
+
335
+ if response.status_code == 200:
336
+ models_data = response.json()
337
+ if 'data' in models_data and isinstance(models_data['data'], list):
338
+ return [model["id"] for model in models_data["data"]]
339
+ return []
340
+ except Exception as e:
341
+ print(f"Помилка отримання списку моделей: {str(e)}")
342
+ return []
343
+
344
+
345
+ def test_model(provider_name, model_name):
346
+ """Тестує доступність конкретної моделі"""
347
+ try:
348
+ provider = get_provider(provider_name)
349
+ original_model = provider._model
350
+ provider._model = model_name
351
+
352
+ # Перевіряємо доступність моделі простим запитом
353
+ result = provider.analyze("Це тестовий запит для перевірки моделі.")
354
+
355
+ print(f"Модель {model_name} відповіла: {result['response'][:100]}...")
356
+ return True
357
+ except Exception as e:
358
+ print(f"Помилка при тестуванні моделі {model_name}: {str(e)}")
359
+ return False
360
+ finally:
361
+ if 'provider' in locals() and 'original_model' in locals():
362
+ provider._model = original_model
test_models.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # test_models.py
2
+ import os
3
+ import sys
4
+ from src.llm_providers import test_model, get_available_openrouter_models
5
+
6
+ # Вивести всі доступні моделі з OpenRouter (опціонально)
7
+ print("Спроба отримати список моделей з OpenRouter API...")
8
+ models = get_available_openrouter_models()
9
+ if models:
10
+ print("Доступні моделі OpenRouter:")
11
+ for idx, model in enumerate(models, 1):
12
+ print(f"{idx}. {model}")
13
+ else:
14
+ print("Не вдалося отримати список моделей або список порожній")
15
+
16
+ # Тестування конкретних моделей
17
+ models_to_test = [
18
+ ("openrouter", "google/gemini-2.5-pro-preview-03-25")
19
+ # ("openrouter", "mistralai/mistral-large")
20
+ # ("openrouter", "openrouter/quasar-alpha"),
21
+ # ("openrouter", "deepseek/deepseek-v3-base:free"),
22
+ # ("anthropic", "claude-3-7-sonnet-20250219")
23
+ ]
24
+
25
+ print("\nПочаток тестування моделей:")
26
+ print("="*50)
27
+
28
+ for provider, model in models_to_test:
29
+ print(f"\nТестування {provider}/{model}...")
30
+ success = test_model(provider, model)
31
+ status = "✅ УСПІШНО" if success else "❌ ПОМИЛКА"
32
+ print(f"{status} - {provider}/{model}")
33
+
34
+ print("="*50)
35
+ print("Тестування завершено!")