Compare commits
14 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
61fbc79211 | |
|
|
d6e1f45686 | |
|
|
9bbe7188ca | |
|
|
3d477870ad | |
|
|
a5e5401548 | |
|
|
4f486f021b | |
|
|
d9541a9a28 | |
|
|
e3a94fa5ae | |
|
|
cd5fab2206 | |
|
|
a47fc31bda | |
|
|
2bc3384235 | |
|
|
049b4703a8 | |
|
|
4315cef3c7 | |
|
|
9f367c2fa4 |
30
Dockerfile
30
Dockerfile
|
|
@ -1,30 +0,0 @@
|
||||||
# Użyj oficjalnego obrazu Python jako bazowego
|
|
||||||
FROM --platform=linux/amd64 python:3.9-slim
|
|
||||||
|
|
||||||
# Ustaw katalog roboczy w kontenerze
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Zainstaluj git
|
|
||||||
RUN apt-get update && apt-get install -y git nano wget curl iputils-ping
|
|
||||||
|
|
||||||
# Skopiuj pliki wymagań (jeśli istnieją) i zainstaluj zależności
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Skopiuj plik requirements.txt do kontenera
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# Zainstaluj zależności z pliku requirements.txt
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Zainstaluj Tesseract OCR
|
|
||||||
RUN apt-get install -y tesseract-ocr
|
|
||||||
|
|
||||||
# Skopiuj kod źródłowy do kontenera
|
|
||||||
COPY . .
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
# Uruchom aplikację
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||||
|
|
||||||
|
model = AutoModelForSeq2SeqLM.from_pretrained("allegro/multislav-5lang")
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained("allegro/multislav-5lang")
|
||||||
|
|
||||||
|
model.save_pretrained("./models/ably")
|
||||||
|
tokenizer.save_pretrained("./models/ably")
|
||||||
|
|
||||||
|
print("✅ Model został wytrenowany i zapisany!")
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"kodekspracy": "Kodeks Pracy",
|
||||||
|
"urlopproporcjonalny": "Rozporządzenie BHP",
|
||||||
|
"ustawaopanstwowejinspekcjipracy": "Ustawa o Państwowej inspekcji pracy"
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,11 @@
|
||||||
|
Podstawowe zasady naliczania urlopu proporcjonalnego
|
||||||
|
|
||||||
|
Kalendarzowy miesiąc pracy odpowiada 1/12 wymiaru urlopu wypoczynkowego, który przysługuje pracownikowi na podstawie art. 154 § 1 i 2 k.p. To oznacza, że 1 kalendarzowy miesiąc pracy to 1/12 z 20 dni (1,66) lub 26 dni (2,16) urlopu wypoczynkowego dla pracownika na pełnym etacie. Niektórzy zaokrąglają wyniki do 1,67 dnia urlopu i 2,17 dnia urlopu.
|
||||||
|
|
||||||
|
Niepełny kalendarzowy miesiąc pracy zaokrągla się w górę do pełnego miesiąca. Jeżeli pracownik przepracuje tylko 1 dzień w miesiącu, zyska prawo do urlopu za cały miesiąc.
|
||||||
|
|
||||||
|
Niepełny dzień urlopu zaokrągla się w górę do pełnego dnia. Uwaga – nie musisz tak postąpić w przypadku urlopu liczonego proporcjonalnie dla osoby, która podjęła pierwszą pracę w życiu.
|
||||||
|
|
||||||
|
Zaokrąglając niepełne dni urlopu, pamiętaj, że wymiar urlopu wypoczynkowego należny pracownikowi pełnoetatowemu w danym roku kalendarzowym nie może przekroczyć 20 lub 26 dni (w zależności od stażu pracy).
|
||||||
|
|
||||||
|
Jeśli pracownik rozwiązuje umowę o pracę z dotychczasowym pracodawcą i zawiera nową umowę o pracę z kolejnym pracodawcą w tym samym miesiącu kalendarzowym, to tylko wcześniejszy pracodawca zaokrągla ten niepełny miesiąc pracy w górę.
|
||||||
Binary file not shown.
|
|
@ -1,2 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
python /app/ollama_service.py
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import os
|
||||||
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
|
||||||
|
import faiss
|
||||||
|
import numpy as np
|
||||||
|
import ollama
|
||||||
|
import gradio as gr
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
|
||||||
|
# === KONFIGURACJA ===
|
||||||
|
model_name = "hse.ably.do:latest" # Nazwa modelu Ollama
|
||||||
|
faiss_index_path = "faiss_index.idx" # Plik indeksu FAISS
|
||||||
|
kodeks_file = "/home/ably.do/docs/kodekspracy.txt" # Plik z treścią kodeksu pracy
|
||||||
|
embedding_model = SentenceTransformer("all-MiniLM-L6-v2") # Model do embedowania tekstu
|
||||||
|
|
||||||
|
# === KROK 1: WCZYTYWANIE KODEKSU PRACY ===
|
||||||
|
def load_kodeks(filepath):
|
||||||
|
with open(filepath, "r", encoding="utf-8") as file:
|
||||||
|
content = file.read()
|
||||||
|
articles = content.split("\n\n") # Dzielimy na sekcje
|
||||||
|
return [article.strip() for article in articles if article.strip().startswith("Art.")]
|
||||||
|
|
||||||
|
# === KROK 2: TWORZENIE INDEKSU FAISS ===
|
||||||
|
def create_faiss_index(sections):
|
||||||
|
embeddings = embedding_model.encode(sections, convert_to_numpy=True) # Tworzenie wektorów
|
||||||
|
index = faiss.IndexFlatL2(embeddings.shape[1]) # Indeks FAISS
|
||||||
|
index.add(embeddings) # Dodanie wektorów do FAISS
|
||||||
|
faiss.write_index(index, faiss_index_path) # Zapis indeksu
|
||||||
|
return index, sections
|
||||||
|
|
||||||
|
# === KROK 3: WYSZUKIWANIE NAJBLIŻSZEGO FRAGMENTU ===
|
||||||
|
def search_faiss(query, index, sections, top_k=3):
|
||||||
|
query_vector = embedding_model.encode([query], convert_to_numpy=True)
|
||||||
|
_, idx = index.search(query_vector, top_k) # Szukamy więcej wyników
|
||||||
|
|
||||||
|
results = [sections[i] for i in idx[0] if i < len(sections)]
|
||||||
|
return "\n\n".join(results) # Połącz kilka najlepszych fragmentów
|
||||||
|
|
||||||
|
# === KROK 4: GENEROWANIE ODPOWIEDZI Z OLLAMA ===
|
||||||
|
def generate_response(user_query):
|
||||||
|
if not os.path.exists(faiss_index_path):
|
||||||
|
return "Błąd: Indeks FAISS nie istnieje. Uruchom aplikację z opcją --rebuild-index."
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = faiss.read_index(faiss_index_path)
|
||||||
|
except Exception as e:
|
||||||
|
return f"Błąd ładowania FAISS: {str(e)}"
|
||||||
|
|
||||||
|
sections = load_kodeks(kodeks_file)
|
||||||
|
best_match = search_faiss(user_query, index, sections)
|
||||||
|
|
||||||
|
# 👀 DEBUG: Sprawdź, co zwraca FAISS
|
||||||
|
print(f"🔍 Najlepsze dopasowanie FAISS dla '{user_query}':\n{best_match}")
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
Odpowiedz na pytanie na podstawie następującego tekstu:
|
||||||
|
|
||||||
|
{best_match}
|
||||||
|
|
||||||
|
Pytanie: {user_query}
|
||||||
|
Podaj dokładny tekst artykułu, jeśli go znajdziesz w treści powyżej.
|
||||||
|
"""
|
||||||
|
|
||||||
|
response = ollama.chat(model=model_name, messages=[{"role": "user", "content": prompt}])
|
||||||
|
|
||||||
|
print(f"📝 Odpowiedź modelu:\n{response}") # 👀 DEBUG: Sprawdź odpowiedź Ollama
|
||||||
|
|
||||||
|
return response.get("message", response.get("content", "Błąd: Nie udało się wygenerować odpowiedzi."))
|
||||||
|
|
||||||
|
# === KROK 5: INTERFEJS WEBOWY ===
|
||||||
|
iface = gr.Interface(
|
||||||
|
fn=generate_response,
|
||||||
|
inputs=gr.Textbox(label="Zadaj pytanie o kodeks pracy"),
|
||||||
|
outputs=gr.Textbox(label="Odpowiedź"),
|
||||||
|
title="Asystent Kodeksu Pracy",
|
||||||
|
description="Wpisz pytanie, a system zwróci odpowiedni fragment kodeksu pracy."
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--rebuild-index", action="store_true", help="Odbudowanie indeksu FAISS")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.rebuild_index or not os.path.exists(faiss_index_path):
|
||||||
|
print("Tworzenie nowego indeksu FAISS...")
|
||||||
|
sections = load_kodeks(kodeks_file)
|
||||||
|
create_faiss_index(sections)
|
||||||
|
else:
|
||||||
|
print("Indeks FAISS już istnieje.")
|
||||||
|
|
||||||
|
iface.launch(share=True)
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import os
|
||||||
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import faiss
|
||||||
|
import numpy as np
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
from datasets import Dataset
|
||||||
|
from peft import LoraConfig, get_peft_model
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForLanguageModeling
|
||||||
|
|
||||||
|
# 1️⃣ Inicjalizacja modelu do embeddingów
|
||||||
|
embed_model = SentenceTransformer("all-MiniLM-L6-v2")
|
||||||
|
|
||||||
|
# 2️⃣ Dodanie dokumentów i embeddingów
|
||||||
|
def read_documents_from_file(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
articles = content.split('\n\n')
|
||||||
|
documents = []
|
||||||
|
for article in articles:
|
||||||
|
if article.strip().startswith('Art.'):
|
||||||
|
documents.append(article.strip())
|
||||||
|
return documents
|
||||||
|
#documents = [
|
||||||
|
# "Jak założyć firmę w Polsce?",
|
||||||
|
# "Jak rozliczyć podatek VAT?",
|
||||||
|
# "Procedura składania reklamacji w e-sklepie.",
|
||||||
|
# "Jakie dokumenty są potrzebne do rejestracji działalności?"
|
||||||
|
#]
|
||||||
|
file_path = './docs/kodekspracy.txt' # Zmień na właściwą ścieżkę
|
||||||
|
documents = read_documents_from_file(file_path)
|
||||||
|
embeddings = embed_model.encode(documents)
|
||||||
|
|
||||||
|
# 3️⃣ Inicjalizacja FAISS i dodanie wektorów
|
||||||
|
dim = embeddings.shape[1]
|
||||||
|
index = faiss.IndexFlatL2(dim)
|
||||||
|
index.add(np.array(embeddings, dtype=np.float32))
|
||||||
|
|
||||||
|
# 4️⃣ Przygotowanie danych treningowych
|
||||||
|
def create_training_data():
|
||||||
|
data = {
|
||||||
|
"text": documents,
|
||||||
|
"embedding": embeddings.tolist()
|
||||||
|
}
|
||||||
|
return Dataset.from_dict(data)
|
||||||
|
|
||||||
|
dataset = create_training_data()
|
||||||
|
|
||||||
|
# Podział danych na treningowe i ewaluacyjne
|
||||||
|
split_dataset = dataset.train_test_split(test_size=0.25)
|
||||||
|
train_dataset = split_dataset["train"]
|
||||||
|
eval_dataset = split_dataset["test"]
|
||||||
|
|
||||||
|
# 5️⃣ Ładowanie modelu Gemma 2B
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
model_name = "google/gemma-2-2b"
|
||||||
|
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16).to(device)
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||||||
|
|
||||||
|
# 6️⃣ Konfiguracja LoRA
|
||||||
|
lora_config = LoraConfig(
|
||||||
|
r=8, lora_alpha=32, lora_dropout=0.1, bias="none", task_type="CAUSAL_LM"
|
||||||
|
)
|
||||||
|
model = get_peft_model(model, lora_config)
|
||||||
|
|
||||||
|
# 7️⃣ Tokenizacja danych
|
||||||
|
max_length = 384
|
||||||
|
|
||||||
|
def tokenize_function(examples):
|
||||||
|
return tokenizer(
|
||||||
|
examples["text"],
|
||||||
|
padding="max_length",
|
||||||
|
truncation=True,
|
||||||
|
max_length=max_length
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenized_train = train_dataset.map(tokenize_function, batched=True)
|
||||||
|
tokenized_eval = eval_dataset.map(tokenize_function, batched=True)
|
||||||
|
|
||||||
|
# 8️⃣ Parametry treningu
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
output_dir="./results",
|
||||||
|
eval_strategy="steps", # Ewaluacja co określoną liczbę kroków
|
||||||
|
eval_steps=500, # Ewaluacja co 500 kroków
|
||||||
|
save_strategy="steps", # Zapis modelu co określoną liczbę kroków
|
||||||
|
save_steps=500, # Zapis modelu co 500 kroków
|
||||||
|
learning_rate=1e-5,
|
||||||
|
per_device_train_batch_size=2,
|
||||||
|
per_device_eval_batch_size=2,
|
||||||
|
num_train_epochs=16,
|
||||||
|
weight_decay=0.01,
|
||||||
|
load_best_model_at_end=True, # Wczytaj najlepszy model na końcu
|
||||||
|
metric_for_best_model="loss", # Kryterium wyboru najlepszego modelu
|
||||||
|
greater_is_better=False, # Niższy loss = lepszy model
|
||||||
|
)
|
||||||
|
|
||||||
|
# 9️⃣ Data Collator
|
||||||
|
data_collator = DataCollatorForLanguageModeling(
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
mlm=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 🔟 Trening modelu
|
||||||
|
trainer = Trainer(
|
||||||
|
model=model,
|
||||||
|
args=training_args,
|
||||||
|
train_dataset=tokenized_train,
|
||||||
|
eval_dataset=tokenized_eval, # Dodany zestaw ewaluacyjny
|
||||||
|
data_collator=data_collator,
|
||||||
|
)
|
||||||
|
|
||||||
|
trainer.train()
|
||||||
|
|
||||||
|
# 1️⃣1️⃣ Zapis modelu
|
||||||
|
model.save_pretrained("./trained_model/gemma")
|
||||||
|
tokenizer.save_pretrained("./trained_model/gemma")
|
||||||
|
|
||||||
|
print("✅ Model został wytrenowany i zapisany!")
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import torch
|
||||||
|
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForLanguageModeling
|
||||||
|
from datasets import Dataset
|
||||||
|
|
||||||
|
# Konfiguracja
|
||||||
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
MODEL_NAME = "gpt2-medium"
|
||||||
|
SPECIAL_TOKENS = ["[CITATION_START]", "[CITATION_END]"]
|
||||||
|
TEXT_FILE_PATH = "./docs/kodekspracy.txt" # Zmień na właściwą ścieżkę
|
||||||
|
|
||||||
|
def prepare_dataset_from_file(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
# Wydziel artykuły za pomocą wyrażenia regularnego
|
||||||
|
articles = re.findall(r'Art\.\s*\d+[a-z]*\..*?(?=\s*Art\.\s*\d+[a-z]*\.|\Z)', text, flags=re.DOTALL)
|
||||||
|
|
||||||
|
formatted_articles = []
|
||||||
|
for article in articles:
|
||||||
|
# Usuń zbędne białe znaki
|
||||||
|
article = ' '.join(article.strip().split())
|
||||||
|
|
||||||
|
# Wydziel numer artykułu i treść
|
||||||
|
art_match = re.match(r'Art\.\s*(\d+[a-z]*)\.?\s*(.*)', article, re.DOTALL)
|
||||||
|
if art_match:
|
||||||
|
art_number = art_match.group(1)
|
||||||
|
art_text = art_match.group(2)
|
||||||
|
|
||||||
|
# Podziel na paragrafy, jeśli istnieją
|
||||||
|
paragraphs = re.split(r'(§\s*\d+\.)', art_text)
|
||||||
|
if len(paragraphs) > 1:
|
||||||
|
formatted_paragraphs = []
|
||||||
|
for i in range(1, len(paragraphs), 2):
|
||||||
|
para_num = paragraphs[i].strip()
|
||||||
|
para_text = paragraphs[i+1].strip()
|
||||||
|
formatted_paragraphs.append(f"{para_num} {para_text}")
|
||||||
|
formatted = f"[CITATION_START] Kodeks Pracy, Art. {art_number} [CITATION_END]\n" + "\n".join(formatted_paragraphs)
|
||||||
|
else:
|
||||||
|
formatted = f"[CITATION_START] Kodeks Pracy, Art. {art_number} [CITATION_END] {art_text}"
|
||||||
|
|
||||||
|
formatted_articles.append({"text": formatted})
|
||||||
|
|
||||||
|
# Dodaj przykłady pytań i odpowiedzi
|
||||||
|
questions = [
|
||||||
|
f"Zacytuj artykuł {art_number} Kodeksu pracy.",
|
||||||
|
f"Co mówi artykuł {art_number} Kodeksu pracy?",
|
||||||
|
f"Podaj treść artykułu {art_number} Kodeksu pracy."
|
||||||
|
]
|
||||||
|
for question in questions:
|
||||||
|
formatted_articles.append({"text": f"{question}\n{formatted}"})
|
||||||
|
|
||||||
|
return formatted_articles
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Inicjalizacja tokenizera
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
||||||
|
tokenizer.add_special_tokens({"additional_special_tokens": SPECIAL_TOKENS})
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
|
||||||
|
# Przygotowanie danych
|
||||||
|
data = prepare_dataset_from_file(TEXT_FILE_PATH)
|
||||||
|
dataset = Dataset.from_dict({"text": [d["text"] for d in data]})
|
||||||
|
|
||||||
|
# Tokenizacja
|
||||||
|
def tokenize_function(examples):
|
||||||
|
tokenized = tokenizer(
|
||||||
|
examples["text"],
|
||||||
|
truncation=True,
|
||||||
|
padding="max_length",
|
||||||
|
max_length=1024, # Zwiększono dla dłuższych artykułów
|
||||||
|
return_tensors="pt"
|
||||||
|
)
|
||||||
|
tokenized["labels"] = tokenized["input_ids"].clone()
|
||||||
|
return tokenized
|
||||||
|
|
||||||
|
tokenized_dataset = dataset.map(tokenize_function, batched=True)
|
||||||
|
|
||||||
|
# Model i data collator
|
||||||
|
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)
|
||||||
|
model.resize_token_embeddings(len(tokenizer), mean_resizing=False)
|
||||||
|
|
||||||
|
data_collator = DataCollatorForLanguageModeling(
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
mlm=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Konfiguracja treningu
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
output_dir="./results",
|
||||||
|
num_train_epochs=32, # Zwiększono liczbę epok
|
||||||
|
per_device_train_batch_size=2,
|
||||||
|
learning_rate=1e-5, #precyzja uczenia
|
||||||
|
logging_steps=10,
|
||||||
|
weight_decay=0.01,
|
||||||
|
report_to="none",
|
||||||
|
save_strategy="no",
|
||||||
|
load_best_model_at_end=True, # Ładowanie najlepszego modelu na końcu
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Trainer
|
||||||
|
trainer = Trainer(
|
||||||
|
model=model,
|
||||||
|
args=training_args,
|
||||||
|
train_dataset=tokenized_dataset,
|
||||||
|
data_collator=data_collator
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Rozpoczęcie treningu...")
|
||||||
|
trainer.train()
|
||||||
|
trainer.save_model("./trained_model/gpt")
|
||||||
|
tokenizer.save_pretrained("./trained_model/gpt")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import os
|
||||||
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import faiss
|
||||||
|
import numpy as np
|
||||||
|
from sentence_transformers import SentenceTransformer
|
||||||
|
from datasets import Dataset
|
||||||
|
from peft import LoraConfig, get_peft_model
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForLanguageModeling
|
||||||
|
|
||||||
|
# 1️⃣ Inicjalizacja modelu do embeddingów
|
||||||
|
embed_model = SentenceTransformer("all-MiniLM-L6-v2")
|
||||||
|
|
||||||
|
# 2️⃣ Dodanie dokumentów i embeddingów
|
||||||
|
def read_documents_from_file(file_path):
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
articles = content.split('\n\n')
|
||||||
|
documents = []
|
||||||
|
for article in articles:
|
||||||
|
if article.strip().startswith('Art.'):
|
||||||
|
documents.append(article.strip())
|
||||||
|
return documents
|
||||||
|
#documents = [
|
||||||
|
# "Jak założyć firmę w Polsce?",
|
||||||
|
# "Jak rozliczyć podatek VAT?",
|
||||||
|
# "Procedura składania reklamacji w e-sklepie.",
|
||||||
|
# "Jakie dokumenty są potrzebne do rejestracji działalności?"
|
||||||
|
#]
|
||||||
|
file_path = './docs/kodekspracy.txt' # Zmień na właściwą ścieżkę
|
||||||
|
documents = read_documents_from_file(file_path)
|
||||||
|
embeddings = embed_model.encode(documents)
|
||||||
|
|
||||||
|
# 3️⃣ Inicjalizacja FAISS i dodanie wektorów
|
||||||
|
dim = embeddings.shape[1]
|
||||||
|
index = faiss.IndexFlatL2(dim)
|
||||||
|
index.add(np.array(embeddings, dtype=np.float32))
|
||||||
|
|
||||||
|
# 4️⃣ Przygotowanie danych treningowych
|
||||||
|
def create_training_data():
|
||||||
|
data = {
|
||||||
|
"text": documents,
|
||||||
|
"embedding": embeddings.tolist()
|
||||||
|
}
|
||||||
|
return Dataset.from_dict(data)
|
||||||
|
|
||||||
|
dataset = create_training_data()
|
||||||
|
|
||||||
|
# Podział danych na treningowe i ewaluacyjne
|
||||||
|
split_dataset = dataset.train_test_split(test_size=0.25)
|
||||||
|
train_dataset = split_dataset["train"]
|
||||||
|
eval_dataset = split_dataset["test"]
|
||||||
|
|
||||||
|
# 5️⃣ Ładowanie modelu Gemma 2B
|
||||||
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
model_name = "Lajonbot/vicuna-7b-v1.5-PL-lora_unload"
|
||||||
|
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16).to(device)
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||||||
|
|
||||||
|
# 6️⃣ Konfiguracja LoRA
|
||||||
|
lora_config = LoraConfig(
|
||||||
|
r=8, lora_alpha=32, lora_dropout=0.1, bias="none", task_type="CAUSAL_LM"
|
||||||
|
)
|
||||||
|
model = get_peft_model(model, lora_config)
|
||||||
|
|
||||||
|
# 7️⃣ Tokenizacja danych
|
||||||
|
max_length = 384
|
||||||
|
|
||||||
|
def tokenize_function(examples):
|
||||||
|
return tokenizer(
|
||||||
|
examples["text"],
|
||||||
|
padding="max_length",
|
||||||
|
truncation=True,
|
||||||
|
max_length=max_length
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenized_train = train_dataset.map(tokenize_function, batched=True)
|
||||||
|
tokenized_eval = eval_dataset.map(tokenize_function, batched=True)
|
||||||
|
|
||||||
|
# 8️⃣ Parametry treningu
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
output_dir="./results",
|
||||||
|
eval_strategy="steps", # Ewaluacja co określoną liczbę kroków
|
||||||
|
eval_steps=500, # Ewaluacja co 500 kroków
|
||||||
|
save_strategy="steps", # Zapis modelu co określoną liczbę kroków
|
||||||
|
save_steps=500, # Zapis modelu co 500 kroków
|
||||||
|
learning_rate=1e-5,
|
||||||
|
per_device_train_batch_size=2,
|
||||||
|
per_device_eval_batch_size=2,
|
||||||
|
num_train_epochs=16,
|
||||||
|
weight_decay=0.01,
|
||||||
|
load_best_model_at_end=True, # Wczytaj najlepszy model na końcu
|
||||||
|
metric_for_best_model="loss", # Kryterium wyboru najlepszego modelu
|
||||||
|
greater_is_better=False, # Niższy loss = lepszy model
|
||||||
|
)
|
||||||
|
|
||||||
|
# 9️⃣ Data Collator
|
||||||
|
data_collator = DataCollatorForLanguageModeling(
|
||||||
|
tokenizer=tokenizer,
|
||||||
|
mlm=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# 🔟 Trening modelu
|
||||||
|
trainer = Trainer(
|
||||||
|
model=model,
|
||||||
|
args=training_args,
|
||||||
|
train_dataset=tokenized_train,
|
||||||
|
eval_dataset=tokenized_eval, # Dodany zestaw ewaluacyjny
|
||||||
|
data_collator=data_collator,
|
||||||
|
)
|
||||||
|
|
||||||
|
trainer.train()
|
||||||
|
|
||||||
|
# 1️⃣1️⃣ Zapis modelu
|
||||||
|
model.save_pretrained("./models/herbert")
|
||||||
|
tokenizer.save_pretrained("./models/herbert")
|
||||||
|
|
||||||
|
print("✅ Model został wytrenowany i zapisany!")
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
import os
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForLanguageModeling
|
||||||
|
from datasets import Dataset
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import PyPDF2
|
||||||
|
import docx2txt
|
||||||
|
import pytesseract
|
||||||
|
from PIL import Image
|
||||||
|
from collections import defaultdict
|
||||||
|
from huggingface_hub import login
|
||||||
|
|
||||||
|
# Konfiguracja
|
||||||
|
os.environ['TORCH_USE_CUDA_DSA'] = '1'
|
||||||
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
login(token="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")
|
||||||
|
|
||||||
|
class SourceMapper:
|
||||||
|
def __init__(self):
|
||||||
|
self.source_to_idx = defaultdict(lambda: len(self.source_to_idx))
|
||||||
|
self.idx_to_source = {}
|
||||||
|
|
||||||
|
def add_source(self, source):
|
||||||
|
if source and source not in self.source_to_idx:
|
||||||
|
idx = self.source_to_idx[source]
|
||||||
|
self.idx_to_source[idx] = source
|
||||||
|
|
||||||
|
def get_idx(self, source):
|
||||||
|
return self.source_to_idx[source] if source else -1
|
||||||
|
|
||||||
|
def get_source(self, idx):
|
||||||
|
return self.idx_to_source.get(idx, "Unknown")
|
||||||
|
|
||||||
|
def load_file_catalog(catalog_path):
|
||||||
|
try:
|
||||||
|
with open(catalog_path, 'r', encoding='utf-8') as file:
|
||||||
|
return json.load(file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Błąd wczytywania katalogu plików: {str(e)}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def identify_legal_document(filename, file_catalog):
|
||||||
|
base_name = os.path.splitext(filename)[0].lower()
|
||||||
|
return file_catalog.get(base_name, "Opracowanie własne")
|
||||||
|
|
||||||
|
def extract_text_from_file(file_path):
|
||||||
|
try:
|
||||||
|
_, ext = os.path.splitext(file_path)
|
||||||
|
ext = ext.lower()
|
||||||
|
|
||||||
|
if ext in ['.txt', '.md']:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
return file.read()
|
||||||
|
elif ext == '.pdf':
|
||||||
|
text = ""
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as file:
|
||||||
|
reader = PyPDF2.PdfReader(file)
|
||||||
|
for page in reader.pages:
|
||||||
|
text += page.extract_text() or ""
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Błąd PDF: {str(e)}")
|
||||||
|
return text
|
||||||
|
elif ext in ['.doc', '.docx']:
|
||||||
|
return docx2txt.process(file_path)
|
||||||
|
elif ext in ['.jpg', '.jpeg', '.png', '.bmp', '.tiff']:
|
||||||
|
return pytesseract.image_to_string(Image.open(file_path))
|
||||||
|
else:
|
||||||
|
print(f"Nieobsługiwany format pliku: {ext}")
|
||||||
|
return ""
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Błąd ekstrakcji tekstu: {str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def prepare_dataset(directory, catalog_path, source_mapper):
|
||||||
|
file_catalog = load_file_catalog(catalog_path)
|
||||||
|
data = []
|
||||||
|
|
||||||
|
print(f"\n{'='*50}\nDIAGNOSTYKA DANYCH\n{'='*50}")
|
||||||
|
|
||||||
|
for root, _, files in os.walk(directory):
|
||||||
|
for file in files:
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
print(f"\nPrzetwarzanie pliku: {file_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = extract_text_from_file(file_path)
|
||||||
|
if not text.strip():
|
||||||
|
print("Pominięto - brak tekstu")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Długość tekstu: {len(text)} znaków")
|
||||||
|
|
||||||
|
doc_type = identify_legal_document(file, file_catalog)
|
||||||
|
print(f"Rozpoznany typ dokumentu: {doc_type}")
|
||||||
|
|
||||||
|
if doc_type != "Opracowanie własne":
|
||||||
|
articles = re.split(r'(?i)(Art[\.\s]+\d+[\.\s]?)', text)
|
||||||
|
articles = [a.strip() for a in articles if a.strip()]
|
||||||
|
|
||||||
|
print(f"Znaleziono {len(articles)} fragmentów")
|
||||||
|
|
||||||
|
for i in range(0, len(articles)-1, 2):
|
||||||
|
article_number = articles[i]
|
||||||
|
article_content = articles[i+1]
|
||||||
|
|
||||||
|
if len(article_content) < 50:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source = f"{doc_type}, {article_number}"
|
||||||
|
source_mapper.add_source(source)
|
||||||
|
data.append({
|
||||||
|
"text": f"{article_number} {article_content}",
|
||||||
|
"source_idx": source_mapper.get_idx(source)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
clean_text = re.sub(r'\s+', ' ', text).strip()
|
||||||
|
chunks = [clean_text[i:i+512] for i in range(0, len(clean_text), 512)]
|
||||||
|
chunks = [c for c in chunks if c.strip()]
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
data.append({
|
||||||
|
"text": chunk,
|
||||||
|
"source_idx": -1
|
||||||
|
})
|
||||||
|
print(f"Dodano {len(chunks)} chunków")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Błąd podczas przetwarzania pliku: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"\nPodsumowanie przygotowania danych:")
|
||||||
|
print(f"Łączna liczba przykładów: {len(data)}")
|
||||||
|
if data:
|
||||||
|
print("Przykładowy wpis:")
|
||||||
|
print(json.dumps(data[0], indent=2, ensure_ascii=False))
|
||||||
|
else:
|
||||||
|
print("BRAK DANYCH - sprawdź diagnostykę powyżej")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
class CustomModel(nn.Module):
|
||||||
|
def __init__(self, model_name, config):
|
||||||
|
super().__init__()
|
||||||
|
self.base_model = AutoModelForCausalLM.from_pretrained(model_name, config=config)
|
||||||
|
self.source_embedding = nn.Embedding(10000, config.hidden_size, padding_idx=-1)
|
||||||
|
|
||||||
|
for param in self.base_model.parameters():
|
||||||
|
param.requires_grad = False
|
||||||
|
for param in self.base_model.get_output_embeddings().parameters():
|
||||||
|
param.requires_grad = True
|
||||||
|
|
||||||
|
def forward(self, input_ids=None, attention_mask=None, labels=None, source_idx=None, **kwargs):
|
||||||
|
if source_idx is not None:
|
||||||
|
valid_indices = torch.clamp(source_idx, 0, self.source_embedding.num_embeddings-1)
|
||||||
|
source_embeds = self.source_embedding(valid_indices).unsqueeze(1)
|
||||||
|
inputs_embeds = self.base_model.get_input_embeddings()(input_ids) + source_embeds
|
||||||
|
return self.base_model(
|
||||||
|
inputs_embeds=inputs_embeds,
|
||||||
|
attention_mask=attention_mask,
|
||||||
|
labels=labels,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
return self.base_model(
|
||||||
|
input_ids=input_ids,
|
||||||
|
attention_mask=attention_mask,
|
||||||
|
labels=labels,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate(self, *args, **kwargs):
|
||||||
|
return self.base_model.generate(*args, **kwargs)
|
||||||
|
|
||||||
|
class CustomDataCollator(DataCollatorForLanguageModeling):
|
||||||
|
def torch_call(self, examples):
|
||||||
|
# Przetwórz podstawowe pola
|
||||||
|
input_ids = torch.stack([torch.tensor(ex["input_ids"]) for ex in examples])
|
||||||
|
attention_mask = torch.stack([torch.tensor(ex["attention_mask"]) for ex in examples])
|
||||||
|
labels = torch.stack([torch.tensor(ex["labels"]) for ex in examples])
|
||||||
|
|
||||||
|
batch = {
|
||||||
|
"input_ids": input_ids,
|
||||||
|
"attention_mask": attention_mask,
|
||||||
|
"labels": labels
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dodaj source_idx jeśli istnieje
|
||||||
|
if "source_idx" in examples[0]:
|
||||||
|
source_idx = torch.stack([torch.tensor(ex["source_idx"]) for ex in examples])
|
||||||
|
batch["source_idx"] = source_idx
|
||||||
|
|
||||||
|
return batch
|
||||||
|
|
||||||
|
def main():
|
||||||
|
source_mapper = SourceMapper()
|
||||||
|
model_name = "crumb/nano-mistral"
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
|
||||||
|
# Przygotowanie danych
|
||||||
|
catalog_path = "catalog.json"
|
||||||
|
data = prepare_dataset("docs", catalog_path, source_mapper)
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
print("\nBrak danych do treningu!")
|
||||||
|
return
|
||||||
|
|
||||||
|
#dataset = Dataset.from_list(data)
|
||||||
|
dataset = Dataset.from_dict({k: [d[k] for d in data] for k in data[0]})
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize_function(examples):
|
||||||
|
tokenized = tokenizer(
|
||||||
|
examples["text"],
|
||||||
|
truncation=True,
|
||||||
|
padding="max_length",
|
||||||
|
max_length=512,
|
||||||
|
return_tensors="pt"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"input_ids": tokenized["input_ids"].squeeze(),
|
||||||
|
"attention_mask": tokenized["attention_mask"].squeeze(),
|
||||||
|
"labels": tokenized["input_ids"].squeeze().clone(),
|
||||||
|
"source_idx": examples["source_idx"] # Dodano bez konwersji do tensora
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenized_dataset = dataset.map(tokenize_function, batched=True, batch_size=16)
|
||||||
|
|
||||||
|
model = CustomModel(model_name, AutoModelForCausalLM.from_pretrained(model_name).config)
|
||||||
|
model.source_mapper = source_mapper
|
||||||
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
model.to(device)
|
||||||
|
|
||||||
|
training_args = TrainingArguments(
|
||||||
|
output_dir="./results",
|
||||||
|
num_train_epochs=3,
|
||||||
|
per_device_train_batch_size=2,
|
||||||
|
gradient_accumulation_steps=4,
|
||||||
|
learning_rate=2e-5,
|
||||||
|
fp16=torch.cuda.is_available(),
|
||||||
|
logging_steps=10,
|
||||||
|
save_strategy="steps",
|
||||||
|
save_steps=1000,
|
||||||
|
report_to="none",
|
||||||
|
remove_unused_columns=False
|
||||||
|
)
|
||||||
|
|
||||||
|
trainer = Trainer(
|
||||||
|
model=model,
|
||||||
|
args=training_args,
|
||||||
|
train_dataset=tokenized_dataset,
|
||||||
|
data_collator=CustomDataCollator(tokenizer=tokenizer, mlm=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\nRozpoczęcie treningu...")
|
||||||
|
trainer.train()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -1,385 +0,0 @@
|
||||||
from fastapi import FastAPI, HTTPException, Request
|
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from pydantic import BaseModel
|
|
||||||
import ollama
|
|
||||||
import weaviate
|
|
||||||
from weaviate.connect import ConnectionParams
|
|
||||||
from weaviate.collections.classes.filters import Filter
|
|
||||||
import re
|
|
||||||
import json
|
|
||||||
import uvicorn
|
|
||||||
import httpx
|
|
||||||
from typing import List, Optional
|
|
||||||
import asyncio
|
|
||||||
import os
|
|
||||||
from elasticsearch import Elasticsearch
|
|
||||||
from datetime import datetime
|
|
||||||
####
|
|
||||||
import aiohttp
|
|
||||||
import asyncio
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
import urllib.parse
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
OLLAMA_BASE_URL = "http://ollama:11434"
|
|
||||||
ES_BASE_URL = "http://elastic:9200"
|
|
||||||
WEAVIATE_URL = "http://weaviate:8080"
|
|
||||||
PROMPT_DIR_PATCH = "./prompts"
|
|
||||||
SEARXNG_BASE_URL = "http://searxng:8080"
|
|
||||||
|
|
||||||
# Inicjalizacja klientów
|
|
||||||
ollama_client = ollama.Client(host=OLLAMA_BASE_URL)
|
|
||||||
es = Elasticsearch(ES_BASE_URL)
|
|
||||||
weaviate_client = weaviate.WeaviateClient(
|
|
||||||
connection_params=ConnectionParams.from_params(
|
|
||||||
http_host="weaviate",
|
|
||||||
http_port=8080,
|
|
||||||
http_secure=False,
|
|
||||||
grpc_host="weaviate",
|
|
||||||
grpc_port=50051,
|
|
||||||
grpc_secure=False,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
weaviate_client.connect()
|
|
||||||
collection = weaviate_client.collections.get("Document")
|
|
||||||
files_content = None
|
|
||||||
|
|
||||||
class Message(BaseModel):
|
|
||||||
role: str
|
|
||||||
content: str
|
|
||||||
|
|
||||||
class ChatRequest(BaseModel):
|
|
||||||
model: str
|
|
||||||
messages: List[Message]
|
|
||||||
stream: Optional[bool] = False
|
|
||||||
options: Optional[dict] = None
|
|
||||||
|
|
||||||
class ChatResponse(BaseModel):
|
|
||||||
model: str
|
|
||||||
created_at: str
|
|
||||||
message: Message
|
|
||||||
done: bool
|
|
||||||
total_duration: int
|
|
||||||
load_duration: int
|
|
||||||
prompt_eval_count: int
|
|
||||||
prompt_eval_duration: int
|
|
||||||
eval_count: int
|
|
||||||
eval_duration: int
|
|
||||||
|
|
||||||
def read_text_files_from_directory(directory_path):
|
|
||||||
files_dict = {}
|
|
||||||
|
|
||||||
# Iterowanie przez wszystkie pliki w katalogu
|
|
||||||
for filename in os.listdir(directory_path):
|
|
||||||
# Sprawdzanie, czy plik ma rozszerzenie .txt
|
|
||||||
if filename.endswith('.txt'):
|
|
||||||
file_path = os.path.join(directory_path, filename)
|
|
||||||
try:
|
|
||||||
# Odczytywanie zawartości pliku
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
|
||||||
content = file.read()
|
|
||||||
# Dodawanie do słownika z nazwą pliku bez rozszerzenia jako kluczem
|
|
||||||
file_name_without_extension = os.path.splitext(filename)[0]
|
|
||||||
files_dict[file_name_without_extension] = content
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Błąd przy odczycie pliku {filename}: {e}")
|
|
||||||
|
|
||||||
return files_dict
|
|
||||||
|
|
||||||
def analyze_query(content):
|
|
||||||
analysis = ollama_client.chat(
|
|
||||||
model="gemma2:2b",
|
|
||||||
messages=[{"role": "user", "content": content}]
|
|
||||||
)
|
|
||||||
keywords = [word.strip() for word in analysis['message']['content'].split(',') if word.strip()]
|
|
||||||
print("Słowa kluczowe:", keywords)
|
|
||||||
return keywords
|
|
||||||
|
|
||||||
def extract_full_article(content, article_number):
|
|
||||||
pattern = rf"Art\.\s*{article_number}\..*?(?=Art\.\s*\d+\.|\Z)"
|
|
||||||
match = re.search(pattern, content, re.DOTALL)
|
|
||||||
if match:
|
|
||||||
return match.group(0).strip()
|
|
||||||
return None
|
|
||||||
|
|
||||||
def extract_relevant_fragment(content, query, context_size=100):
|
|
||||||
article_match = re.match(r"Art\.\s*(\d+)", query)
|
|
||||||
if article_match:
|
|
||||||
article_number = article_match.group(1)
|
|
||||||
full_article = extract_full_article(content, article_number)
|
|
||||||
if full_article:
|
|
||||||
return full_article
|
|
||||||
|
|
||||||
index = content.lower().find(query.lower())
|
|
||||||
if index != -1:
|
|
||||||
start = max(0, index - context_size)
|
|
||||||
end = min(len(content), index + len(query) + context_size)
|
|
||||||
return f"...{content[start:end]}..."
|
|
||||||
return content[:1000] + "..."
|
|
||||||
|
|
||||||
def hybrid_search(keywords, limit=100, alpha=0.5):
|
|
||||||
if isinstance(keywords, str):
|
|
||||||
keywords = [keywords]
|
|
||||||
|
|
||||||
query = " ".join(keywords)
|
|
||||||
|
|
||||||
#print(f"\nWyszukiwanie hybrydowe dla słowa kluczowego: '{query}'")
|
|
||||||
response = collection.query.hybrid(
|
|
||||||
query=query,
|
|
||||||
alpha=alpha,
|
|
||||||
limit=limit * 2
|
|
||||||
)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for obj in response.objects:
|
|
||||||
#print(f"UUID: {obj.uuid}")
|
|
||||||
relevant_fragment = extract_relevant_fragment(obj.properties['content'], query)
|
|
||||||
#print(f"Relewantny fragment:\n{relevant_fragment}")
|
|
||||||
print(f"Nazwa pliku: {obj.properties['fileName']}")
|
|
||||||
#print("---")
|
|
||||||
# Zmieniamy warunek na 'any' zamiast 'all'
|
|
||||||
#if any(term.lower() in relevant_fragment.lower() for term in keywords):
|
|
||||||
results.append({
|
|
||||||
"uuid": obj.uuid,
|
|
||||||
"relevant_fragment": relevant_fragment,
|
|
||||||
"file_name": obj.properties['fileName'],
|
|
||||||
"content_type": obj.properties['contentType'],
|
|
||||||
"keyword": query
|
|
||||||
})
|
|
||||||
#print(f"Dodano do wyników: {obj.uuid}")
|
|
||||||
|
|
||||||
if len(results) >= limit:
|
|
||||||
break
|
|
||||||
return results[:limit]
|
|
||||||
|
|
||||||
async def fetch_json(session, url):
|
|
||||||
async with session.get(url) as response:
|
|
||||||
return await response.json()
|
|
||||||
|
|
||||||
async def fetch_text(session, url):
|
|
||||||
async with session.get(url) as response:
|
|
||||||
html = await response.text()
|
|
||||||
soup = BeautifulSoup(html, "html.parser")
|
|
||||||
return soup.get_text()
|
|
||||||
|
|
||||||
async def process_search_results(query):
|
|
||||||
search_url = f"{SEARXNG_BASE_URL}/search?q={urllib.parse.quote(query)}&categories=general&format=json"
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
data = await fetch_json(session, search_url)
|
|
||||||
|
|
||||||
results = data.get("results", [])
|
|
||||||
results_sorted = sorted(results, key=lambda x: x.get("score", float('inf')))[:10]
|
|
||||||
|
|
||||||
tasks = [fetch_text(session, result["url"]) for result in results_sorted]
|
|
||||||
texts = await asyncio.gather(*tasks)
|
|
||||||
|
|
||||||
save_to_weaviate([{
|
|
||||||
"fileName": result["url"],
|
|
||||||
"content": json.dumps({
|
|
||||||
"prompt": query,
|
|
||||||
"completion": text
|
|
||||||
}),
|
|
||||||
"contentHash": generate_content_hash(text)
|
|
||||||
} for result, text in zip(results_sorted, texts)])
|
|
||||||
|
|
||||||
def generate_content_hash(content):
|
|
||||||
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
def save_to_weaviate(data):
|
|
||||||
try:
|
|
||||||
collection = weaviate_client.collections.get("Document")
|
|
||||||
for item in data:
|
|
||||||
filters = Filter.by_property("fileName").equal(item["fileName"])
|
|
||||||
existing_docs = collection.query.fetch_objects(filters=filters)
|
|
||||||
if existing_docs.objects:
|
|
||||||
return
|
|
||||||
|
|
||||||
collection.data.insert({
|
|
||||||
"fileName": item["fileName"],
|
|
||||||
"content": item["content"],
|
|
||||||
"contentHash": item["contentHash"],
|
|
||||||
"contentType": "website"
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Błąd podczas dodawania informacji do bazy. Error: {e}")
|
|
||||||
|
|
||||||
@app.get("/api/tags")
|
|
||||||
async def tags_proxy():
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
@app.get("/api/version")
|
|
||||||
async def tags_proxy():
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.get(f"{OLLAMA_BASE_URL}/api/version")
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
@app.post("/api/generate")
|
|
||||||
async def generate_proxy(request: Request):
|
|
||||||
data = await request.json()
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
response = await client.post(f"{OLLAMA_BASE_URL}/api/generate", json=data)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
@app.get("/api/models")
|
|
||||||
async def list_models():
|
|
||||||
try:
|
|
||||||
models = ollama_client.list()
|
|
||||||
return {"models": [model['name'] for model in models['models']]}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
async def stream_chat(model, messages, options):
|
|
||||||
try:
|
|
||||||
# Użycie httpx do asynchronicznego pobrania danych od Ollamy
|
|
||||||
async with httpx.AsyncClient() as client:
|
|
||||||
async with client.stream(
|
|
||||||
"POST",
|
|
||||||
f"{OLLAMA_BASE_URL}/api/chat",
|
|
||||||
json={"model": model, "messages": messages, "stream": True, "options": options},
|
|
||||||
) as response:
|
|
||||||
async for line in response.aiter_lines():
|
|
||||||
yield line + "\n"
|
|
||||||
except Exception as e:
|
|
||||||
yield json.dumps({"error": str(e)}) + "\n"
|
|
||||||
|
|
||||||
def save_to_elasticsearch(index, request_data, response_data, search_data=None):
|
|
||||||
try:
|
|
||||||
def message_to_dict(message):
|
|
||||||
if isinstance(message, dict):
|
|
||||||
return {
|
|
||||||
"role": message.get("role"),
|
|
||||||
"content": message.get("content")
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"role": getattr(message, "role", None),
|
|
||||||
"content": getattr(message, "content", None)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isinstance(request_data.get("messages"), list):
|
|
||||||
request_data["messages"] = [message_to_dict(msg) for msg in request_data["messages"]]
|
|
||||||
|
|
||||||
if "message" in response_data:
|
|
||||||
response_data["message"] = message_to_dict(response_data["message"])
|
|
||||||
|
|
||||||
if "timestamp" in response_data:
|
|
||||||
response_data["timestamp"] = response_data["timestamp"].isoformat()
|
|
||||||
|
|
||||||
document = {
|
|
||||||
"request": request_data,
|
|
||||||
"response": response_data,
|
|
||||||
"timestamp": datetime.utcnow().isoformat(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if search_data:
|
|
||||||
document["vector_search"] = {
|
|
||||||
"keywords": search_data.get("keywords", []),
|
|
||||||
"results": search_data.get("results", [])
|
|
||||||
}
|
|
||||||
|
|
||||||
json_document = json.dumps(document, default=str)
|
|
||||||
|
|
||||||
index_name = index
|
|
||||||
response = es.index(index=index_name, body=json_document)
|
|
||||||
|
|
||||||
#print(response)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving to Elasticsearch: {e}")
|
|
||||||
|
|
||||||
@app.post("/api/chat")
|
|
||||||
async def chat_endpoint(request: ChatRequest):
|
|
||||||
try:
|
|
||||||
files_content = read_text_files_from_directory(PROMPT_DIR_PATCH)
|
|
||||||
if files_content is None:
|
|
||||||
raise KeyError(f"Nie wczytano promptów!!!")
|
|
||||||
|
|
||||||
prompt_seach = files_content.get("prompt_seach")
|
|
||||||
if prompt_seach is None:
|
|
||||||
raise KeyError(f"Nie znaleziono promptu o nazwie '{prompt_seach}'.")
|
|
||||||
|
|
||||||
prompt_system = files_content.get("prompt_system")
|
|
||||||
if prompt_system is None:
|
|
||||||
raise KeyError(f"Nie znaleziono promptu o nazwie '{prompt_system}'.")
|
|
||||||
|
|
||||||
prompt_answer = files_content.get("prompt_answer")
|
|
||||||
if prompt_answer is None:
|
|
||||||
raise KeyError(f"Nie znaleziono promptu o nazwie '{prompt_answer}'.")
|
|
||||||
|
|
||||||
prompt_data = files_content.get("prompt_data")
|
|
||||||
if prompt_data is None:
|
|
||||||
raise KeyError(f"Nie znaleziono promptu o nazwie '{prompt_data}'.")
|
|
||||||
|
|
||||||
query = request.messages[-1].content if request.messages else ""
|
|
||||||
asyncio.run(process_search_results(query))
|
|
||||||
keywords = analyze_query(prompt_seach.format(query=query))
|
|
||||||
weaviate_results = hybrid_search(keywords)
|
|
||||||
prompt_data += "\n".join([f"Źródło: {doc['file_name']}\n{doc['relevant_fragment']}\n\n" for doc in weaviate_results])
|
|
||||||
|
|
||||||
#messages_with_context =[
|
|
||||||
# {"role": "system", "content": prompt_system},
|
|
||||||
# {"role": "system", "content": prompt_data},
|
|
||||||
# {"role": "user", "content": prompt_answer.format(query=query)}
|
|
||||||
# ]
|
|
||||||
|
|
||||||
# Zmieniamy, aby przekazać pełną historię wiadomości
|
|
||||||
messages_with_context =[
|
|
||||||
{"role": "system", "content": prompt_system},
|
|
||||||
{"role": "system", "content": prompt_data},
|
|
||||||
# Dodajemy wszystkie wiadomości z historii
|
|
||||||
*[{"role": message.role, "content": message.content} for message in request.messages],
|
|
||||||
{"role": "user", "content": prompt_answer.format(query=query)}
|
|
||||||
]
|
|
||||||
|
|
||||||
if request.stream:
|
|
||||||
return StreamingResponse(stream_chat(request.model, messages_with_context, request.options), media_type="application/json")
|
|
||||||
|
|
||||||
ollama_response = ollama_client.chat(
|
|
||||||
model=request.model,
|
|
||||||
messages=messages_with_context,
|
|
||||||
stream=False,
|
|
||||||
options=request.options
|
|
||||||
)
|
|
||||||
|
|
||||||
request_data = {
|
|
||||||
"query": query,
|
|
||||||
"messages": request.messages,
|
|
||||||
"options": request.options
|
|
||||||
}
|
|
||||||
response_data = {
|
|
||||||
"model": request.model,
|
|
||||||
"created_at": ollama_response.get('created_at', ''),
|
|
||||||
"message": ollama_response['message'],
|
|
||||||
"done": ollama_response.get('done', True),
|
|
||||||
"total_duration": ollama_response.get('total_duration', 0),
|
|
||||||
"load_duration": ollama_response.get('load_duration', 0),
|
|
||||||
"prompt_eval_count": ollama_response.get('prompt_eval_count', 0),
|
|
||||||
"prompt_eval_duration": ollama_response.get('prompt_eval_duration', 0),
|
|
||||||
"eval_count": ollama_response.get('eval_count', 0),
|
|
||||||
"eval_duration": ollama_response.get('eval_duration', 0)
|
|
||||||
}
|
|
||||||
save_to_elasticsearch("ably.do", request_data, response_data, {"keywords": keywords, "results": weaviate_results })
|
|
||||||
|
|
||||||
return ChatResponse(
|
|
||||||
model=request.model,
|
|
||||||
created_at=ollama_response.get('created_at', ''),
|
|
||||||
message=Message(
|
|
||||||
role=ollama_response['message']['role'],
|
|
||||||
content=ollama_response['message']['content']
|
|
||||||
),
|
|
||||||
done=ollama_response.get('done', True),
|
|
||||||
total_duration=ollama_response.get('total_duration', 0),
|
|
||||||
load_duration=ollama_response.get('load_duration', 0),
|
|
||||||
prompt_eval_count=ollama_response.get('prompt_eval_count', 0),
|
|
||||||
prompt_eval_duration=ollama_response.get('prompt_eval_duration', 0),
|
|
||||||
eval_count=ollama_response.get('eval_count', 0),
|
|
||||||
eval_duration=ollama_response.get('eval_duration', 0)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Na podstawie powyższych informacji odpowiedz na pytanie: {query}.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Oto fragmenty aktów prawnych i dokumentów powiązanych z pytaniem użytkownika: \n\n
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
Jesteś precyzyjnym narzędziem do generowania słów kluczowych z zakresu BHP i prawa pracy. Twoje zadanie to podanie WYŁĄCZNIE najistotniejszych terminów do wyszukiwania w bazie dokumentów prawnych.
|
|
||||||
|
|
||||||
Ścisłe zasady działania:
|
|
||||||
1. Jeśli zapytanie dotyczy konkretnego artykułu:
|
|
||||||
- Podaj TYLKO numer artykułu i nazwę kodeksu (np. "Art. 154, Kodeks pracy").
|
|
||||||
- NIE dodawaj żadnych dodatkowych słów.
|
|
||||||
|
|
||||||
2. Jeśli zapytanie nie odnosi się do konkretnego artykułu:
|
|
||||||
- Wybierz maksymalnie 3 najbardziej precyzyjne i specyficzne terminy związane z treścią zapytania.
|
|
||||||
- Unikaj ogólników takich jak „praca”, „pracownik”, „pracodawca” – chyba że są częścią specjalistycznego terminu prawnego.
|
|
||||||
|
|
||||||
3. Używaj wyłącznie terminów, które realnie występują w dokumentach prawnych lub specjalistycznych opracowaniach.
|
|
||||||
|
|
||||||
4. Nie dodawaj żadnych interpretacji, rozszerzeń ani dodatkowych komentarzy.
|
|
||||||
|
|
||||||
Format odpowiedzi:
|
|
||||||
- Odpowiedz wyłącznie listą słów kluczowych oddzielonych przecinkami, bez dodatkowego tekstu, wyjaśnień czy formatowania JSON.
|
|
||||||
|
|
||||||
Przykład:
|
|
||||||
- Zapytanie: Jak brzmi art. 3 Kodeksu pracy?
|
|
||||||
- Odpowiedź: Art. 3, Kodeks pracy
|
|
||||||
|
|
||||||
- Zapytanie: Obowiązki pracodawcy w zakresie BHP
|
|
||||||
- Odpowiedź: obowiązki pracodawcy, BHP, przepisy bezpieczeństwa
|
|
||||||
|
|
||||||
Zapytanie: {query}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
Jesteś asystentem – ekspertem ds. BHP. Twoim zadaniem jest udzielanie rzeczowych, precyzyjnych i merytorycznych odpowiedzi na pytania użytkownika na podstawie dostępnych aktów prawnych (np. kodeksów, rozporządzeń) oraz innych dokumentów, takich jak instrukcje stanowiskowe czy publikacje ekspertów.
|
|
||||||
|
|
||||||
### 📜 Zasady udzielania odpowiedzi:
|
|
||||||
1. **Korzystaj z dostępnych źródeł** – Twoje odpowiedzi powinny być oparte na dokumentach dostarczonych w kontekście.
|
|
||||||
2. **Cytuj akty prawne, jeśli to możliwe** – Jeśli użytkownik pyta o konkretny artykuł, przytocz jego treść w formie cytatu, np. "Art. 3 Kodeksu Pracy stanowi: 'Pracodawcą jest jednostka organizacyjna, choćby nie posiadała osobowości prawnej, a także osoba fizyczna, jeżeli zatrudniają one pracowników.'".
|
|
||||||
3. **Nie podawaj nazw plików źródłowych** – Możesz powoływać się na źródła (np. "Zgodnie z Kodeksem Pracy, art. 3..."), ale nie możesz ujawniać nazw plików zawierających te informacje.
|
|
||||||
4. **Wskazuj źródła przepisów** – Jeśli odpowiedź wynika z aktu prawnego, zaznacz to, np. "Zgodnie z art. 237 Kodeksu Pracy...".
|
|
||||||
5. **Jeżeli pytanie dotyczy zagadnienia, a nie konkretnego artykułu**, odpowiedź może być swobodniejsza, ale nadal musi być oparta na bazie wiedzy. Jeśli to możliwe, wskaż podstawę prawną lub dokument, który odnosi się do danej kwestii.
|
|
||||||
6. **Nie wymyślaj informacji spoza kontekstu** – Jeśli brakuje Ci danych w dostępnych dokumentach, poinformuj użytkownika, że potrzebne jest inne źródło.
|
|
||||||
|
|
||||||
### 📌 Przykłady odpowiedzi:
|
|
||||||
|
|
||||||
**🟢 Pytanie o konkretny artykuł:**
|
|
||||||
- **Pytanie:** Jak brzmi art. 3 Kodeksu Pracy?
|
|
||||||
- **Odpowiedź:** Art. 3 Kodeksu Pracy stanowi: *"Pracodawcą jest jednostka organizacyjna, choćby nie posiadała osobowości prawnej, a także osoba fizyczna, jeżeli zatrudniają one pracowników."*
|
|
||||||
|
|
||||||
**🟢 Pytanie o zagadnienie:**
|
|
||||||
- **Pytanie:** Kiedy pracownik może iść na urlop?
|
|
||||||
- **Odpowiedź:** Pracownik nabywa prawo do urlopu proporcjonalnie do przepracowanego okresu. Zgodnie z art. 154 Kodeksu Pracy, wymiar urlopu wynosi 20 dni dla pracownika zatrudnionego krócej niż 10 lat oraz 26 dni dla pracownika z co najmniej 10-letnim stażem. Urlop powinien być udzielany zgodnie z planem urlopowym lub na wniosek pracownika, jeśli pracodawca wyrazi na to zgodę.
|
|
||||||
|
|
||||||
**🟢 Pytanie bez jasnej podstawy prawnej:**
|
|
||||||
- **Pytanie:** Czy pracodawca musi zapewnić wodę pitną w miejscu pracy?
|
|
||||||
- **Odpowiedź:** Tak, zgodnie z przepisami BHP pracodawca jest zobowiązany do zapewnienia pracownikom dostępu do wody pitnej. Obowiązek ten wynika z przepisów rozporządzenia dotyczącego ogólnych przepisów BHP, które określają warunki higieniczne w miejscu pracy.
|
|
||||||
|
|
||||||
Dostosuj odpowiedzi do poziomu wiedzy użytkownika i nie podawaj nazw plików, w których znajdują się dokumenty źródłowe.
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
|
||||||
|
# Przeczytaj uważnie przed uruchomieniem tego repo 📝
|
||||||
|
To jest biblia szkolenia modeli obsługiwanych przez Ably.do
|
||||||
|
|
||||||
|
## Konfiguracja Git 🔥
|
||||||
|
**git config --global credential.helper store** \
|
||||||
|
Przejdź do folderu, w którym będziesz przechowywał lokalne repo. (np. **cd /home**) \
|
||||||
|
Pobierz repo: \
|
||||||
|
**git clone https://repo.pokash.pl/POKASH.PL/ably.do.git** \
|
||||||
|
pierwszym razem zostaniesz poproszony o zalogowanie się.
|
||||||
|
|
||||||
|
## Konfiguracja Hugging Face Transpormers 🚀
|
||||||
|
**huggingface-cli login** \
|
||||||
|
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
|
||||||
|
|
||||||
|
**W przypadku niektórych obrazów modeli musisz przejść przez akceptację licencji**
|
||||||
|
|
||||||
|
## Trenowanie modelu 🔥
|
||||||
|
Przejdź do folderu, w którym będziesz pobierał wiedzę z repo. (np. /home). \
|
||||||
|
Pobierz najnowsze zmiany (**git pull**) \
|
||||||
|
Uruchom skrypt Python, który rozpocznie trenowanie modelu: \
|
||||||
|
**python3 hft.py**
|
||||||
|
|
||||||
|
## Wdrażanie modelu ✨
|
||||||
|
Po wytrenowaniu modelu,
|
||||||
|
musisz przekonwertować go do formatu GGUF, który obsługuje Ollama. \
|
||||||
|
Konwersja do GGUF
|
||||||
|
1. Skorzystaj z narzędzia dostarczonego przez Ollama do konwersji: \
|
||||||
|
**ollama convert your_model.bin --output your_model.gguf** \
|
||||||
|
2. Umieść przekonwertowany model w katalogu Ollama: \
|
||||||
|
**mv your_model.gguf ~/.ollama/models/**
|
||||||
|
|
||||||
|
Uruchomienie dostrojonego modelu \
|
||||||
|
Użyj nazwy swojego modelu w poleceniu: \
|
||||||
|
**ollama run your_model** *"Jakie są wymagania dotyczące ochrony słuchu?"*
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
fastapi
|
torch>=2.0.1
|
||||||
uvicorn
|
transformers>=4.30.2
|
||||||
ollama
|
datasets>=2.13.1
|
||||||
weaviate-client
|
Pillow>=9.4.0
|
||||||
unidecode
|
pytesseract>=0.3.10
|
||||||
elasticsearch
|
python-docx>=0.8.11
|
||||||
aiohttp
|
PyPDF2>=3.0.1
|
||||||
beautifulsoup4
|
huggingface-hub>=0.16.4
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||||
|
|
||||||
|
model_path = "./trained_model/gpt"
|
||||||
|
model = AutoModelForCausalLM.from_pretrained(model_path)
|
||||||
|
tokenizer = AutoTokenizer.from_pretrained(model_path)
|
||||||
|
tokenizer.pad_token = tokenizer.eos_token
|
||||||
|
model.config.pad_token_id = tokenizer.eos_token_id
|
||||||
|
|
||||||
|
def generate_response(prompt, max_length=1000):
|
||||||
|
inputs = tokenizer(prompt, return_tensors="pt", padding=True, truncation=True)
|
||||||
|
outputs = model.generate(
|
||||||
|
inputs.input_ids,
|
||||||
|
attention_mask=inputs.attention_mask,
|
||||||
|
pad_token_id=tokenizer.pad_token_id,
|
||||||
|
max_length=100
|
||||||
|
)
|
||||||
|
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
|
||||||
|
return response
|
||||||
|
|
||||||
|
prompt = "Zacytuj paragraf pierwszy artykułu 154 Kodeksu pracy."
|
||||||
|
response = generate_response(prompt)
|
||||||
|
print(response)
|
||||||
Loading…
Reference in New Issue