Files
homelab-docs/homelab/paperless-ollama-ask.py
2026-02-23 16:47:17 +03:00

143 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Спрашивает по документам Paperless-ngx через поиск по OCR и отвечает через Ollama.
Использование: PAPERLESS_URL=... PAPERLESS_TOKEN=... python paperless-ollama-ask.py "номер паспорта?"
"""
import json
import os
import sys
import urllib.parse
import urllib.request
def env(name: str, default: str = "") -> str:
v = os.environ.get(name, default).strip()
if not v and name.startswith("PAPERLESS_"):
raise SystemExit(f"Set {name} (e.g. PAPERLESS_URL=http://192.168.1.104:8000 PAPERLESS_TOKEN=...)")
return v
# Слова, которые редко есть в OCR тексте документов — выкидываем при запасном поиске
_STOP_WORDS = frozenset(
"кем какой какая какие как где когда что кто чей чья чьи откуда куда зачем почему сколько".split()
)
def search_query_for_paperless(question: str) -> str:
"""Убираем пунктуацию, которая может ломать full-text поиск Paperless (?, ! и т.д.)."""
s = question.strip().rstrip("?!.;")
return s if s else question
def fallback_search_query(question: str, max_words: int = 4) -> str:
"""Короткий запрос для повторного поиска: ключевые слова без вопросительных слов."""
q = search_query_for_paperless(question)
words = [w for w in q.split() if len(w) >= 2 and w.lower() not in _STOP_WORDS]
return " ".join(words[:max_words]) if words else q
def paperless_search(base_url: str, token: str, query: str, max_docs: int = 5) -> list[dict]:
"""Full-text поиск по документам, возвращает список с полем content (OCR)."""
base_url = base_url.rstrip("/")
search_q = search_query_for_paperless(query)
query_encoded = urllib.parse.quote(search_q, encoding="utf-8", safe="")
url = f"{base_url}/api/documents/?query={query_encoded}&page_size={max_docs}"
req = urllib.request.Request(url)
req.add_header("Authorization", f"Token {token}")
req.add_header("Accept", "application/json; version=6")
if os.environ.get("PAPERLESS_DEBUG"):
print(f"[DEBUG] GET {url}", file=sys.stderr)
with urllib.request.urlopen(req, timeout=30) as r:
data = r.read().decode("utf-8")
out = json.loads(data)
results = out.get("results") or []
if os.environ.get("PAPERLESS_DEBUG"):
print(f"[DEBUG] count={out.get('count', 0)} results={len(results)}", file=sys.stderr)
return results
def ollama_generate(ollama_url: str, model: str, prompt: str, timeout: int = 120) -> str:
"""Один запрос к Ollama /api/generate, возвращает response."""
ollama_url = ollama_url.rstrip("/")
url = f"{ollama_url}/api/generate"
body = {"model": model, "prompt": prompt, "stream": False}
data = json.dumps(body).encode()
req = urllib.request.Request(url, data=data, method="POST")
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=timeout) as r:
out = json.loads(r.read().decode("utf-8"))
return (out.get("response") or "").strip()
def main():
question = " ".join(sys.argv[1:]).strip()
if not question:
print("Usage: PAPERLESS_URL=... PAPERLESS_TOKEN=... python paperless-ollama-ask.py 'ваш вопрос'", file=sys.stderr)
sys.exit(1)
base_url = env("PAPERLESS_URL", "http://localhost:8000")
token = env("PAPERLESS_TOKEN")
ollama_url = env("OLLAMA_URL", "http://localhost:11434")
model = env("OLLAMA_MODEL", "saiga")
max_docs = int(env("PAPERLESS_MAX_DOCS", "5"))
# Поиск по OCR: сначала полный вопрос, при пустом ответе — по ключевым словам
try:
docs = paperless_search(base_url, token, question, max_docs=max_docs)
if not docs:
fallback = fallback_search_query(question)
if fallback and fallback != search_query_for_paperless(question):
docs = paperless_search(base_url, token, fallback, max_docs=max_docs)
if os.environ.get("PAPERLESS_DEBUG") and docs:
print(f"[DEBUG] fallback query '{fallback}' found {len(docs)} docs", file=sys.stderr)
except urllib.error.HTTPError as e:
print(f"Paperless API error: {e.code} {e.reason}", file=sys.stderr)
if e.code == 401:
print("Check PAPERLESS_TOKEN (My Profile → API token in Paperless UI).", file=sys.stderr)
sys.exit(2)
except Exception as e:
print(f"Paperless request failed: {e}", file=sys.stderr)
sys.exit(2)
if not docs:
print("По документам ничего не найдено.")
return
# Собираем текст из документов (OCR content)
parts = []
for i, d in enumerate(docs, 1):
title = d.get("title") or f"Документ {d.get('id')}"
content = (d.get("content") or "").strip()
if not content:
content = "(текст пустой)"
parts.append(f"[Документ {i}: {title}]\n{content[:8000]}") # ограничиваем длину
docs_text = "\n\n---\n\n".join(parts)
prompt = f"""Ниже текст из документов (OCR). Ответь на вопрос пользователя, опираясь только на этот текст.
Если в тексте нет ответа — напиши "В документах не найдено".
Отвечай кратко, по делу.
Документы:
{docs_text}
Вопрос: {question}
Ответ:"""
try:
answer = ollama_generate(ollama_url, model, prompt)
except urllib.error.URLError as e:
print(f"Ollama недоступна ({ollama_url}): {e}", file=sys.stderr)
sys.exit(3)
except Exception as e:
print(f"Ollama error: {e}", file=sys.stderr)
sys.exit(3)
print(answer)
if __name__ == "__main__":
main()