Budowa chatbota CLI
4. Baza wiedzy i metadane
4.1. Własne pliki TXT i Markdown
Lokalne chatboty, agenci i systemy RAG z własną bazą wiedzy
Instrukcja krok po kroku · materiał uzupełniający
Budujemy chatbota w konsoli
baza.py
Utwórz prosty moduł przygotowania danych. W pierwszym przejściu zapisz ten plik, ale zachowaj dostarczoną bazę. Własne dokumenty podłączysz po sprawdzeniu całej aplikacji.
Utwórz baza.py
Kod zawiera dwie ścieżki: kontrolę istniejącego JSONL oraz podział plików TXT/MD według nagłówków ## i okien znakowych. Kopiuj cały plik:
"""Odczyt zatwierdzonych fragmentów i prosty podział własnych plików TXT/MD."""
import argparse
import hashlib
import json
import re
from pathlib import Path
from lokalny import ROOT
CHUNKS = ROOT / "dane" / "chunks.jsonl"
def read_chunks(path=CHUNKS):
records = [json.loads(line) for line in Path(path).read_text(
encoding="utf-8-sig").splitlines() if line.strip()]
if not records:
raise ValueError("Baza jest pusta.")
seen = set()
for record in records:
if not isinstance(record.get("chunk_id"), str) or not record["chunk_id"].strip():
raise ValueError("Brak identyfikatora fragmentu.")
if record["chunk_id"] in seen:
raise ValueError("Powtórzony chunk_id: " + record["chunk_id"])
seen.add(record["chunk_id"])
if not isinstance(record.get("text"), str) or not record["text"].strip():
raise ValueError("Pusty tekst fragmentu.")
meta = record.get("metadata", {})
for key in ("document_id", "source", "title", "version", "status"):
if not isinstance(meta.get(key), str) or not meta[key].strip():
raise ValueError("Brak metadanych: " + key)
if meta["status"] != "active":
raise ValueError("Baza zawiera dokument nieaktywny: " + meta["source"])
return records
def split_sections(text, size=900, overlap=150):
section = "Początek dokumentu"
for part in re.split(r"(?m)^(## .+)$", text.replace("\r\n", "\n")):
if part.startswith("## "):
section = part[3:].strip()
continue
part = part.strip()
start = 0
while start < len(part):
end = min(start + size, len(part))
# Staraj się kończyć na granicy wyrazu, bez utraty znaków.
if end < len(part):
boundary = part.rfind(" ", start + size // 2, end)
if boundary > start:
end = boundary
yield section, part[start:end].strip()
if end == len(part):
break
start = end - overlap
def prepare(folder, version):
files = sorted(p for p in Path(folder).iterdir()
if p.is_file() and p.suffix.lower() in (".txt", ".md"))
if not files:
raise ValueError("Brak plików TXT/MD w wybranym folderze.")
records = []
for file in files:
text = file.read_text(encoding="utf-8-sig")
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:10]
doc = file.name
for i, (section, fragment) in enumerate(split_sections(text), 1):
if not fragment:
continue
records.append({"chunk_id": f"{doc}-{digest}-{i:04d}", "text": fragment,
"metadata": {"document_id": doc, "source": file.name,
"title": file.stem, "version": version, "status": "active",
"section": section, "page_number": None}})
if not records:
raise ValueError("W plikach nie znaleziono treści.")
return records
def main():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--folder", type=Path)
group.add_argument("--import-m2", type=Path)
parser.add_argument("--version", default="1.0")
args = parser.parse_args()
if CHUNKS.exists():
raise FileExistsError("chunks.jsonl już istnieje. Pracuj w nowej kopii projektu "
"albo zmień nazwę dotychczasowego pliku na chunks_poprzednie.jsonl.")
records = read_chunks(args.import_m2) if args.import_m2 else prepare(args.folder, args.version)
CHUNKS.parent.mkdir(parents=True, exist_ok=True)
with CHUNKS.open("x", encoding="utf-8") as output:
for record in records:
output.write(json.dumps(record, ensure_ascii=False) + "\n")
print("Zapisano fragmentów:", len(records))
print("Przejrzyj teksty i metadane przed budowaniem indeksu:", CHUNKS)
if __name__ == "__main__":
main()Jak użyć go do własnych dokumentów
- Pracuj na kolejnej kopii projektu. Dotychczasowe
dane/chunks.jsonlzachowaj pod nazwąchunks_docassist.jsonl. Jeśli istniejeindex.json, zachowaj go jakoindex_docassist.json. - Utwórz folder
moje_dokumentyi umieść w nim 2–5 krótkich plików TXT lub MD zapisanych w UTF-8. Użyj jednej aktualnej wersji każdego dokumentu. Program nie przeszukuje podfolderów. - Dodaj nagłówki
##i zachowaj kompletne procedury wraz z wyjątkami. Ustal wspólną wersję tego małego zestawu. - Uruchom:
.\.venv\Scripts\python.exe baza.py --folder moje_dokumenty --version 1.0Program tworzy rekordy z limitem około 900 znaków i nakładaniem 150 znaków wewnątrz sekcji. Nie jest to podział tokenowy. Może przeciąć zdanie lub procedurę, więc przejrzyj wynik. Status active jest tu Twoją deklaracją wynikającą z doboru plików, a nie automatyczną oceną ich aktualności.
Warto wiedzieć. Ten wariant nadaje się do niewielkiej, świadomie wybranej bazy. Jeśli dokumenty mają różne wersje, strony, złożone tabele lub wymagają ekstrakcji PDF/DOCX/HTML, przygotuj je ścieżką M2 opisaną w kolejnym podrozdziale. Nie zmieniaj rozszerzenia PDF na TXT.
Punkt kontrolny
- Po użyciu własnych plików widzisz niepuste fragmenty i poprawne sekcje.
- Umiesz wskazać miejsce, w którym długi warunek mógł zostać przecięty.
- Po zmianie bazy planujesz budowę nowego indeksu.
Instrukcja uczestnika · Python i Ollama · wydanie 29.09.2026