This commit is contained in:
2026-02-01 22:02:49 +03:00
parent 65d7be8795
commit 2d4eff6c3f
18 changed files with 1321 additions and 419 deletions

View File

@@ -26,6 +26,27 @@
JSON: проверенные теги по категориям с обновлёнными confidence; теги, снятые при валидации, — в блоке `removed` (tag, category, reason). Формат `tags` совпадает с выходом шага 3 для передачи в эмбеддинг и сохранение в БД.
## Использование
## Скрипт через Ollama
`run_validate_tags_ollama.py` — один вызов Ollama для валидации извлечённых тегов. На выходе один JSON-файл.
**Вход (по умолчанию):**
- `../3_извлечениеегов/extracted_tags.json` — результат шага 3 (tags по категориям, proposed)
- `../1_анализ_главы/merge.json` — полный анализ (framework, insights, application)
- `../1_анализ_главы/вход_главы.json` — текст главы и метаданные
**Выход:** `validated_tags.json` в каталоге скрипта (или путь через `-o`).
**Формат выхода:** `tags` (по категориям с обновлённым confidence), `removed` (массив снятых тегов: tag, category, reason).
**Запуск:**
```bash
cd 4_валидация_тегов
python3 run_validate_tags_ollama.py
# с указанием путей:
python3 run_validate_tags_ollama.py --extracted-tags /path/to/extracted_tags.json --merge /path/to/merge.json --chapter /path/to/вход_главы.json -o validated_tags.json
```
## Использование в пайплайне
Вызывается после шага 3 (извлечение тегов). Модель: qwen3-14b:8bit (или аналог). Время: ~2030 сек на главу.

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""
Валидация извлечённых тегов через Ollama (шаг 4).
Проверка релевантности тегов содержанию главы и анализу; снятие нерелевантных, обновление confidence.
Вход: extracted_tags.json (шаг 3), merge.json (анализ), вход_главы.json (текст главы).
Выход: один JSON-файл (tags с обновлённым confidence, removed).
"""
import argparse
import json
import re
import sys
import time
import urllib.request
from pathlib import Path
OLLAMA_URL = "http://localhost:11434"
MODEL = "qwen3:14b"
DIR = Path(__file__).resolve().parent
DEFAULT_EXTRACTED_TAGS = DIR.parent / "3_извлечениеегов" / "extracted_tags.json"
DEFAULT_MERGE = DIR.parent / "1_анализ_главы" / "merge.json"
DEFAULT_CHAPTER = DIR.parent / "1_анализ_главы" / "вход_главы.json"
OLLAMA_OPTIONS = {
"temperature": 0.2,
"num_ctx": 8500,
"num_predict": 2048,
"repeat_penalty": 1.1,
}
PROMPT_FILE = "validate_tags.txt"
def load_json(path: Path) -> dict:
"""Загружает JSON из файла."""
with open(path, encoding="utf-8") as f:
return json.load(f)
def load_prompt(filename: str) -> str:
"""Загружает шаблон промпта из файла."""
with open(DIR / filename, encoding="utf-8") as f:
return f.read()
def substitute_prompt(
prompt: str,
book_title: str,
chapter_title: str,
extracted_tags_str: str,
framework_str: str,
insights_str: str,
application_str: str,
chapter_text: str,
) -> str:
"""Подставляет в промпт все поля."""
return (
prompt.replace("{book_title}", book_title)
.replace("{chapter_title}", chapter_title)
.replace("{extracted_tags_json}", extracted_tags_str)
.replace("{framework}", framework_str)
.replace("{insights}", insights_str)
.replace("{application}", application_str)
.replace("{chapter_text}", chapter_text)
)
def extract_json_from_response(text: str) -> dict:
"""Достаёт JSON из ответа модели (может быть обёрнут в ```json ... ```)."""
text = text.strip()
match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text)
if match:
text = match.group(1).strip()
return json.loads(text)
def call_ollama(prompt: str) -> str:
"""Вызывает Ollama /api/chat и возвращает content ответа."""
body = json.dumps(
{
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"format": "json",
"options": OLLAMA_OPTIONS,
"keep_alive": 0,
},
ensure_ascii=False,
).encode("utf-8")
req = urllib.request.Request(
f"{OLLAMA_URL}/api/chat",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=None) as resp:
data = json.load(resp)
return data.get("message", {}).get("content", "")
except urllib.error.HTTPError as e:
body_b = b""
if e.fp:
try:
body_b = e.fp.read()[:1000]
except Exception:
pass
raise RuntimeError(
f"Ollama HTTP {e.code}: {e.reason}. Body: {body_b.decode('utf-8', errors='replace')}"
) from e
def main() -> int:
"""Загружает данные, вызывает валидацию тегов, пишет результат в JSON."""
parser = argparse.ArgumentParser(
description="Валидация извлечённых тегов через Ollama (шаг 4)."
)
parser.add_argument(
"--extracted-tags",
type=Path,
default=DEFAULT_EXTRACTED_TAGS,
help=f"Путь к extracted_tags.json из шага 3 (по умолчанию: {DEFAULT_EXTRACTED_TAGS})",
)
parser.add_argument(
"--merge",
type=Path,
default=DEFAULT_MERGE,
help=f"Путь к merge.json (по умолчанию: {DEFAULT_MERGE})",
)
parser.add_argument(
"--chapter",
type=Path,
default=DEFAULT_CHAPTER,
help=f"Путь к вход_главы.json (по умолчанию: {DEFAULT_CHAPTER})",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DIR / "validated_tags.json",
help="Путь к выходному JSON (по умолчанию: validated_tags.json)",
)
args = parser.parse_args()
if not args.extracted_tags.is_file():
print(f"Файл не найден: {args.extracted_tags}", file=sys.stderr)
return 1
if not args.merge.is_file():
print(f"Файл не найден: {args.merge}", file=sys.stderr)
return 1
if not args.chapter.is_file():
print(f"Файл не найден: {args.chapter}", file=sys.stderr)
return 1
print("Загрузка extracted_tags.json, merge.json и вход_главы.json...")
extracted = load_json(args.extracted_tags)
merge = load_json(args.merge)
chapter = load_json(args.chapter)
book_title = chapter.get("book_title", "")
chapter_title = chapter.get("chapter_title", "")
chapter_text = chapter.get("chapter_text", "")
extracted_tags_str = json.dumps(extracted, ensure_ascii=False, indent=2)
framework_str = json.dumps(merge.get("framework", {}), ensure_ascii=False, indent=2)
insights_str = json.dumps(merge.get("insights", []), ensure_ascii=False, indent=2)
application_str = json.dumps(
merge.get("application", {}), ensure_ascii=False, indent=2
)
prompt_tpl = load_prompt(PROMPT_FILE)
prompt = substitute_prompt(
prompt_tpl,
book_title,
chapter_title,
extracted_tags_str,
framework_str,
insights_str,
application_str,
chapter_text,
)
print(f"Вызов Ollama {MODEL} — валидация тегов...")
t0 = time.monotonic()
try:
raw = call_ollama(prompt)
except Exception as e:
print(f"Ошибка вызова Ollama: {e}", file=sys.stderr)
return 1
elapsed = time.monotonic() - t0
print(f"Ответ получен за {elapsed:.1f} сек ({elapsed / 60:.1f} мин)")
try:
result = extract_json_from_response(raw)
except json.JSONDecodeError as e:
print(f"Не удалось распарсить JSON ответа: {e}", file=sys.stderr)
print("Первые 500 символов ответа:", raw[:500], file=sys.stderr)
return 1
if not isinstance(result, dict):
print(
f"Ожидался объект JSON, получен: {type(result).__name__}",
file=sys.stderr,
)
return 1
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Записано: {args.output}")
removed_count = len(result.get("removed", []))
print(f"Снято тегов: {removed_count}")
return 0
if __name__ == "__main__":
sys.exit(main())