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

@@ -19,8 +19,8 @@ DIR = Path(__file__).resolve().parent
OLLAMA_OPTIONS = {
"temperature": 0.3,
"num_ctx": 8500,
"num_predict": 4096,
"num_ctx": 9000,
"num_predict": 8500,
"repeat_penalty": 1.1,
}

View File

@@ -22,3 +22,27 @@
- `{limitations_json}` — JSON блока `limitations`
Промпты рассчитаны на тестирование по одному; доработка — по результатам прогонов.
## Скрипт валидации через Ollama
`run_validation_ollama.py` — последовательная валидация всех четырёх блоков через Ollama. На выходе один JSON-файл со статусами по каждому блоку.
**Вход (по умолчанию):**
- `../1_анализ_главы/merge.json` — результат этапа 1 (framework, insights, application, limitations)
- `../1_анализ_главы/вход_главы.json` — текст главы и метаданные (book_title, chapter_title, chapter_text)
**Выход:** `validation_status.json` в каталоге скрипта (или путь через `-o`).
**Формат выхода:** по одному ключу на блок (`framework`, `insights`, `application`, `limitations`). Каждое значение:
- `verdict``ok` | `needs_review` | `bad` | `skipped` | `error`
- `score` — число 0.0 или `null`
- `hallucinations` — список замечаний по галлюцинациям
- `missing_key_points` — список пропущенных ключевых моментов
**Запуск:**
```bash
cd 2_валидация_анализа_по_блокам
python3 run_validation_ollama.py
# с указанием путей:
python3 run_validation_ollama.py --merge /path/to/merge.json --chapter /path/to/вход_главы.json -o validation_status.json
```

View File

