fix
This commit is contained in:
@@ -19,8 +19,8 @@ DIR = Path(__file__).resolve().parent
|
|||||||
|
|
||||||
OLLAMA_OPTIONS = {
|
OLLAMA_OPTIONS = {
|
||||||
"temperature": 0.3,
|
"temperature": 0.3,
|
||||||
"num_ctx": 8500,
|
"num_ctx": 9000,
|
||||||
"num_predict": 4096,
|
"num_predict": 8500,
|
||||||
"repeat_penalty": 1.1,
|
"repeat_penalty": 1.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,3 +22,27 @@
|
|||||||
- `{limitations_json}` — JSON блока `limitations`
|
- `{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
|
||||||
|
```
|
||||||
|
|||||||
236
2_валидация_анализа_по_блокам/run_validation_ollama.py
Normal file
236
2_валидация_анализа_по_блокам/run_validation_ollama.py
Normal 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())
|
||||||
@@ -14,7 +14,27 @@
|
|||||||
- `{chapter_title}` — название главы
|
- `{chapter_title}` — название главы
|
||||||
- `{full_analysis_json}` — полный JSON анализа (склейка блоков framework + insights + application + limitations)
|
- `{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` по главе; опционально — «примиряющий» промпт для минимальных правок.
|
Вызывается после шага 2a (склейка блоков). При вердикте bad или needs_review — флаг `needs_review` по главе; опционально — «примиряющий» промпт для минимальных правок.
|
||||||
|
|
||||||
|
|||||||
@@ -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 раз/день до 1–2 раз/день за 2 недели"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"limitations": [
|
|
||||||
{
|
|
||||||
"description": "Идеи не работают, если триггер (то, что в окружении запускает действие) не устранён или остаётся доступным, так как привычка может вернуться при появлении стимула",
|
|
||||||
"when_relevant": "Когда человек не полностью убирает триггер из своей среды или сталкивается с ним в новой обстановке",
|
|
||||||
"example": "Человек удалил сигареты из дома, но снова начинает курить на работе среди коллег-курильщиков"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Техники могут не сработать, если негативные эмоции (стресс, тревога) остаются необратленными и усиливают нежелательное поведение",
|
|
||||||
"when_relevant": "Когда человек продолжает испытывать сильный стресс или эмоциональные трудности, не решая их на уровне среды",
|
|
||||||
"example": "Человек убрал телефон из комнаты, но всё равно переедает перед сном из-за хронической тревоги"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Изменения среды могут быть недостаточны, если привычка закреплена на уровне автоматических нейронных паттернов и не сопровождается новыми, здоровыми привычками",
|
|
||||||
"when_relevant": "Когда человек устраняет триггер, но не вводит альтернативное поведение для подкрепления",
|
|
||||||
"example": "Человек убрал телевизор из спальни, но не нашёл замену в виде чтения или медитации и проводит время впустую"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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 раз/день до 1–2 раз/день за 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": []
|
|
||||||
}
|
|
||||||
185
2b_финальная_валидация_согласованности/run_consistency_ollama.py
Normal file
185
2b_финальная_валидация_согласованности/run_consistency_ollama.py
Normal 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())
|
||||||
@@ -27,6 +27,29 @@
|
|||||||
|
|
||||||
JSON с тегами по категориям (ключи на английском: `principle`, `psychology`, `method`, `result`, `context`) и confidence score для каждого тега; при необходимости — кандидаты в `proposed` с полем `category` из того же набора. Маппинг категорий: ARCHITECTURE_SUMMARY.md → раздел «Хранение данных» → таблица `tags`.
|
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`).
|
||||||
|
|||||||
31
3_извлечение_тегов/allowed_tags.example.json
Normal file
31
3_извлечение_тегов/allowed_tags.example.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"principle": [
|
||||||
|
"среда формирует поведение",
|
||||||
|
"привычки не исчезают",
|
||||||
|
"самоконтроль краткосрочен",
|
||||||
|
"дисциплина через среду",
|
||||||
|
"негативные эмоции усиливают поведение"
|
||||||
|
],
|
||||||
|
"psychology": [
|
||||||
|
"стимул",
|
||||||
|
"привычка",
|
||||||
|
"желание вызванное стимулом",
|
||||||
|
"аутокаталитический процесс",
|
||||||
|
"подкрепление"
|
||||||
|
],
|
||||||
|
"method": [
|
||||||
|
"снижение доступности стимула",
|
||||||
|
"настройка среды",
|
||||||
|
"устранение триггеров"
|
||||||
|
],
|
||||||
|
"result": [
|
||||||
|
"уменьшение вредных привычек",
|
||||||
|
"устойчивое поведение",
|
||||||
|
"снижение искушений"
|
||||||
|
],
|
||||||
|
"context": [
|
||||||
|
"поведенческая психология",
|
||||||
|
"привычки",
|
||||||
|
"атомные привычки"
|
||||||
|
]
|
||||||
|
}
|
||||||
224
3_извлечение_тегов/run_extract_tags_ollama.py
Normal file
224
3_извлечение_тегов/run_extract_tags_ollama.py
Normal 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())
|
||||||
@@ -26,6 +26,27 @@
|
|||||||
|
|
||||||
JSON: проверенные теги по категориям с обновлёнными confidence; теги, снятые при валидации, — в блоке `removed` (tag, category, reason). Формат `tags` совпадает с выходом шага 3 для передачи в эмбеддинг и сохранение в БД.
|
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 (или аналог). Время: ~20–30 сек на главу.
|
Вызывается после шага 3 (извлечение тегов). Модель: qwen3-14b:8bit (или аналог). Время: ~20–30 сек на главу.
|
||||||
|
|||||||
216
4_валидация_тегов/run_validate_tags_ollama.py
Normal file
216
4_валидация_тегов/run_validate_tags_ollama.py
Normal 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())
|
||||||
@@ -14,16 +14,37 @@
|
|||||||
|
|
||||||
Один JSON: все поля анализа + `tags`, `removed`. Если задан `--input-chapter`, в выход добавляются метаданные главы/книги (book_id, chapter_id, chapter_number, chapter_title, book_title, author). Этот документ передаётся в шаг 6 (генерация эмбеддингов) и в шаг 7 (payload Qdrant).
|
Один 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 и т.д.; подставляются в выход для шагов 6–8)
|
||||||
|
|
||||||
|
**Выход:** `merged_with_tags.json` в каталоге скрипта (или путь через `-o`).
|
||||||
|
|
||||||
|
**Запуск:**
|
||||||
```bash
|
```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
|
```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.
|
Без `-o` результат выводится в stdout.
|
||||||
|
|||||||
95
5_мерж_анализа_и_тегов/run_merge_analysis_tags.py
Normal file
95
5_мерж_анализа_и_тегов/run_merge_analysis_tags.py
Normal 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())
|
||||||
@@ -30,6 +30,29 @@
|
|||||||
|
|
||||||
Ограничения и limitations в эмбеддинг по умолчанию не включаются (опционально — в конфиге). Теги — только в payload, не в тексте для эмбеддинга.
|
Ограничения и 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, при смене — пересчёт эмбеддингов по необходимости.
|
Вызывается после шага 5 (мерж анализа и тегов). Модель задаётся конфигом (env/конфиг); смена модели не меняет формат хранения в Qdrant, при смене — пересчёт эмбеддингов по необходимости.
|
||||||
|
|||||||
142
6_генерация_эмбеддингов/run_embed_ollama.py
Normal file
142
6_генерация_эмбеддингов/run_embed_ollama.py
Normal 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())
|
||||||
@@ -14,11 +14,17 @@
|
|||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
|
Вектор — JSON-массив чисел из шага 6: файл `embedding.json` (Ollama, `run_embed_ollama.py`) или `vector.json` / любой другой файл с массивом, либо stdin.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Вектор из файла (merged должен содержать book_id, chapter_id, chapter_number, chapter_title из шага 5 с --input-chapter)
|
# Вектор из файла (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]
|
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 6_генерация_эмбеддингов/embed_cli.py merged_with_tags.json model_name \
|
||||||
| python3 7_сохранение_qdrant/save_to_qdrant.py merged_with_tags.json -
|
| python3 7_сохранение_qdrant/save_to_qdrant.py merged_with_tags.json -
|
||||||
```
|
```
|
||||||
|
|||||||
42
check_postgres_schema.py
Normal file
42
check_postgres_schema.py
Normal 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)
|
||||||
Reference in New Issue
Block a user