Compare commits

...

9 Commits

Author SHA1 Message Date
l.gabrysiak 52f4e8579e Dodanie historii wiadomości 2025-03-02 21:23:11 +01:00
l.gabrysiak 38c7ab8956 Wyszukiwanie ONLINE 2025-03-02 21:13:50 +01:00
l.gabrysiak f83da185f8 Zapis w ElasticSearch 2025-02-28 18:12:08 +01:00
l.gabrysiak 3dbba3f2d6 code ready to deploy + prompty 2025-02-28 11:44:28 +01:00
l.gabrysiak ad08ce74ce code ready to deploy 2025-02-27 20:31:55 +01:00
l.gabrysiak 10f40c54ee init new branch 2025-02-27 15:58:30 +01:00
l.gabrysiak 481cae2a61 init 2025-02-27 13:10:09 +01:00
l.gabrysiak c617cb3f95 code ready to deploy 2025-02-27 13:09:02 +01:00
l.gabrysiak b68ffa3112 init new branch 2025-02-26 20:38:27 +01:00
19 changed files with 478 additions and 4650 deletions

30
Dockerfile Normal file
View File

@ -0,0 +1,30 @@
# 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"]

View File

@ -1,119 +0,0 @@
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 = "allegro/herbert-base-cased"
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()
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:
article = ' '.join(article.strip().split())
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)
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})
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.pad_token = tokenizer.eos_token
tokenizer.add_special_tokens({"additional_special_tokens": SPECIAL_TOKENS})
print(f"Pad token: {tokenizer.pad_token}")
print(f"Pad token ID: {tokenizer.pad_token_id}")
# 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=512,
return_tensors="pt"
)
tokenized["labels"] = tokenized["input_ids"].clone()
return tokenized
tokenized_dataset = dataset.map(tokenize_function, batched=True, remove_columns=dataset.column_names)
# Model i data collator
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)
model.resize_token_embeddings(len(tokenizer))
model.config.pad_token_id = tokenizer.pad_token_id
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False
)
# Konfiguracja treningu
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=32,
per_device_train_batch_size=2,
learning_rate=1e-5,
logging_steps=10,
weight_decay=0.01,
report_to="none",
save_strategy="steps",
save_steps=500,
evaluation_strategy="steps",
eval_steps=500,
load_best_model_at_end=True,
)
# Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset,
eval_dataset=tokenized_dataset,
data_collator=data_collator
)
print("Rozpoczęcie treningu...")
trainer.train()
trainer.save_model("./trained_model/allegro")
tokenizer.save_pretrained("./trained_model/allegro")
if __name__ == "__main__":
main()

View File

@ -1,5 +0,0 @@
{
"kodekspracy": "Kodeks Pracy",
"urlopproporcjonalny": "Rozporządzenie BHP",
"ustawaopanstwowejinspekcjipracy": "Ustawa o Państwowej inspekcji pracy"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
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ę.

2
entrypoint.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
python /app/ollama_service.py

View File

@ -1,93 +0,0 @@
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)

119
gemma.py
View File

@ -1,119 +0,0 @@
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!")

118
gpt.py
View File

@ -1,118 +0,0 @@
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()

261
hft.py
View File

@ -1,261 +0,0 @@
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()

385
ollama_service.py Normal file
View File

@ -0,0 +1,385 @@
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)

View File

@ -0,0 +1 @@
Na podstawie powyższych informacji odpowiedz na pytanie: {query}.

1
prompts/prompt_data.txt Normal file
View File

@ -0,0 +1 @@
Oto fragmenty aktów prawnych i dokumentów powiązanych z pytaniem użytkownika: \n\n

26
prompts/prompt_seach.txt Normal file
View File

@ -0,0 +1,26 @@
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}

25
prompts/prompt_system.txt Normal file
View File

@ -0,0 +1,25 @@
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.

View File

@ -1,35 +0,0 @@
# 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?"*

View File

@ -1,8 +1,8 @@
torch>=2.0.1
transformers>=4.30.2
datasets>=2.13.1
Pillow>=9.4.0
pytesseract>=0.3.10
python-docx>=0.8.11
PyPDF2>=3.0.1
huggingface-hub>=0.16.4
fastapi
uvicorn
ollama
weaviate-client
unidecode
elasticsearch
aiohttp
beautifulsoup4

22
test.py
View File

@ -1,22 +0,0 @@
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)