@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
Валидация анализа главы по блокам через Ollama: framework → insights → application → limitations.
Вход: merge.json (анализ из этапа 1), вход_главы.json (текст главы и метаданные).
Выход: один JSON-файл со статусами по каждому блоку (verdict, score, hallucinations, missing_key_points).
"""
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_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,
}
BLOCKS = [
("framework", "validate_framework.txt", "framework_json"),
("insights", "validate_insights.txt", "insights_json"),
("application", "validate_application.txt", "application_json"),
("limitations", "validate_limitations.txt", "limitations_json"),
]
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,
chapter_text: str,
block_json: str,
block_placeholder: str,
) -> str:
"""Подставляет в промпт поля главы и JSON блока. block_placeholder — например {framework_json}."""
return (
prompt.replace("{book_title}", book_title)
.replace("{chapter_title}", chapter_title)
.replace("{chapter_text}", chapter_text)
.replace(block_placeholder, block_json)
)
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:
"""Последовательно валидирует 4 блока и пишет один JSON со статусами."""
parser = argparse.ArgumentParser(
description="Валидация анализа главы по блокам через Ollama. Выход — JSON со статусами."
)
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 / "validation_status.json",
help="Путь к выходному JSON со статусами (по умолчанию: validation_status.json)",
)
args = parser.parse_args()
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("Загрузка merge.json и вход_главы.json...")
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", "")
results: dict = {}
for block_name, prompt_file, json_placeholder in BLOCKS:
block_data = merge.get(block_name)
if block_data is None:
print(f"Блок «{block_name}» отсутствует в merge.json, пропуск.", file=sys.stderr)
results[block_name] = {
"verdict": "skipped",
"score": None,
"hallucinations": [],
"missing_key_points": [],
"error": "block not found in merge",
}
continue
prompt_tpl = load_prompt(prompt_file)
block_json_str = json.dumps(block_data, ensure_ascii=False, indent=2)
placeholder = "{" + json_placeholder + "}" # {framework_json}, {insights_json}, ...
prompt = substitute_prompt(
prompt_tpl,
book_title,
chapter_title,
chapter_text,
block_json_str,
placeholder,
)
print(f"Валидация блока «{block_name}»...")
t0 = time.monotonic()
try:
raw = call_ollama(prompt)
except Exception as e:
print(f"Ошибка вызова Ollama на блоке «{block_name}»: {e}", file=sys.stderr)
results[block_name] = {
"verdict": "error",
"score": None,
"hallucinations": [],
"missing_key_points": [],
"error": str(e),
}
continue
elapsed = time.monotonic() - t0
print(f" Ответ за {elapsed:.1f} сек ({elapsed / 60:.1f} мин)")
try:
block_result = extract_json_from_response(raw)
except json.JSONDecodeError as e:
print(
f"Не удалось распарсить JSON в блоке «{block_name}»: {e}",
file=sys.stderr,
)
print("Первые 500 символов ответа:", raw[:500], file=sys.stderr)
results[block_name] = {
"verdict": "error",
"score": None,
"hallucinations": [],
"missing_key_points": [],
"error": f"JSON decode: {e}",
}
continue
if not isinstance(block_result, dict):
results[block_name] = {
"verdict": "error",
"score": None,
"hallucinations": [],
"missing_key_points": [],
"error": f"expected dict, got {type(block_result).__name__}",
}
continue
results[block_name] = {
"verdict": block_result.get("verdict", "unknown"),
"score": block_result.get("score"),
"hallucinations": block_result.get("hallucinations", []),
"missing_key_points": block_result.get("missing_key_points", []),
}
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"Записано: {args.output}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -14,7 +14,27 @@
- `{chapter_title}` — название главы
- `{full_analysis_json}` — полный JSON анализа (склейка блоков framework + insights + application + limitations)
## Использование
## Скрипт через Ollama
`run_consistency_ollama.py` — один вызов Ollama для проверки согласованности блоков. На выходе один JSON-файл.
**Вход (по умолчанию):**
- `../1_анализ_главы/merge.json` — полный анализ (framework, insights, application, limitations)
- `../1_анализ_главы/вход_главы.json` — метаданные главы (book_title, chapter_title)
**Выход:** `consistency_result.json` в каталоге скрипта (или путь через `-o`).
**Формат выхода:** `verdict` (ok | needs_review | bad), `score`, `inconsistencies` — список рассогласованностей с полями type, location, summary.
**Запуск:**
```bash
cd 2b_финальная_валидация_согласованности
python3 run_consistency_ollama.py
# с указанием путей:
python3 run_consistency_ollama.py --merge /path/to/merge.json --chapter /path/to/вход_главы.json -o consistency_result.json
```
## Использование в пайплайне
Вызывается после шага 2a (склейка блоков). При вердикте bad или needs_review — флаг `needs_review` по главе; опционально — «примиряющий» промпт для минимальных правок.

View File

@@ -1,159 +0,0 @@
{
"framework": {
"terms": {
"стимул": "что в окружающей среде заставляет человека действовать",
"привычка": "повторяющееся действие, которое становится автоматическим",
"подкрепление": "воздействие, усиливающее вероятность повторения действия",
"среда": "окружение, влияющее на поведение через стимулы и ограничения",
"желание, вызванное стимулом": "непроизвольная тяга к действию, спровоцированному внешним триггером",
"аутокаталитический процесс": "процесс, усиливающий себя сам"
},
"principles": [
{
"title": "Среда формирует поведение",
"description": "Человек не управляет привычками напрямую, а воздействует на окружение, чтобы оно вело его к нужным действиям",
"example": "Убрать телефон с рабочего стола, чтобы не отвлекаться на соцсети",
"chains": [
{
"cause": "Человек находится в среде с доступными стимулами для нежелательной привычки",
"mechanism": "Стимул активирует уже сформированную привычку, которая запускает автоматическое действие",
"result": "Человек повторяет вредную привычку, не осознавая этого"
},
{
"cause": "Изменяется среда, удаляются стимулы, провоцирующие нежелательную привычку",
"mechanism": "Отсутствие триггеров снижает вероятность срабатывания привычки",
"result": "Человек реже проявляет нежелательное поведение"
}
]
},
{
"title": "Привычки не исчезают, они только временно подавлены",
"description": "Даже если человек перестаёт делать что-то, привычка остаётся в мозге и может вернуться при подходящих условиях",
"example": "Человек перестаёт курить, но снова начинает после посещения места, где раньше курил",
"chains": [
{
"cause": "Человек прекращает проявлять вредную привычку",
"mechanism": "Мозг сохраняет нейронные связи, связанные с этой привычкой",
"result": "Привычка может вернуться, если появятся подходящие стимулы"
},
{
"cause": "Окружение меняется, и в нем появляются триггеры, связанные с прошлой привычкой",
"mechanism": "Стимулы запускают автоматические паттерны поведения",
"result": "Человек снова начинает делать то, что уже перестал"
}
]
},
{
"title": "Самоконтроль — это краткосрочный, а не долгосрочный инструмент",
"description": "Противостояние искушению неэффективно на длительном сроке, нужно менять контекст",
"example": "Человек отказывается от сладкого, но возвращается к нему после стрессового дня",
"chains": [
{
"cause": "Человек пытается контролировать себя в момент искушения",
"mechanism": "Это требует энергии и усилий, которые невозможно поддерживать всегда",
"result": "Человек устаёт от борьбы и возвращается к нежелательной привычке"
},
{
"cause": "Человек изменяет окружение, чтобы искушения стали менее доступными",
"mechanism": "Стимулы для плохого поведения становятся незаметными или невозможными",
"result": "Человек реже сталкивается с искушением"
}
]
},
{
"title": "Дисциплина — это настройка среды, а не внутреннее усилие",
"description": "Сильные люди не просто дисциплинированы, а живут в среде, которая поддерживает их цели",
"example": "Человек встаёт раньше, потому что спальня тёмная и не имеет гаджетов",
"chains": [
{
"cause": "Человек пытается быть дисциплинированным только силой воли",
"mechanism": "Это требует большого количества энергии и часто неудачно",
"result": "Человек теряет мотивацию и возвращается к нежелательному поведению"
},
{
"cause": "Человек настраивает окружение так, чтобы хорошее поведение было лёгким",
"mechanism": "Стимулы для правильных действий становятся более доступными",
"result": "Человек чаще выбирает здоровые привычки"
}
]
},
{
"title": "Негативные эмоции усиливают негативное поведение",
"description": "Стресс, тревога и другие негативные чувства провоцируют возвращение к вредным привычкам",
"example": "Человек садится на диван и ест шоколад, когда ему плохо",
"chains": [
{
"cause": "Человек испытывает стресс или негативные эмоции",
"mechanism": "Это активирует привычку, которая помогает снизить дискомфорт",
"result": "Человек снова начинает делать то, что раньше снимало напряжение"
},
{
"cause": "Человек пытается избавиться от вредной привычки, но не меняет среду",
"mechanism": "Стресс и негативные эмоции остаются, усиливая желание к действию",
"result": "Человек возвращается к нежелательной привычке"
}
]
}
]
},
"insights": [
{
"title": "Среда влияет на привычки больше, чем сила воли",
"description": "Чтобы изменить привычку, нужно изменить окружение, а не надеяться только на самоконтроль",
"example": "Убрать телефон с рабочего стола, чтобы не отвлекаться на соцсети"
},
{
"title": "Привычки не исчезают, только временно подавляются",
"description": "Даже если человек перестаёт курить или есть сладкое, эти привычки остаются в мозге и могут вернуться при подходящих условиях",
"example": "После посещения бара человек снова начинает пить, не осознавая этого"
},
{
"title": "Негативные эмоции усиливают вредное поведение",
"description": "Стресс, тревога и другие негативные чувства могут спровоцировать возвращение к старым привычкам",
"example": "После ссоры человек снова начинает есть вредную пищу, чтобы успокоиться"
},
{
"title": "Сокращение стимулов уменьшает повторение нежелательного поведения",
"description": "Удаление триггеров, связанных с вредной привычкой, снижает вероятность её возобновления",
"example": "Убрать сигареты из дома, чтобы уменьшить вероятность их употребления"
},
{
"title": "Самоконтроль — это временная стратегия, а не долгосрочное решение",
"description": "Постоянно бороться с искушением утомительно и непрочное; нужно настраивать среду так, чтобы искушений не было совсем",
"example": "Поставить телефон в другую комнату, чтобы не проверять соцсети во время работы"
}
],
"application": {
"techniques": [
{
"name": "Снижение доступности стимула",
"goal": "Уменьшение вероятности повторения нежелательной привычки за счёт удаления её триггеров",
"context_example": "Вечер после работы, когда тянет залипнуть в телефон и проверять соцсети",
"steps": [
"Определите конкретный стимул (например, наличие телефона на столе)",
"Уберите объект из зоны видимости или досягаемости (например, оставьте телефон в другой комнате)",
"Запишите дату и время изменения (для отслеживания эффективности)"
],
"client_phrase": "Уберите источник искушения из вашей среды — действие станет менее вероятным",
"success_criteria": "Частота использования телефона в моменты, когда это не нужно, снижается с 5 раз/день до 12 раз/день за 2 недели"
}
]
},
"limitations": [
{
"description": "Идеи не работают, если триггер (то, что в окружении запускает действие) не устранён или остаётся доступным, так как привычка может вернуться при появлении стимула",
"when_relevant": "Когда человек не полностью убирает триггер из своей среды или сталкивается с ним в новой обстановке",
"example": "Человек удалил сигареты из дома, но снова начинает курить на работе среди коллег-курильщиков"
},
{
"description": "Техники могут не сработать, если негативные эмоции (стресс, тревога) остаются необратленными и усиливают нежелательное поведение",
"when_relevant": "Когда человек продолжает испытывать сильный стресс или эмоциональные трудности, не решая их на уровне среды",
"example": "Человек убрал телефон из комнаты, но всё равно переедает перед сном из-за хронической тревоги"
},
{
"description": "Изменения среды могут быть недостаточны, если привычка закреплена на уровне автоматических нейронных паттернов и не сопровождается новыми, здоровыми привычками",
"when_relevant": "Когда человек устраняет триггер, но не вводит альтернативное поведение для подкрепления",
"example": "Человек убрал телевизор из спальни, но не нашёл замену в виде чтения или медитации и проводит время впустую"
}
]
}

View File

@@ -1,248 +0,0 @@
{
"framework": {
"terms": {
"стимул": "что в окружающей среде заставляет человека действовать",
"привычка": "повторяющееся действие, которое становится автоматическим",
"подкрепление": "воздействие, усиливающее вероятность повторения действия",
"среда": "окружение, влияющее на поведение через стимулы и ограничения",
"желание, вызванное стимулом": "непроизвольная тяга к действию, спровоцированному внешним триггером",
"аутокаталитический процесс": "процесс, усиливающий себя сам"
},
"principles": [
{
"title": "Среда формирует поведение",
"description": "Человек не управляет привычками напрямую, а воздействует на окружение, чтобы оно вело его к нужным действиям",
"example": "Убрать телефон с рабочего стола, чтобы не отвлекаться на соцсети",
"chains": [
{
"cause": "Человек находится в среде с доступными стимулами для нежелательной привычки",
"mechanism": "Стимул активирует уже сформированную привычку, которая запускает автоматическое действие",
"result": "Человек повторяет вредную привычку, не осознавая этого"
},
{
"cause": "Изменяется среда, удаляются стимулы, провоцирующие нежелательную привычку",
"mechanism": "Отсутствие триггеров снижает вероятность срабатывания привычки",
"result": "Человек реже проявляет нежелательное поведение"
}
]
},
{
"title": "Привычки не исчезают, они только временно подавлены",
"description": "Даже если человек перестаёт делать что-то, привычка остаётся в мозге и может вернуться при подходящих условиях",
"example": "Человек перестаёт курить, но снова начинает после посещения места, где раньше курил",
"chains": [
{
"cause": "Человек прекращает проявлять вредную привычку",
"mechanism": "Мозг сохраняет нейронные связи, связанные с этой привычкой",
"result": "Привычка может вернуться, если появятся подходящие стимулы"
},
{
"cause": "Окружение меняется, и в нем появляются триггеры, связанные с прошлой привычкой",
"mechanism": "Стимулы запускают автоматические паттерны поведения",
"result": "Человек снова начинает делать то, что уже перестал"
}
]
},
{
"title": "Самоконтроль — это краткосрочный, а не долгосрочный инструмент",
"description": "Противостояние искушению неэффективно на длительном сроке, нужно менять контекст",
"example": "Человек отказывается от сладкого, но возвращается к нему после стрессового дня",
"chains": [
{
"cause": "Человек пытается контролировать себя в момент искушения",
"mechanism": "Это требует энергии и усилий, которые невозможно поддерживать всегда",
"result": "Человек устаёт от борьбы и возвращается к нежелательной привычке"
},
{
"cause": "Человек изменяет окружение, чтобы искушения стали менее доступными",
"mechanism": "Стимулы для плохого поведения становятся незаметными или невозможными",
"result": "Человек реже сталкивается с искушением"
}
]
},
{
"title": "Дисциплина — это настройка среды, а не внутреннее усилие",
"description": "Сильные люди не просто дисциплинированы, а живут в среде, которая поддерживает их цели",
"example": "Человек встаёт раньше, потому что спальня тёмная и не имеет гаджетов",
"chains": [
{
"cause": "Человек пытается быть дисциплинированным только силой воли",
"mechanism": "Это требует большого количества энергии и часто неудачно",
"result": "Человек теряет мотивацию и возвращается к нежелательному поведению"
},
{
"cause": "Человек настраивает окружение так, чтобы хорошее поведение было лёгким",
"mechanism": "Стимулы для правильных действий становятся более доступными",
"result": "Человек чаще выбирает здоровые привычки"
}
]
},
{
"title": "Негативные эмоции усиливают негативное поведение",
"description": "Стресс, тревога и другие негативные чувства провоцируют возвращение к вредным привычкам",
"example": "Человек садится на диван и ест шоколад, когда ему плохо",
"chains": [
{
"cause": "Человек испытывает стресс или негативные эмоции",
"mechanism": "Это активирует привычку, которая помогает снизить дискомфорт",
"result": "Человек снова начинает делать то, что раньше снимало напряжение"
},
{
"cause": "Человек пытается избавиться от вредной привычки, но не меняет среду",
"mechanism": "Стресс и негативные эмоции остаются, усиливая желание к действию",
"result": "Человек возвращается к нежелательной привычке"
}
]
}
]
},
"insights": [
{
"title": "Среда влияет на привычки больше, чем сила воли",
"description": "Чтобы изменить привычку, нужно изменить окружение, а не надеяться только на самоконтроль",
"example": "Убрать телефон с рабочего стола, чтобы не отвлекаться на соцсети"
},
{
"title": "Привычки не исчезают, только временно подавляются",
"description": "Даже если человек перестаёт курить или есть сладкое, эти привычки остаются в мозге и могут вернуться при подходящих условиях",
"example": "После посещения бара человек снова начинает пить, не осознавая этого"
},
{
"title": "Негативные эмоции усиливают вредное поведение",
"description": "Стресс, тревога и другие негативные чувства могут спровоцировать возвращение к старым привычкам",
"example": "После ссоры человек снова начинает есть вредную пищу, чтобы успокоиться"
},
{
"title": "Сокращение стимулов уменьшает повторение нежелательного поведения",
"description": "Удаление триггеров, связанных с вредной привычкой, снижает вероятность её возобновления",
"example": "Убрать сигареты из дома, чтобы уменьшить вероятность их употребления"
},
{
"title": "Самоконтроль — это временная стратегия, а не долгосрочное решение",
"description": "Постоянно бороться с искушением утомительно и непрочное; нужно настраивать среду так, чтобы искушений не было совсем",
"example": "Поставить телефон в другую комнату, чтобы не проверять соцсети во время работы"
}
],
"application": {
"techniques": [
{
"name": "Снижение доступности стимула",
"goal": "Уменьшение вероятности повторения нежелательной привычки за счёт удаления её триггеров",
"context_example": "Вечер после работы, когда тянет залипнуть в телефон и проверять соцсети",
"steps": [
"Определите конкретный стимул (например, наличие телефона на столе)",
"Уберите объект из зоны видимости или досягаемости (например, оставьте телефон в другой комнате)",
"Запишите дату и время изменения (для отслеживания эффективности)"
],
"client_phrase": "Уберите источник искушения из вашей среды — действие станет менее вероятным",
"success_criteria": "Частота использования телефона в моменты, когда это не нужно, снижается с 5 раз/день до 12 раз/день за 2 недели"
}
]
},
"limitations": [
{
"description": "Идеи не работают, если триггер (то, что в окружении запускает действие) не устранён или остаётся доступным, так как привычка может вернуться при появлении стимула",
"when_relevant": "Когда человек не полностью убирает триггер из своей среды или сталкивается с ним в новой обстановке",
"example": "Человек удалил сигареты из дома, но снова начинает курить на работе среди коллег-курильщиков"
},
{
"description": "Техники могут не сработать, если негативные эмоции (стресс, тревога) остаются необратленными и усиливают нежелательное поведение",
"when_relevant": "Когда человек продолжает испытывать сильный стресс или эмоциональные трудности, не решая их на уровне среды",
"example": "Человек убрал телефон из комнаты, но всё равно переедает перед сном из-за хронической тревоги"
},
{
"description": "Изменения среды могут быть недостаточны, если привычка закреплена на уровне автоматических нейронных паттернов и не сопровождается новыми, здоровыми привычками",
"when_relevant": "Когда человек устраняет триггер, но не вводит альтернативное поведение для подкрепления",
"example": "Человек убрал телевизор из спальни, но не нашёл замену в виде чтения или медитации и проводит время впустую"
}
],
"tags": {
"principle": [
{
"tag": "среда формирует поведение",
"confidence": 0.95
},
{
"tag": "привычки не исчезают",
"confidence": 0.92
},
{
"tag": "самоконтроль краткосрочен",
"confidence": 0.9
},
{
"tag": "дисциплина через среду",
"confidence": 0.9
},
{
"tag": "негативные эмоции усиливают поведение",
"confidence": 0.85
}
],
"psychology": [
{
"tag": "стимул",
"confidence": 0.95
},
{
"tag": "привычка",
"confidence": 0.95
},
{
"tag": "желание вызванное стимулом",
"confidence": 0.9
},
{
"tag": "аутокаталитический процесс",
"confidence": 0.85
},
{
"tag": "подкрепление",
"confidence": 0.8
}
],
"method": [
{
"tag": "снижение доступности стимула",
"confidence": 0.95
},
{
"tag": "настройка среды",
"confidence": 0.92
},
{
"tag": "устранение триггеров",
"confidence": 0.9
}
],
"result": [
{
"tag": "уменьшение вредных привычек",
"confidence": 0.95
},
{
"tag": "устойчивое поведение",
"confidence": 0.9
},
{
"tag": "снижение искушений",
"confidence": 0.85
}
],
"context": [
{
"tag": "поведенческая психология",
"confidence": 0.95
},
{
"tag": "привычки",
"confidence": 0.92
},
{
"tag": "атомные привычки",
"confidence": 0.9
}
]
},
"removed": []
}

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""
Финальная валидация согласованности анализа главы через Ollama (шаг 2b).
Проверяет связи между блоками: application ↔ framework, insights ↔ framework, limitations ↔ остальное.
Вход: merge.json (полный анализ), вход_главы.json (метаданные главы).
Выход: один JSON-файл (verdict, score, inconsistencies).
"""
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_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_consistency.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,
full_analysis_json: str,
) -> str:
"""Подставляет в промпт поля главы и полный JSON анализа."""
return (
prompt.replace("{book_title}", book_title)
.replace("{chapter_title}", chapter_title)
.replace("{full_analysis_json}", full_analysis_json)
)
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 (шаг 2b)."
)
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 / "consistency_result.json",
help="Путь к выходному JSON (по умолчанию: consistency_result.json)",
)
args = parser.parse_args()
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("Загрузка merge.json и вход_главы.json...")
merge = load_json(args.merge)
chapter = load_json(args.chapter)
book_title = chapter.get("book_title", "")
chapter_title = chapter.get("chapter_title", "")
full_analysis_json = json.dumps(merge, ensure_ascii=False, indent=2)
prompt_tpl = load_prompt(PROMPT_FILE)
prompt = substitute_prompt(
prompt_tpl,
book_title,
chapter_title,
full_analysis_json,
)
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}")
verdict = result.get("verdict", "?")
print(f"Вердикт: {verdict}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -27,6 +27,29 @@
JSON с тегами по категориям (ключи на английском: `principle`, `psychology`, `method`, `result`, `context`) и confidence score для каждого тега; при необходимости — кандидаты в `proposed` с полем `category` из того же набора. Маппинг категорий: ARCHITECTURE_SUMMARY.md → раздел «Хранение данных» → таблица `tags`.
## Использование
## Скрипт через Ollama
Вызывается после шага 2b (финальная валидация). Модель: qwen3-14b:8bit (или аналог). Перед вызовом в промпт подставляется актуальный список тегов из БД.
`run_extract_tags_ollama.py` — один вызов Ollama для извлечения тегов по категориям. На выходе один JSON-файл.
**Вход (по умолчанию):**
- `../1_анализ_главы/merge.json` — полный анализ (framework, insights, application, limitations)
- `../1_анализ_главы/вход_главы.json` — текст главы и метаданные
- `allowed_tags.json` — допустимые теги по категориям (объект с ключами principle, psychology, method, result, context и массивами строк). Если файла нет — используется пустой список (модель вернёт только блок `proposed`). Пример: `allowed_tags.example.json`.
**Выход:** `extracted_tags.json` в каталоге скрипта (или путь через `-o`).
**Формат выхода:** `tags` (по категориям: principle, psychology, method, result, context — массивы объектов `{ "tag", "confidence" }`), `proposed` (кандидаты в новые теги).
**Запуск:**
```bash
cd 3_извлечениеегов
# подготовить список тегов (скопировать пример или экспорт из БД):
cp allowed_tags.example.json allowed_tags.json
python3 run_extract_tags_ollama.py
# с указанием путей:
python3 run_extract_tags_ollama.py --merge /path/to/merge.json --chapter /path/to/вход_главы.json --allowed-tags allowed_tags.json -o extracted_tags.json
```
## Использование в пайплайне
Вызывается после шага 2b (финальная валидация). Модель: qwen3-14b:8bit (или аналог). Перед вызовом в промпт подставляется актуальный список тегов из БД (или из файла `allowed_tags.json`).

View File

@@ -0,0 +1,31 @@
{
"principle": [
"среда формирует поведение",
"привычки не исчезают",
"самоконтроль краткосрочен",
"дисциплина через среду",
"негативные эмоции усиливают поведение"
],
"psychology": [
"стимул",
"привычка",
"желание вызванное стимулом",
"аутокаталитический процесс",
"подкрепление"
],
"method": [
"снижение доступности стимула",
"настройка среды",
"устранение триггеров"
],
"result": [
"уменьшение вредных привычек",
"устойчивое поведение",
"снижение искушений"
],
"context": [
"поведенческая психология",
"привычки",
"атомные привычки"
]
}

View File

@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Извлечение тегов по категориям из анализа главы через Ollama (шаг 3).
Вход: merge.json (анализ), вход_главы.json (текст главы), allowed_tags.json (допустимые теги).
Выход: один JSON-файл (tags по категориям, proposed).
"""
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_MERGE = DIR.parent / "1_анализ_главы" / "merge.json"
DEFAULT_CHAPTER = DIR.parent / "1_анализ_главы" / "вход_главы.json"
DEFAULT_ALLOWED_TAGS = DIR / "allowed_tags.json"
OLLAMA_OPTIONS = {
"temperature": 0.3,
"num_ctx": 8500,
"num_predict": 2048,
"repeat_penalty": 1.1,
}
PROMPT_FILE = "extract_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,
framework_str: str,
insights_str: str,
application_str: str,
limitations_str: str,
chapter_text: str,
allowed_tags_str: str,
) -> str:
"""Подставляет в промпт все поля."""
return (
prompt.replace("{book_title}", book_title)
.replace("{chapter_title}", chapter_title)
.replace("{framework}", framework_str)
.replace("{insights}", insights_str)
.replace("{application}", application_str)
.replace("{limitations}", limitations_str)
.replace("{chapter_text}", chapter_text)
.replace("{allowed_tags_json}", allowed_tags_str)
)
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 (шаг 3)."
)
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(
"--allowed-tags",
type=Path,
default=DEFAULT_ALLOWED_TAGS,
help=f"Путь к JSON со списком допустимых тегов по категориям (по умолчанию: allowed_tags.json). Формат: объект с ключами principle, psychology, method, result, context и массивами строк.",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DIR / "extracted_tags.json",
help="Путь к выходному JSON (по умолчанию: extracted_tags.json)",
)
args = parser.parse_args()
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("Загрузка merge.json, вход_главы.json и allowed_tags...")
merge = load_json(args.merge)
chapter = load_json(args.chapter)
if args.allowed_tags.is_file():
allowed_tags = load_json(args.allowed_tags)
else:
allowed_tags = {}
print(
f"Файл {args.allowed_tags} не найден; используется пустой список тегов (модель вернёт только proposed).",
file=sys.stderr,
)
book_title = chapter.get("book_title", "")
chapter_title = chapter.get("chapter_title", "")
chapter_text = chapter.get("chapter_text", "")
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
)
limitations_str = json.dumps(
merge.get("limitations", []), ensure_ascii=False, indent=2
)
allowed_tags_str = json.dumps(allowed_tags, ensure_ascii=False, indent=2)
prompt_tpl = load_prompt(PROMPT_FILE)
prompt = substitute_prompt(
prompt_tpl,
book_title,
chapter_title,
framework_str,
insights_str,
application_str,
limitations_str,
chapter_text,
allowed_tags_str,
)
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}")
return 0
if __name__ == "__main__":
sys.exit(main())

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())

View File

@@ -14,16 +14,37 @@
Один JSON: все поля анализа + `tags`, `removed`. Если задан `--input-chapter`, в выход добавляются метаданные главы/книги (book_id, chapter_id, chapter_number, chapter_title, book_title, author). Этот документ передаётся в шаг 6 (генерация эмбеддингов) и в шаг 7 (payload Qdrant).
## Использование
## Скрипт с путями по умолчанию
`run_merge_analysis_tags.py` — мерж с путями по умолчанию (как в остальных этапах). Без вызова LLM.
**Вход (по умолчанию):**
- `../1_анализ_главы/merge.json` — полный анализ (framework, insights, application, limitations)
- `../4_валидация_тегов/validated_tags.json` — результат шага 4 (tags, removed)
- `../1_анализ_главы/вход_главы.json` — метаданные главы/книги (book_id, chapter_id и т.д.; подставляются в выход для шагов 68)
**Выход:** `merged_with_tags.json` в каталоге скрипта (или путь через `-o`).
**Запуск:**
```bash
python3 merge_analysis_tags.py <merge.json> <выход_valid_tag.json> [--input-chapter вход_главы.json] [-o выход.json]
cd 5_мерж_анализа_и_тегов
python3 run_merge_analysis_tags.py
# с указанием путей:
python3 run_merge_analysis_tags.py --merge /path/to/merge.json --tags /path/to/validated_tags.json --input-chapter /path/to/вход_главы.json -o merged_with_tags.json
# без метаданных главы:
python3 run_merge_analysis_tags.py --no-chapter
```
Пример (с метаданными для шага 7):
## Универсальный скрипт (позиционные аргументы)
```bash
python3 merge_analysis_tags.py ../2b_финальная_валидация_согласованности/merge.json ../4_валидация_тегов/выход_valid_tag.json --input-chapter ../1_анализ_главы/вход_главы.example.json -o merged_with_tags.json
python3 merge_analysis_tags.py <merge.json> <validated_tags.json> [--input-chapter вход_главы.json] [-o выход.json]
```
Пример:
```bash
python3 merge_analysis_tags.py ../1_анализ_главы/merge.json ../4_валидация_тегов/validated_tags.json --input-chapter ../1_анализ_главы/вход_главы.json -o merged_with_tags.json
```
Без `-o` результат выводится в stdout.

View File

@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Запуск шага 5 с путями по умолчанию: мерж анализа (2b) и валидированных тегов (4).
Без вызова LLM — только слияние JSON. Использует merge_analysis_tags.merge_analysis_and_tags.
Вход по умолчанию: merge.json (1), validated_tags.json (4), вход_главы.json (1).
Выход по умолчанию: merged_with_tags.json в каталоге скрипта.
"""
import argparse
import json
import sys
from pathlib import Path
DIR = Path(__file__).resolve().parent
DEFAULT_ANALYSIS = DIR.parent / "1_анализ_главы" / "merge.json"
DEFAULT_TAGS = DIR.parent / "4_валидация_тегов" / "validated_tags.json"
DEFAULT_CHAPTER = DIR.parent / "1_анализ_главы" / "вход_главы.json"
DEFAULT_OUTPUT = DIR / "merged_with_tags.json"
# Импорт логики мержа из основного скрипта
from merge_analysis_tags import load_json, merge_analysis_and_tags
def main() -> int:
"""Загружает анализ и теги по путям (по умолчанию — из соседних этапов), мержит, пишет выход."""
parser = argparse.ArgumentParser(
description="Мерж анализа (2b) и валидированных тегов (4) в один JSON (шаг 5). Пути по умолчанию — из этапов 1 и 4.",
)
parser.add_argument(
"--merge",
type=Path,
default=DEFAULT_ANALYSIS,
help=f"Путь к merge.json с анализом (по умолчанию: {DEFAULT_ANALYSIS})",
)
parser.add_argument(
"--tags",
type=Path,
default=DEFAULT_TAGS,
help=f"Путь к validated_tags.json (по умолчанию: {DEFAULT_TAGS})",
)
parser.add_argument(
"--input-chapter",
type=Path,
default=DEFAULT_CHAPTER,
help=f"Путь к вход_главы.json для метаданных главы/книги (по умолчанию: {DEFAULT_CHAPTER})",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"Путь к выходному JSON (по умолчанию: {DEFAULT_OUTPUT})",
)
parser.add_argument(
"--no-chapter",
action="store_true",
help="Не подставлять метаданные главы из --input-chapter.",
)
args = parser.parse_args()
if not args.merge.is_file():
print(f"Файл не найден: {args.merge}", file=sys.stderr)
return 1
if not args.tags.is_file():
print(f"Файл не найден: {args.tags}", file=sys.stderr)
return 1
print("Загрузка merge.json и validated_tags.json...")
try:
analysis = load_json(args.merge)
tags_doc = load_json(args.tags)
except json.JSONDecodeError as e:
print(f"Ошибка разбора JSON: {e}", file=sys.stderr)
return 1
input_chapter = None
if not args.no_chapter and args.input_chapter.is_file():
try:
input_chapter = load_json(args.input_chapter)
except json.JSONDecodeError as e:
print(f"Ошибка разбора input-chapter JSON: {e}", file=sys.stderr)
return 1
elif not args.no_chapter and not args.input_chapter.is_file():
print(f"Файл не найден (метаданные главы не добавлены): {args.input_chapter}", file=sys.stderr)
merged = merge_analysis_and_tags(analysis, tags_doc, input_chapter)
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(merged, f, ensure_ascii=False, indent=2)
print(f"Записано: {args.output}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -30,6 +30,29 @@
Ограничения и limitations в эмбеддинг по умолчанию не включаются (опционально — в конфиге). Теги — только в payload, не в тексте для эмбеддинга.
## Использование
## Скрипт через Ollama
`run_embed_ollama.py` — один вызов Ollama `/api/embed` для генерации вектора по JSON шага 5. Текст для эмбеддинга собирается из framework, insights, application по `embed_input_spec.txt` (функция `merged_json_to_embed_text` из `embed_cli.py`).
**Вход (по умолчанию):**
- `../5_мерж_анализа_и_тегов/merged_with_tags.json` — результат шага 5 (анализ + теги)
**Выход:** `embedding.json` в каталоге скрипта — массив float (вектор размерности 1024 для bge-m3).
**Запуск:**
```bash
cd 6_генерация_эмбеддингов
python3 run_embed_ollama.py
# с указанием путей:
python3 run_embed_ollama.py --merged /path/to/merged_with_tags.json -o embedding.json
# другая модель или URL Ollama:
python3 run_embed_ollama.py --model bge-m3 --ollama-url http://localhost:11434
```
## Универсальный CLI (OpenAI-совместимый API)
`embed_cli.py` — для LM Studio или другого OpenAI-совместимого API (позиционные аргументы: путь к JSON, имя модели, опционально `--base-url`, `-o`).
## Использование в пайплайне
Вызывается после шага 5 (мерж анализа и тегов). Модель задаётся конфигом (env/конфиг); смена модели не меняет формат хранения в Qdrant, при смене — пересчёт эмбеддингов по необходимости.

View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Генерация эмбеддинга по JSON шага 5 через Ollama (шаг 6).
Текст для эмбеддинга собирается из framework, insights, application по embed_input_spec.txt.
Вход по умолчанию: merged_with_tags.json (5). Выход по умолчанию: embedding.json (вектор).
"""
import argparse
import json
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
DIR = Path(__file__).resolve().parent
DEFAULT_MERGED = DIR.parent / "5_мерж_анализа_и_тегов" / "merged_with_tags.json"
DEFAULT_OUTPUT = DIR / "embedding.json"
OLLAMA_URL = "http://localhost:11434"
EMBED_MODEL = "bge-m3"
def get_embedding_ollama(base_url: str, model: str, text: str) -> list[float]:
"""
Запрашивает эмбеддинг текста у Ollama API (POST /api/embed).
Args:
base_url: Базовый URL Ollama (например http://localhost:11434).
model: Имя модели эмбеддингов (например bge-m3).
text: Текст для эмбеддинга.
Returns:
Вектор эмбеддинга (список float).
Raises:
urllib.error.HTTPError: При ошибке HTTP.
ValueError: Если в ответе нет ожидаемой структуры.
"""
url = f"{base_url.rstrip('/')}/api/embed"
payload = {"model": model, "input": text}
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
url,
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as resp:
data: dict[str, Any] = json.loads(resp.read().decode("utf-8"))
if "embeddings" not in data or not data["embeddings"]:
raise ValueError("В ответе Ollama нет поля embeddings")
embedding = data["embeddings"][0]
if not isinstance(embedding, list):
raise ValueError("embeddings[0] не является массивом")
return [float(x) for x in embedding]
def main() -> int:
"""Собирает текст из merged JSON, вызывает Ollama /api/embed, пишет вектор в файл."""
from embed_cli import merged_json_to_embed_text
parser = argparse.ArgumentParser(
description="Эмбеддинг по JSON шага 5 через Ollama (шаг 6). На выход — вектор (JSON).",
)
parser.add_argument(
"--merged",
type=Path,
default=DEFAULT_MERGED,
help=f"Путь к merged_with_tags.json (по умолчанию: {DEFAULT_MERGED})",
)
parser.add_argument(
"--model",
default=EMBED_MODEL,
help=f"Модель эмбеддингов в Ollama (по умолчанию: {EMBED_MODEL})",
)
parser.add_argument(
"--ollama-url",
default=OLLAMA_URL,
help=f"URL Ollama (по умолчанию: {OLLAMA_URL})",
)
parser.add_argument(
"-o",
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help=f"Путь к выходному JSON с вектором (по умолчанию: {DEFAULT_OUTPUT})",
)
args = parser.parse_args()
if not args.merged.is_file():
print(f"Файл не найден: {args.merged}", file=sys.stderr)
return 1
print("Загрузка merged_with_tags.json...")
try:
with open(args.merged, encoding="utf-8") as f:
merged = json.load(f)
except json.JSONDecodeError as e:
print(f"Ошибка разбора JSON: {e}", file=sys.stderr)
return 1
text = merged_json_to_embed_text(merged)
if not text:
print(
"Ошибка: текст для эмбеддинга пуст (нет framework/insights/application).",
file=sys.stderr,
)
return 1
print(f"Вызов Ollama {args.model} — генерация эмбеддинга...")
t0 = time.monotonic()
try:
vector = get_embedding_ollama(args.ollama_url, args.model, text)
except urllib.error.HTTPError as e:
print(f"Ошибка HTTP {e.code}: {e.reason}", file=sys.stderr)
if e.fp:
try:
body = e.fp.read().decode("utf-8")
print(body[:500], file=sys.stderr)
except Exception:
pass
return 1
except urllib.error.URLError as e:
print(f"Ошибка запроса: {e.reason}", file=sys.stderr)
return 1
except ValueError as e:
print(f"Ошибка: {e}", file=sys.stderr)
return 1
elapsed = time.monotonic() - t0
print(f"Эмбеддинг получен за {elapsed:.1f} сек, размерность {len(vector)}")
args.output.parent.mkdir(parents=True, exist_ok=True)
with open(args.output, "w", encoding="utf-8") as f:
json.dump(vector, f, ensure_ascii=False)
print(f"Записано: {args.output}")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -14,11 +14,17 @@
## Использование
Вектор — JSON-массив чисел из шага 6: файл `embedding.json` (Ollama, `run_embed_ollama.py`) или `vector.json` / любой другой файл с массивом, либо stdin.
```bash
# Вектор из файла (merged должен содержать book_id, chapter_id, chapter_number, chapter_title из шага 5 с --input-chapter)
# Вариант 1: после run_embed_ollama.py (шаг 6 через Ollama)
python3 save_to_qdrant.py 5_мерж_анализа_и_тегов/merged_with_tags.json 6_генерация_эмбеддингов/embedding.json [--validation-score 0.95]
# Вариант 2: вектор в любом файле (например vector.json)
python3 save_to_qdrant.py merged_with_tags.json vector.json [--validation-score 0.95]
# Вектор из stdin (пайплайн с шагом 6)
# Вектор из stdin (пайплайн с шагом 6 через embed_cli.py)
python3 6_генерация_эмбеддингов/embed_cli.py merged_with_tags.json model_name \
| python3 7_сохранение_qdrant/save_to_qdrant.py merged_with_tags.json -
```

42
check_postgres_schema.py Normal file
View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Проверка наличия таблиц схемы (books, chapters, chapter_analyses, tags, chapter_tags) в Postgres."""
import sys
try:
import psycopg2
except ImportError:
print("Установите: pip install psycopg2-binary")
sys.exit(1)
HOST = "192.168.88.15"
PORT = 5432
USER = "n8n"
PASSWORD = "n8n_password"
DB = "n8n"
EXPECTED = {"books", "chapters", "chapter_analyses", "tags", "chapter_tags"}
try:
conn = psycopg2.connect(
host=HOST, port=PORT, user=USER, password=PASSWORD, dbname=DB
)
with conn.cursor() as cur:
cur.execute("""
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
ORDER BY table_name;
""")
found = {row[0] for row in cur.fetchall()}
conn.close()
print(f"Postgres {HOST}:{PORT}/{DB}")
print("Таблицы в public:", ", ".join(sorted(found)) if found else "(нет)")
missing = EXPECTED - found
extra = found - EXPECTED
if not missing:
print("Ожидаемые таблицы этапа 8: все на месте.")
else:
print("Не найдены:", ", ".join(sorted(missing)))
if extra:
print("Доп. таблицы:", ", ".join(sorted(extra)))
except Exception as e:
print(f"Ошибка: {e}")
sys.exit(1)