feat: папка dictionaries/prompts — словари промтов для каждой ИИ-кнопки

- prompts/suggest_object_params.json: жёсткие правила подбора характеристик —
  выбирать ТОЛЬКО из предоставленных списков (сферы/типы/архитектуры/
  типовые объекты), ничего не выдумывать; назначение строго по форме
  «Объект предназначен для … с целью …»; типовой объект — дословно из 360-р
- prompts/suggest_threats.json, check_security.json, check_company.json,
  declension.json — промты для остальных ИИ-кнопок
- verify.py: _load_prompt() + подключение промтов в suggest_object_params_ai,
  suggest_threats_ai, check_security_ai; ИИ получает полные списки
  (spheres/object_types/architectures) и выбирает из них
- ui.py: окно рекомендаций ИИ — галочки у каждого поля, применяются
  только отмеченные (ИИ может ошибаться в архитектуре — оператор решает)
This commit is contained in:
2026-08-04 23:32:58 +04:00
parent acb7aeaa64
commit e4e7bb444a
7 changed files with 149 additions and 57 deletions
@@ -0,0 +1,8 @@
{
"system": "Ты — юрист по защите персональных данных и критической информационной инфраструктуры (187-ФЗ). Проверь реквизиты организации.",
"rules": [
"Не выдумывай реквизиты — исправляй/дополняй только если уверен.",
"В «полях» для каждого поля укажи статус: ok (заполнено корректно), пусто (не заполнено), ошибка (заполнено с ошибкой)."
],
"response_format": "Верни ТОЛЬКО JSON: {\"оценка\": \"ok|warning|error\", \"замечания\": [\"...\"], \"предложения\": {\"full_name\": \"...\", \"short_name\": \"...\", \"address\": \"...\", \"inn\": \"...\", \"kpp\": \"...\", \"ogrn\": \"...\", \"ogrn_date\": \"...\", \"chief_position\": \"...\", \"chief_fio\": \"...\"}, \"поля\": {\"full_name\": {\"статус\": \"ok|пусто|ошибка\", \"комментарий\": \"...\"}, ...}}"
}
@@ -0,0 +1,4 @@
{
"system": "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК России № 239 и № 236). Проверь заполненные сведения об объекте КИИ и дай рекомендации.",
"response_format": "Верни ТОЛЬКО JSON: {\"оценка\": \"ok|warning|error\", \"замечания\": [\"...\"], \"предложения\": {\"attacker_category\": \"...\", \"threats\": \"...\", \"incidents\": \"...\", \"criteria_values\": \"...\", \"criteria_justification\": \"...\", \"org_measures\": \"...\", \"tech_measures\": \"...\"}}. В «предложениях» укажи, что добавить/исправить по каждому разделу, если нужно, иначе пустые строки."
}
@@ -0,0 +1,9 @@
{
"system": "Ты — лингвист, специалист по склонению русских наименований организаций (187-ФЗ). Просклоняй название организации по падежам.",
"rules": [
"Склоняй только само название (включая организационно-правовую форму, если она склоняется по правилам русского языка).",
"Сохраняй кавычки, регистр и пунктуацию оригинала.",
"Не выдумывай падежи — используй стандартную парадигму: именительный, родительный, дательный, винительный, творительный, предложный."
],
"response_format": "Верни ТОЛЬКО JSON: {\"nominative\": \"...\", \"genitive\": \"...\", \"dative\": \"...\", \"accusative\": \"...\", \"instrumental\": \"...\", \"prepositional\": \"...\"}"
}
@@ -0,0 +1,13 @@
{
"system": "Ты — эксперт ФСТЭК по категорированию объектов критической информационной инфраструктуры (187-ФЗ, ПП РФ № 127, Распоряжение ПРФ № 360-р, приказ ФСТЭК № 236). Твоя задача — по описанию объекта КИИ ПОДОБРАТЬ его характеристики ТОЛЬКО из предоставленных списков, ничего не выдумывая.",
"rules": [
"Выбирай значения ТОЛЬКО из предоставленных списков. Если подходящего значения в списке нет — верни пустую строку, не придумывай своё.",
"Поле «типовой_объект»: выбери полную строку ТОЛЬКО из предоставленного перечня типовых отраслевых объектов (Распоряжение ПРФ № 360-р), наиболее подходящую. Не перефразируй, не сокращай — копируй строку дословно. Если ничего не подходит — пустая строка.",
"Поле «сфера»: выбери ТОЛЬКО одну из предоставленных сфер. Не придумывай свои.",
"Поле «тип»: выбери ТОЛЬКО один из предоставленных типов объектов.",
"Поле «архитектура»: выбери ТОЛЬКО одну из предоставленных архитектур.",
"Поле «назначение»: заполни строго по форме: «Объект предназначен для <что делает> с целью <зачем>.». Не отклоняйся от этой формы.",
"Поле «обоснование»: 1-2 предложения, почему выбраны такие характеристики."
],
"response_format": "Верни ТОЛЬКО JSON без пояснений: {\"назначение\": \"...\", \"сфера\": \"...\", \"тип\": \"...\", \"архитектура\": \"...\", \"типовой_объект\": \"...\", \"обоснование\": \"...\"}"
}
@@ -0,0 +1,9 @@
{
"system": "Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры (187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации.",
"rules": [
"Выбирай угрозы ТОЛЬКО из предоставленного перечня — не выдумывай и не перефразируй, используй полные строки (код + название) как они есть.",
"Верни от 10 до 40 угроз.",
"Учитывай характеристики объекта: тип (ИС/АСУ/ИТКС), наличие интернета, сферу, категорию нарушителя."
],
"response_format": "Верни ТОЛЬКО JSON: {\"threats\": [\"УБИ.xxx Название угрозы\", ...]} — без пояснений и комментариев."
}
+31 -16
View File
@@ -1671,6 +1671,10 @@ class DokoGenApp:
'sphere': isys.sphere or '', 'sphere': isys.sphere or '',
'object_type': isys.object_type or '', 'object_type': isys.object_type or '',
} }
# Полные списки допустимых значений — ИИ выбирает ТОЛЬКО из них
spheres = list(self.is_sphere_combo['values']) if hasattr(self, 'is_sphere_combo') else []
object_types = list(self.is_object_type_combo['values']) if hasattr(self, 'is_object_type_combo') else []
architectures = list(self.is_architecture_combo['values']) if hasattr(self, 'is_architecture_combo') else []
self.log(f"🤖 Запрос к ИИ: подбор характеристик для «{isys.name}»") self.log(f"🤖 Запрос к ИИ: подбор характеристик для «{isys.name}»")
self._verify_log("Подбор характеристик через ИИ…") self._verify_log("Подбор характеристик через ИИ…")
@@ -1679,7 +1683,8 @@ class DokoGenApp:
from .verify import suggest_object_params_ai from .verify import suggest_object_params_ai
res, err = suggest_object_params_ai( res, err = suggest_object_params_ai(
data, settings['ai_api_key'], settings.get('ai_model'), data, settings['ai_api_key'], settings.get('ai_model'),
settings.get('ai_base_url'), typical_objects=typical_list) settings.get('ai_base_url'), typical_objects=typical_list,
spheres=spheres, object_types=object_types, architectures=architectures)
self.root.after(0, lambda: self._suggest_object_params_ai_done(res, err, isys.name)) self.root.after(0, lambda: self._suggest_object_params_ai_done(res, err, isys.name))
except Exception as e: except Exception as e:
self.root.after(0, lambda: self._suggest_object_params_ai_done(None, str(e), isys.name)) self.root.after(0, lambda: self._suggest_object_params_ai_done(None, str(e), isys.name))
@@ -1697,28 +1702,35 @@ class DokoGenApp:
dlg = tk.Toplevel(self.root) dlg = tk.Toplevel(self.root)
dlg.title("🤖 Рекомендации ИИ по характеристикам объекта") dlg.title("🤖 Рекомендации ИИ по характеристикам объекта")
dlg.geometry("860x560") dlg.geometry("880x620")
dlg.transient(self.root) dlg.transient(self.root)
frm = ttk.Frame(dlg, padding=8) frm = ttk.Frame(dlg, padding=8)
frm.pack(fill=tk.BOTH, expand=True) frm.pack(fill=tk.BOTH, expand=True)
ttk.Label(frm, text=f"Объект: {obj_name} — рекомендации ИИ (подтвердите или скорректируйте)", ttk.Label(frm, text=f"Объект: {obj_name} — рекомендации ИИ. Отметьте галочками, "
font=('Arial', 10, 'bold')).pack(anchor=tk.W) f"какие поля применить (ИИ может ошибаться — проверяйте).",
font=('Arial', 10, 'bold'), wraplength=840, justify=tk.LEFT).pack(anchor=tk.W)
rows = [ rows = [
('назначение', 'Назначение объекта'), ('назначение', 'Назначение объекта (форма: «Объект предназначен для … с целью …»)'),
('сфера', 'Сфера деятельности'), ('сфера', 'Сфера деятельности'),
('тип', 'Тип объекта'), ('тип', 'Тип объекта'),
('архитектура', 'Архитектура объекта'), ('архитектура', 'Архитектура объекта'),
('типовой_объект', 'Типовой отраслевой объект КИИ'), ('типовой_объект', 'Типовой отраслевой объект КИИ'),
] ]
entries = {} entries = {}
checks = {}
for key, label in rows: for key, label in rows:
ttk.Label(frm, text=label + ":").pack(anchor=tk.W, pady=(6, 0)) row = ttk.Frame(frm)
e = tk.Text(frm, height=2 if key == 'типовой_объект' else 1, wrap=tk.WORD) row.pack(fill=tk.X, pady=(6, 0))
var = tk.BooleanVar(value=True)
cb = ttk.Checkbutton(row, text=label, variable=var)
cb.pack(anchor=tk.W)
e = tk.Text(frm, height=2 if key in ('типовой_объект', 'назначение') else 1, wrap=tk.WORD)
e.pack(fill=tk.X) e.pack(fill=tk.X)
e.insert('1.0', res.get(key, '') or '') e.insert('1.0', res.get(key, '') or '')
entries[key] = e entries[key] = e
checks[key] = var
ttk.Label(frm, text="Обоснование ИИ:").pack(anchor=tk.W, pady=(6, 0)) ttk.Label(frm, text="Обоснование ИИ:").pack(anchor=tk.W, pady=(6, 0))
just = tk.Text(frm, height=2, wrap=tk.WORD) just = tk.Text(frm, height=2, wrap=tk.WORD)
@@ -1727,20 +1739,23 @@ class DokoGenApp:
btn_row = ttk.Frame(frm) btn_row = ttk.Frame(frm)
btn_row.pack(fill=tk.X, pady=(8, 0)) btn_row.pack(fill=tk.X, pady=(8, 0))
ttk.Button(btn_row, text="✔️ Применить", ttk.Button(btn_row, text="✔️ Применить отмеченное",
command=lambda: self._apply_ai_params(entries, dlg)).pack(side=tk.LEFT, padx=2) command=lambda: self._apply_ai_params(entries, dlg, checks)).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_row, text="Отмена", command=dlg.destroy).pack(side=tk.LEFT, padx=2) ttk.Button(btn_row, text="Отмена", command=dlg.destroy).pack(side=tk.LEFT, padx=2)
def _apply_ai_params(self, entries, dlg): def _apply_ai_params(self, entries, dlg, checks=None):
"""Применить рекомендации ИИ к полям объекта.""" """Применить рекомендации ИИ к полям объекта (только отмеченные галочками)."""
def _get(e): def _get(e):
return e.get('1.0', 'end-1c').strip() return e.get('1.0', 'end-1c').strip()
purpose = _get(entries['назначение']) def _wanted(key):
sphere = _get(entries['сфера']) return checks is None or checks.get(key, tk.BooleanVar(value=True)).get()
otype = _get(entries['тип'])
arch = _get(entries['архитектура']) purpose = _get(entries['назначение']) if _wanted('назначение') else ''
typical = _get(entries['типовой_объект']) sphere = _get(entries['сфера']) if _wanted('сфера') else ''
otype = _get(entries['тип']) if _wanted('тип') else ''
arch = _get(entries['архитектура']) if _wanted('архитектура') else ''
typical = _get(entries['типовой_объект']) if _wanted('типовой_объект') else ''
if purpose and hasattr(self, 'is_purpose_entry'): if purpose and hasattr(self, 'is_purpose_entry'):
self.is_purpose_entry.delete(0, tk.END) self.is_purpose_entry.delete(0, tk.END)
+75 -41
View File
@@ -14,6 +14,18 @@
""" """
import json import json
def _load_prompt(name):
"""Загружает промт из dictionaries/prompts/<name>.json (system/rules/response_format)."""
import os as _os
p = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
'dictionaries', 'prompts', name + '.json')
try:
with open(p, encoding='utf-8') as f:
return json.load(f)
except Exception:
return {}
import re import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
import urllib.parse import urllib.parse
@@ -526,20 +538,24 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
else: else:
threat_list_text = "(перечень не предоставлен — используй знания об актуальном перечне БДУ ФСТЭК)" threat_list_text = "(перечень не предоставлен — используй знания об актуальном перечне БДУ ФСТЭК)"
system = ( pr = _load_prompt('suggest_threats')
system = pr.get('system', (
"Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры " "Ты — эксперт ФСТЭК по безопасности критической информационной инфраструктуры "
"(187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из " "(187-ФЗ, приказ ФСТЭК № 236). На основе характеристик объекта КИИ выбери из "
"предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации и верни " "предоставленного перечня АКТУАЛЬНЫЕ угрозы безопасности информации."
"ТОЛЬКО JSON: " ))
'{"threats": ["УБИ.xxx Название угрозы", ...]} — от 10 до 40 угроз, ' rules = pr.get('rules', [])
"без пояснений и комментариев. Не выдумывай угрозы, которых нет в перечне; " resp = pr.get('response_format',
"используй полные строки из перечня (код + название) как они есть." 'Верни ТОЛЬКО JSON: {"threats": ["УБИ.xxx Название угрозы", ...]} '
) '— от 10 до 40 угроз, без пояснений.')
user = ( user = (
"Характеристики объекта КИИ:\n" "Характеристики объекта КИИ:\n"
f"{json.dumps(security_data, ensure_ascii=False)}\n\n" f"{json.dumps(security_data, ensure_ascii=False)}\n\n"
"Актуальный перечень угроз БДУ ФСТЭК:\n" "Актуальный перечень угроз БДУ ФСТЭК (выбирай ТОЛЬКО из него, "
f"{threat_list_text}" "используй полные строки как они есть):\n"
f"{threat_list_text}\n\n"
+ ("Правила:\n" + "\n".join(f"{i+1}. {r}" for i, r in enumerate(rules)) + "\n\n" if rules else "")
+ resp
) )
js, err = _ai_json(api_key, model, base_url, system, user) js, err = _ai_json(api_key, model, base_url, system, user)
if err: if err:
@@ -552,48 +568,66 @@ def suggest_threats_ai(security_data, api_key, model='deepseek-chat', base_url=N
def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None, def suggest_object_params_ai(object_data, api_key, model='deepseek-chat', base_url=None,
typical_objects=None): typical_objects=None, spheres=None, object_types=None,
architectures=None):
"""ИИ предлагает характеристики объекта КИИ: назначение, сферу, тип, архитектуру, """ИИ предлагает характеристики объекта КИИ: назначение, сферу, тип, архитектуру,
типовой отраслевой объект как рекомендацию оператору. типовой отраслевой объект как рекомендацию оператору.
object_data dict: name, description, purpose (что известно об объекте). object_data dict: name, description, purpose (что известно об объекте).
typical_objects перечень типовых отраслевых объектов (из 360-р) для выбора. typical_objects перечень типовых отраслевых объектов (из 360-р) для выбора;
spheres / object_types / architectures полные списки допустимых значений.
Промт берётся из dictionaries/prompts/suggest_object_params.json.
Возвращает (dict, ошибка): dict с полями Возвращает (dict, ошибка): dict с полями
purpose / sphere / object_type / architecture / typical_object / обоснование. назначение / сфера / тип / архитектура / типовой_объект / обоснование.
""" """
if not api_key: if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
# Загрузка промта из папки dictionaries/prompts
import os as _os
prompt_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)),
'dictionaries', 'prompts', 'suggest_object_params.json')
try:
with open(prompt_path, encoding='utf-8') as _f:
prompt = json.load(_f)
except Exception:
prompt = {}
system = prompt.get('system', 'Ты — эксперт ФСТЭК по категорированию объектов КИИ.')
rules = prompt.get('rules', [])
response_format = prompt.get('response_format',
'Верни ТОЛЬКО JSON: {"назначение": "...", "сфера": "...", '
'"тип": "...", "архитектура": "...", "типовой_объект": "...", '
'"обоснование": "..."}')
# Полные списки допустимых значений
def _fmt_list(name, items):
if not items:
return f"{name}: (список не предоставлен)"
return f"{name}:\n" + "\n".join(f"- {i}" for i in items)
lists_text = "\n\n".join([
_fmt_list("Сферы деятельности (выбирай ТОЛЬКО из них)", spheres),
_fmt_list("Типы объектов (выбирай ТОЛЬКО из них)", object_types),
_fmt_list("Архитектуры (выбирай ТОЛЬКО из них)", architectures),
])
if typical_objects: if typical_objects:
to_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(typical_objects[:400])) to_text = "\n".join(f"{i+1}. {t}" for i, t in enumerate(typical_objects[:400]))
else: else:
to_text = "(перечень не предоставлен)" to_text = "(перечень не предоставлен)"
system = ( rules_text = "\n".join(f"{i+1}. {r}" for i, r in enumerate(rules))
"Ты — эксперт ФСТЭК по категорированию объектов критической информационной инфраструктуры "
"(187-ФЗ, ПП РФ № 127, Распоряжение ПРФ № 360-р, приказ ФСТЭК № 236). "
"По описанию объекта КИИ предложи его характеристики. Верни ТОЛЬКО JSON: "
'{"назначение": "короткое назначение объекта", '
'"сфера": "одна из сфер: Здравоохранение, Наука, Транспорт, Связь, Энергетика, '
'Государственная регистрация прав, Банковская сфера и финансовый рынок, '
'Топливно-энергетический комплекс, Атомная энергетика, Оборонная промышленность, '
'Ракетно-космическая промышленность, Горнодобывающая промышленность, '
'Металлургическая промышленность, Химическая промышленность, Иная сфера деятельности", '
'"тип": "Информационная система | Автоматизированные системы управления | '
'Информационно-телекоммуникационная сеть", '
'"архитектура": "одно из: клиент-серверная система, автономная (standalone), '
'распределенная система, иное", '
'"типовой_объект": "полная строка из предоставленного перечня, наиболее подходящая, '
'или пустая строка", '
'"обоснование": "почему выбраны такие характеристики (1-2 предложения)"} '
"Не выдумывай типовой объект — бери только из перечня."
)
user = ( user = (
"Информация об объекте КИИ:\n" "Информация об объекте КИИ:\n"
f"{json.dumps(object_data, ensure_ascii=False)}\n\n" f"{json.dumps(object_data, ensure_ascii=False)}\n\n"
"Перечень типовых отраслевых объектов КИИ (Распоряжение ПРФ № 360-р):\n" f"{lists_text}\n\n"
f"{to_text}" "Перечень типовых отраслевых объектов КИИ (Распоряжение ПРФ № 360-р, "
"выбирай ТОЛЬКО из него, копируй строку дословно):\n"
f"{to_text}\n\n"
"Правила:\n"
f"{rules_text}\n\n"
f"{response_format}"
) )
js, err = _ai_json(api_key, model, base_url, system, user) js, err = _ai_json(api_key, model, base_url, system, user)
if err: if err:
@@ -615,18 +649,18 @@ def check_security_ai(security_data, api_key, model='deepseek-chat', base_url=No
""" """
if not api_key: if not api_key:
return None, "Не задан API-ключ ИИ (Файл → Настройки → API)" return None, "Не задан API-ключ ИИ (Файл → Настройки → API)"
system = ( pr = _load_prompt('check_security')
system = pr.get('system', (
"Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, " "Ты — эксперт по безопасности критической информационной инфраструктуры (187-ФЗ, "
"приказ ФСТЭК России от 25.12.2017 № 239 «Об утверждении Требований по обеспечению " "приказ ФСТЭК России от 25.12.2017 № 239 и приказ № 236). Проверь заполненные "
"безопасности значимых объектов КИИ» и приказ № 236). Проверь заполненные сведения " "сведения об объекте КИИ и дай рекомендации."
"об объекте КИИ и дай рекомендации. Верни ТОЛЬКО JSON: " ))
'{"оценка": "ok|warning|error", ' system = system + " " + pr.get('response_format',
'"замечания": ["..."], ' 'Верни ТОЛЬКО JSON: {"оценка": "ok|warning|error", "замечания": ["..."], '
'"предложения": {"attacker_category": "...", "threats": "...", "incidents": "...", ' '"предложения": {"attacker_category": "...", "threats": "...", "incidents": "...", '
'"criteria_values": "...", "criteria_justification": "...", ' '"criteria_values": "...", "criteria_justification": "...", '
'"org_measures": "...", "tech_measures": "..."}} ' '"org_measures": "...", "tech_measures": "..."}} '
"В «предложениях» укажи, что добавить/исправить по каждому разделу, если нужно, иначе пустые строки." 'В «предложениях» укажи, что добавить/исправить по каждому разделу, иначе пустые строки.')
)
user = json.dumps(security_data, ensure_ascii=False) user = json.dumps(security_data, ensure_ascii=False)
js, err = _ai_json(api_key, model, base_url, system, user) js, err = _ai_json(api_key, model, base_url, system, user)
if err: if err: