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

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