first commit
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
data/
|
||||||
|
*.csv
|
||||||
|
*.xlsx
|
||||||
|
*.json
|
||||||
|
.pytest_cache/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
65
README.md
Normal file
65
README.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# 🕷️ Web Scraping & RAG MCP Server Projesi
|
||||||
|
|
||||||
|
Bu proje, çeşitli web sitelerindeki (özellikle yazılım dokümantasyonlarındaki) verileri toplayan, temizleyerek Markdown formatına getiren ve ardından bu verileri lokal bir yapay zeka vektör veritabanına aktararak **Model Context Protocol (MCP)** üzerinden dışarıya bir araç (tool) olarak sunan kapsamlı bir sistemdir.
|
||||||
|
|
||||||
|
## 🌟 Sistemin Modülleri
|
||||||
|
|
||||||
|
Proje temel olarak 3 ana modülden oluşmaktadır:
|
||||||
|
|
||||||
|
### 1. Web Scraping Altyapısı (Kazıyıcılar)
|
||||||
|
- **`scraper/static_scraper.py`**: `requests` ve `BeautifulSoup` kullanarak statik ve çok hızlı veri çekmek içindir.
|
||||||
|
- **`scraper/dynamic_scraper.py`**: `Playwright` kullanarak JavaScript ile render olan (dinamik) sitelerden veri çekmek içindir.
|
||||||
|
- **`main.py`**: Bu temel kazıyıcıların örnek kullanımını ve sonuçların `data/*.csv` formatında kaydedilmesini içerir.
|
||||||
|
|
||||||
|
### 2. Docs to Markdown Crawler (Doküman Örümceği)
|
||||||
|
- **`scraper/docs_crawler.py`**: Belirli bir dokümantasyon adresini (örneğin Next.js veya Nuxt) başlangıç kabul ederek o site içindeki tüm bağlantıları (linkleri) gezen sistemdir.
|
||||||
|
- **Nasıl Çalışır?**: Girdiği sayfalardaki menü (`<nav>`), altlık (`<footer>`) gibi kısımları temizler. Geriye kalan saf metni ve kod bloklarını `markdownify` kütüphanesi ile tertemiz bir **.md** formatına çevirir.
|
||||||
|
- **Çalıştırma Betikleri**:
|
||||||
|
- `next.py` (Next.js docs için)
|
||||||
|
- `next_auth.py` (NextAuth docs için)
|
||||||
|
- `nuxt.py` (Nuxt 4 docs için)
|
||||||
|
- Çıktılar otomatik olarak `data/md_docs/<domain_adi>` klasörüne hiyerarşik bir şekilde yerleştirilir.
|
||||||
|
|
||||||
|
### 3. Lokal RAG ve MCP Server (Yapay Zeka Entegrasyonu)
|
||||||
|
Bu modül, indirilen saf Markdown dosyalarının Claude, ChatGPT gibi yapay zekalar tarafından aranabilmesini sağlar.
|
||||||
|
- **`rag_local/ingest.py`**:
|
||||||
|
- `data/md_docs/` dizinindeki tüm markdown dosyalarını okur.
|
||||||
|
- Metinleri yaklaşık 1000 karakterlik parçalara böler (chunking).
|
||||||
|
- Lokal yapay zeka modeli olan `all-MiniLM-L6-v2` (`sentence-transformers`) ile bu metinleri vektörleştirir.
|
||||||
|
- Vektörleri `chroma_db/` isimli, tamamen lokal çalışan ve kurulum gerektirmeyen klasör tabanlı bir veritabanına yazar.
|
||||||
|
- **`rag_local/mcp_server.py`**:
|
||||||
|
- FastMCP kullanarak bir Model Context Protocol sunucusu başlatır.
|
||||||
|
- Dışarıya **`search_documentation`** adında bir yetenek sunar.
|
||||||
|
- Yapay zeka bu yeteneği kullanarak bir kavram sorduğunda (Örn: "Next.js routing"), veritabanında semantik (anlamsal) arama yapılır ve en iyi eşleşen dokümanlar saniyeler içinde asistana iletilir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Kurulum ve Çalıştırma Rehberi
|
||||||
|
|
||||||
|
### Adım 1: Yeni Doküman Kazımak (İsteğe Bağlı)
|
||||||
|
Eğer sisteme yeni dokümanlar eklemek isterseniz, örneğin `nuxt.py` dosyasını çalıştırın:
|
||||||
|
```bash
|
||||||
|
python nuxt.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adım 2: Vektör Veritabanını Güncellemek (Ingest)
|
||||||
|
Markdown dosyalarınız indikçe veya yenilendikçe bu dosyaları yapay zekanın anlayabileceği vektör formatına çevirmeniz gerekir:
|
||||||
|
```bash
|
||||||
|
# İlk çalıştırmada biraz sürebilir, çünkü parçaları tek tek vektöre çevirir.
|
||||||
|
python rag_local/ingest.py
|
||||||
|
```
|
||||||
|
**Not:** İşlem bittiğinde proje ana dizininde `chroma_db` isimli bir klasör oluşur. Bu sizin yapay zeka "hafızanızdır".
|
||||||
|
|
||||||
|
### Adım 3: MCP Server'ı Ayağa Kaldırmak
|
||||||
|
Vektörleştirme bittikten sonra bu veriyi Claude Masaüstü (veya uyumlu MCP istemcileri) üzerinden kullanmak için sunucuyu başlatın:
|
||||||
|
```bash
|
||||||
|
python rag_local/mcp_server.py
|
||||||
|
```
|
||||||
|
**Not:** Claude Desktop içerisindeki `mcp.json` konfigürasyonuna bu sunucunun dosya yolunu ekleyerek, Claude'un direkt olarak bu dokümanlar üzerinde RAG (Arama) yapmasını sağlayabilirsiniz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Gelecek Geliştirmeler (Roadmap)
|
||||||
|
- Vektör aramasını lokal ChromaDB yerine `.env` dosyasında yer alan **Postgres (`pgvector`)** sistemine taşıma.
|
||||||
|
- Sorulan soruların anlık cevaplanması için **Redis** cache (önbellek) mekanizması ekleme.
|
||||||
|
- Yüksek performans için MCP Server uygulamasını **Go (Golang)** dili ile yeniden yazma.
|
||||||
BIN
chroma_db/chroma.sqlite3
Normal file
BIN
chroma_db/chroma.sqlite3
Normal file
Binary file not shown.
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/data_level0.bin
Normal file
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/data_level0.bin
Normal file
Binary file not shown.
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/header.bin
Normal file
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/header.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/length.bin
Normal file
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/length.bin
Normal file
Binary file not shown.
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/link_lists.bin
Normal file
BIN
chroma_db/d2cd470f-d933-4a5f-bed0-dc279b7cbda2/link_lists.bin
Normal file
Binary file not shown.
24
fiber.py
Normal file
24
fiber.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
from scraper.docs_crawler import DocsCrawler
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Docs to Markdown Crawler Başlatılıyor (Go Fiber) ---")
|
||||||
|
|
||||||
|
# Başlangıç URL'i (Go Fiber dokümantasyonu)
|
||||||
|
start_url = "https://docs.gofiber.io/"
|
||||||
|
|
||||||
|
MAX_PAGES = 5000
|
||||||
|
|
||||||
|
crawler = DocsCrawler(
|
||||||
|
start_url=start_url,
|
||||||
|
max_pages=MAX_PAGES,
|
||||||
|
headless=True
|
||||||
|
)
|
||||||
|
|
||||||
|
crawler.crawl()
|
||||||
|
|
||||||
|
print("\n--- İşlem Tamamlandı ---")
|
||||||
|
print(f"Oluşturulan markdown dosyalarını kontrol et: data/md_docs/docs.gofiber.io/")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
26
main.py
Normal file
26
main.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
from scraper.docs_crawler import DocsCrawler
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Docs to Markdown Crawler Başlatılıyor ---")
|
||||||
|
|
||||||
|
# Başlangıç URL'i (Örn: Next.js dokümantasyonu)
|
||||||
|
start_url = "https://nextjs.org/docs"
|
||||||
|
|
||||||
|
# Test amaçlı sadece ilk 10 sayfayı çekecek şekilde sınırlandıralım
|
||||||
|
# Gerçek kullanımda max_pages=1000 gibi büyük bir sayı verilebilir.
|
||||||
|
MAX_PAGES = 5000
|
||||||
|
|
||||||
|
crawler = DocsCrawler(
|
||||||
|
start_url=start_url,
|
||||||
|
max_pages=MAX_PAGES,
|
||||||
|
headless=True
|
||||||
|
)
|
||||||
|
|
||||||
|
crawler.crawl()
|
||||||
|
|
||||||
|
print("\n--- İşlem Tamamlandı ---")
|
||||||
|
print(f"Oluşturulan markdown dosyalarını kontrol et: data/md_docs/nextjs.org/")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
26
next.py
Normal file
26
next.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
from scraper.docs_crawler import DocsCrawler
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Docs to Markdown Crawler Başlatılıyor ---")
|
||||||
|
|
||||||
|
# Başlangıç URL'i (Örn: Next.js dokümantasyonu)
|
||||||
|
start_url = "https://nextjs.org/docs"
|
||||||
|
|
||||||
|
# Test amaçlı sadece ilk 10 sayfayı çekecek şekilde sınırlandıralım
|
||||||
|
# Gerçek kullanımda max_pages=1000 gibi büyük bir sayı verilebilir.
|
||||||
|
MAX_PAGES = 5000
|
||||||
|
|
||||||
|
crawler = DocsCrawler(
|
||||||
|
start_url=start_url,
|
||||||
|
max_pages=MAX_PAGES,
|
||||||
|
headless=True
|
||||||
|
)
|
||||||
|
|
||||||
|
crawler.crawl()
|
||||||
|
|
||||||
|
print("\n--- İşlem Tamamlandı ---")
|
||||||
|
print(f"Oluşturulan markdown dosyalarını kontrol et: data/md_docs/nextjs.org/")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
23
next_auth.py
Normal file
23
next_auth.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import os
|
||||||
|
from scraper.docs_crawler import DocsCrawler
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- NextAuth Docs Crawler Başlatılıyor ---")
|
||||||
|
|
||||||
|
start_url = "https://next-auth.js.org/getting-started/introduction"
|
||||||
|
|
||||||
|
# Tüm next-auth.js.org domainini taramak için allowed_path="/" veriyoruz
|
||||||
|
crawler = DocsCrawler(
|
||||||
|
start_url=start_url,
|
||||||
|
max_pages=1000,
|
||||||
|
headless=True,
|
||||||
|
allowed_path="/"
|
||||||
|
)
|
||||||
|
|
||||||
|
crawler.crawl()
|
||||||
|
|
||||||
|
print("\n--- İşlem Tamamlandı ---")
|
||||||
|
print(f"Oluşturulan markdown dosyalarını kontrol et: data/md_docs/next-auth.js.org/")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
23
nuxt.py
Normal file
23
nuxt.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import os
|
||||||
|
from scraper.docs_crawler import DocsCrawler
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("--- Nuxt 4 Docs Crawler Başlatılıyor ---")
|
||||||
|
|
||||||
|
start_url = "https://nuxt.com/docs/4.x/getting-started/"
|
||||||
|
|
||||||
|
# Sadece Nuxt 4.x dökümanlarını (https://nuxt.com/docs/4.x/...) taramak için
|
||||||
|
crawler = DocsCrawler(
|
||||||
|
start_url=start_url,
|
||||||
|
max_pages=1500,
|
||||||
|
headless=True,
|
||||||
|
allowed_path="/docs/4.x/"
|
||||||
|
)
|
||||||
|
|
||||||
|
crawler.crawl()
|
||||||
|
|
||||||
|
print("\n--- İşlem Tamamlandı ---")
|
||||||
|
print(f"Oluşturulan markdown dosyalarını kontrol et: data/md_docs/nuxt.com/")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
88
rag_local/ingest.py
Normal file
88
rag_local/ingest.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
from langchain_text_splitters import MarkdownTextSplitter
|
||||||
|
import chromadb
|
||||||
|
from chromadb.utils import embedding_functions
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
def ingest_docs(docs_dir="data/md_docs", chroma_path="chroma_db", collection_name="docs"):
|
||||||
|
logging.info(f"ChromaDB başlatılıyor... ({chroma_path})")
|
||||||
|
|
||||||
|
# ChromaDB client oluştur (klasöre kaydeder)
|
||||||
|
client = chromadb.PersistentClient(path=chroma_path)
|
||||||
|
|
||||||
|
# Default embedding function (sentence-transformers: all-MiniLM-L6-v2)
|
||||||
|
# Bu model boyut olarak küçük (yaklaşık 80MB) ve lokalde çok hızlı çalışır.
|
||||||
|
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
||||||
|
|
||||||
|
# Collection oluştur veya var olanı al
|
||||||
|
collection = client.get_or_create_collection(name=collection_name, embedding_function=sentence_transformer_ef)
|
||||||
|
|
||||||
|
# Markdown dosyalarını bul
|
||||||
|
md_files = glob.glob(os.path.join(docs_dir, "**", "*.md"), recursive=True)
|
||||||
|
logging.info(f"Toplam {len(md_files)} markdown dosyası bulundu.")
|
||||||
|
|
||||||
|
# Metinleri anlamlı parçalara bölecek (chunk) nesne
|
||||||
|
# Her parça yaklaşık 1000 karakter olacak, aralarında 200 karakter örtüşme (overlap) olacak.
|
||||||
|
splitter = MarkdownTextSplitter(chunk_size=1000, chunk_overlap=200)
|
||||||
|
|
||||||
|
batch_documents = []
|
||||||
|
batch_metadatas = []
|
||||||
|
batch_ids = []
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for file_path in md_files:
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Metni parçalara böl
|
||||||
|
chunks = splitter.split_text(content)
|
||||||
|
|
||||||
|
for i, chunk in enumerate(chunks):
|
||||||
|
# ID örneği: data/md_docs/nextjs.org/docs.md_chunk_0
|
||||||
|
chunk_id = f"{file_path}_chunk_{i}"
|
||||||
|
|
||||||
|
batch_documents.append(chunk)
|
||||||
|
batch_metadatas.append({"source": file_path, "chunk_index": i})
|
||||||
|
batch_ids.append(chunk_id)
|
||||||
|
|
||||||
|
# Belleği doldurmamak için her 100 parçada bir veritabanına yaz
|
||||||
|
if len(batch_documents) >= 100:
|
||||||
|
collection.add(
|
||||||
|
documents=batch_documents,
|
||||||
|
metadatas=batch_metadatas,
|
||||||
|
ids=batch_ids
|
||||||
|
)
|
||||||
|
count += len(batch_documents)
|
||||||
|
logging.info(f"{count} parça vektörleştirildi ve veritabanına eklendi...")
|
||||||
|
batch_documents = []
|
||||||
|
batch_metadatas = []
|
||||||
|
batch_ids = []
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Dosya okunamadı ({file_path}): {e}")
|
||||||
|
|
||||||
|
# Kalan son parçaları yaz
|
||||||
|
if batch_documents:
|
||||||
|
collection.add(
|
||||||
|
documents=batch_documents,
|
||||||
|
metadatas=batch_metadatas,
|
||||||
|
ids=batch_ids
|
||||||
|
)
|
||||||
|
count += len(batch_documents)
|
||||||
|
|
||||||
|
logging.info(f"İşlem tamam! Toplam {count} metin parçası (chunk) vektörleştirilerek ChromaDB'ye işlendi.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Bu dosya rag_local dizininde olsa bile, çalışma dizini olarak mdscrap/ ana dizinini kullanalım
|
||||||
|
# böylece 'data/md_docs' ve 'chroma_db' ana dizinde oluşur.
|
||||||
|
|
||||||
|
# Çalışma dizinini ana dizine ayarla
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
main_dir = os.path.dirname(script_dir)
|
||||||
|
os.chdir(main_dir)
|
||||||
|
|
||||||
|
ingest_docs()
|
||||||
51
rag_local/mcp_server.log
Normal file
51
rag_local/mcp_server.log
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
2026-04-23 03:23:02,022 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-23 03:23:07,881 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/modules.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:07,881 - WARNING - Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
|
||||||
|
2026-04-23 03:23:07,926 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/modules.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:08,071 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/config_sentence_transformers.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:08,116 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/config_sentence_transformers.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:08,116 - INFO - Loading SentenceTransformer model from sentence-transformers/all-MiniLM-L6-v2.
|
||||||
|
2026-04-23 03:23:08,263 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/config_sentence_transformers.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:08,308 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/config_sentence_transformers.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:08,457 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/README.md "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:08,501 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/README.md "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:08,657 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/modules.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:08,702 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/modules.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:08,847 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/sentence_bert_config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:08,892 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/sentence_bert_config.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:09,040 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/adapter_config.json "HTTP/1.1 404 Not Found"
|
||||||
|
2026-04-23 03:23:09,185 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:09,230 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/config.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:09,497 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/processor_config.json "HTTP/1.1 404 Not Found"
|
||||||
|
2026-04-23 03:23:09,649 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/preprocessor_config.json "HTTP/1.1 404 Not Found"
|
||||||
|
2026-04-23 03:23:09,798 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/video_preprocessor_config.json "HTTP/1.1 404 Not Found"
|
||||||
|
2026-04-23 03:23:09,948 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/preprocessor_config.json "HTTP/1.1 404 Not Found"
|
||||||
|
2026-04-23 03:23:10,094 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer_config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:10,138 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/tokenizer_config.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:10,289 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:10,334 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/config.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:10,487 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:10,532 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/config.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:10,678 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer_config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:10,723 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/tokenizer_config.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:10,883 - INFO - HTTP Request: GET https://huggingface.co/api/models/sentence-transformers/all-MiniLM-L6-v2/tree/main/additional_chat_templates?recursive=false&expand=false "HTTP/1.1 404 Not Found"
|
||||||
|
2026-04-23 03:23:11,036 - INFO - HTTP Request: GET https://huggingface.co/api/models/sentence-transformers/all-MiniLM-L6-v2/tree/main?recursive=true&expand=false "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:11,235 - INFO - HTTP Request: HEAD https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/1_Pooling/config.json "HTTP/1.1 307 Temporary Redirect"
|
||||||
|
2026-04-23 03:23:11,279 - INFO - HTTP Request: HEAD https://huggingface.co/api/resolve-cache/models/sentence-transformers/all-MiniLM-L6-v2/c9745ed1d9f207416be6d2e6f8de32d1f16199bf/1_Pooling%2Fconfig.json "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:23:11,429 - INFO - HTTP Request: GET https://huggingface.co/api/models/sentence-transformers/all-MiniLM-L6-v2 "HTTP/1.1 200 OK"
|
||||||
|
2026-04-23 03:24:50,994 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-23 15:07:20,808 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-23 18:37:20,770 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-24 04:12:05,022 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-24 04:13:23,239 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-24 06:14:03,870 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-24 16:32:57,441 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-24 16:53:45,204 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-24 16:54:03,188 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-25 03:37:55,778 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-25 18:38:12,228 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-25 21:01:15,905 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-26 00:40:32,063 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-26 00:59:41,360 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-26 01:00:53,094 - INFO - DocsRAGServer başlatılıyor...
|
||||||
|
2026-04-26 01:17:01,563 - INFO - DocsRAGServer başlatılıyor...
|
||||||
88
rag_local/mcp_server.py
Normal file
88
rag_local/mcp_server.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import chromadb
|
||||||
|
from chromadb.utils import embedding_functions
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
# Log ayarları: Sadece kendi loglarımızı yazalım, diğer kütüphaneler (httpx vb.) karışmasın.
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
logger = logging.getLogger("DocsRAGServer")
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
# Sadece bu logger için bir dosya işleyicisi (handler) ekleyelim
|
||||||
|
file_handler = logging.FileHandler(os.path.join(SCRIPT_DIR, 'mcp_server.log'))
|
||||||
|
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
||||||
|
logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# Propagate'i kapatalım ki root logger'a gitmesin
|
||||||
|
logger.propagate = False
|
||||||
|
|
||||||
|
# Dış kütüphanelerin gereksiz loglarını susturmak için:
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
logging.getLogger("sentence_transformers").setLevel(logging.WARNING)
|
||||||
|
|
||||||
|
logger.info("DocsRAGServer başlatılıyor...")
|
||||||
|
|
||||||
|
# MCP Server'ı oluştur
|
||||||
|
# FastMCP, standart stdio/SSE vb. ayarlarını otomatik halleder.
|
||||||
|
mcp = FastMCP("DocsRAGServer")
|
||||||
|
|
||||||
|
# Çalışma dizinine göre ChromaDB klasörünün yolunu bul
|
||||||
|
MAIN_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
CHROMA_PATH = os.path.join(MAIN_DIR, "chroma_db")
|
||||||
|
|
||||||
|
# ChromaDB'ye bağlan
|
||||||
|
# Not: Sunucu ayağa kalktığında sadece bir kez bağlanır.
|
||||||
|
try:
|
||||||
|
client = chromadb.PersistentClient(path=CHROMA_PATH)
|
||||||
|
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
|
||||||
|
collection = client.get_or_create_collection(name="docs", embedding_function=sentence_transformer_ef)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"UYARI: ChromaDB'ye bağlanırken hata oluştu. Henüz ingest.py çalıştırılmamış olabilir mi? Hata: {e}"
|
||||||
|
print(error_msg)
|
||||||
|
logger.error(error_msg)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def search_documentation(query: str, num_results: int = 5) -> str:
|
||||||
|
"""
|
||||||
|
Kullanıcının sorusuna en uygun dokümantasyon parçalarını vektör veritabanından bulup döndürür.
|
||||||
|
Bu araç (tool) sistemin belleği gibi davranır ve AI'ın kod/kavram araması yapmasını sağlar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: Aranacak soru veya anahtar kelimeler (Örn: 'app router data fetching' veya 'next-auth credentials provider').
|
||||||
|
num_results: Döndürülecek maksimum sonuç sayısı (varsayılan: 5).
|
||||||
|
"""
|
||||||
|
logger.info(f"Arama isteği alındı: query='{query}', num_results={num_results}")
|
||||||
|
try:
|
||||||
|
# Sorguyu vektöre çevirip en benzer metinleri (semantic search) getir
|
||||||
|
results = collection.query(
|
||||||
|
query_texts=[query],
|
||||||
|
n_results=num_results
|
||||||
|
)
|
||||||
|
|
||||||
|
if not results['documents'] or not results['documents'][0]:
|
||||||
|
logger.info("Sorgu için ilgili bir doküman bulunamadı.")
|
||||||
|
return "İlgili bir doküman bulunamadı."
|
||||||
|
|
||||||
|
docs = results['documents'][0]
|
||||||
|
metadatas = results['metadatas'][0]
|
||||||
|
distances = results['distances'][0]
|
||||||
|
|
||||||
|
response_parts = [f"Arama Sorgusu: '{query}'\nBulunan En İyi Eşleşmeler:\n"]
|
||||||
|
|
||||||
|
for i, (doc, meta, dist) in enumerate(zip(docs, metadatas, distances)):
|
||||||
|
source = meta.get("source", "Bilinmeyen Kaynak")
|
||||||
|
# dist (distance) değeri ChromaDB için varsayılan olarak L2 mesafesidir, küçük olan daha benzerdir.
|
||||||
|
response_parts.append(f"--- Sonuç {i+1} (Kaynak: {source}, Uzaklık: {dist:.4f}) ---\n{doc}\n")
|
||||||
|
|
||||||
|
logger.info(f"Sorgu başarılı, {len(docs)} sonuç bulundu.")
|
||||||
|
return "\n".join(response_parts)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Arama sırasında hata oluştu: {str(e)}")
|
||||||
|
return f"Arama sırasında hata oluştu: {str(e)}"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Server'ı stdio üzerinden çalıştır. (Claude veya diğer MCP istemcileri bu şekilde bağlanır)
|
||||||
|
mcp.run()
|
||||||
124
requirements.txt
Normal file
124
requirements.txt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
annotated-doc==0.0.4
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==4.13.0
|
||||||
|
attrs==26.1.0
|
||||||
|
bcrypt==5.0.0
|
||||||
|
beautifulsoup4==4.14.3
|
||||||
|
build==1.4.4
|
||||||
|
certifi==2026.4.22
|
||||||
|
charset-normalizer==3.4.7
|
||||||
|
chromadb==1.5.8
|
||||||
|
click==8.3.3
|
||||||
|
cuda-bindings==13.2.0
|
||||||
|
cuda-pathfinder==1.5.3
|
||||||
|
cuda-toolkit==13.0.2
|
||||||
|
durationpy==0.10
|
||||||
|
filelock==3.29.0
|
||||||
|
flatbuffers==25.12.19
|
||||||
|
fsspec==2026.3.0
|
||||||
|
googleapis-common-protos==1.74.0
|
||||||
|
greenlet==3.4.0
|
||||||
|
grpcio==1.80.0
|
||||||
|
h11==0.16.0
|
||||||
|
hf-xet==1.4.3
|
||||||
|
httpcore==1.0.9
|
||||||
|
httptools==0.7.1
|
||||||
|
httpx==0.28.1
|
||||||
|
huggingface_hub==1.12.0
|
||||||
|
idna==3.13
|
||||||
|
importlib_metadata==8.7.1
|
||||||
|
importlib_resources==7.1.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
joblib==1.5.3
|
||||||
|
jsonpatch==1.33
|
||||||
|
jsonpointer==3.1.1
|
||||||
|
jsonschema==4.26.0
|
||||||
|
jsonschema-specifications==2025.9.1
|
||||||
|
kubernetes==35.0.0
|
||||||
|
langchain-core==1.3.1
|
||||||
|
langchain-text-splitters==1.1.2
|
||||||
|
langsmith==0.7.35
|
||||||
|
lxml==6.1.0
|
||||||
|
markdown-it-py==4.0.0
|
||||||
|
markdownify==1.2.2
|
||||||
|
MarkupSafe==3.0.3
|
||||||
|
mdurl==0.1.2
|
||||||
|
mmh3==5.2.1
|
||||||
|
mpmath==1.3.0
|
||||||
|
networkx==3.6.1
|
||||||
|
numpy==2.4.4
|
||||||
|
nvidia-cublas==13.1.0.3
|
||||||
|
nvidia-cuda-cupti==13.0.85
|
||||||
|
nvidia-cuda-nvrtc==13.0.88
|
||||||
|
nvidia-cuda-runtime==13.0.96
|
||||||
|
nvidia-cudnn-cu13==9.19.0.56
|
||||||
|
nvidia-cufft==12.0.0.61
|
||||||
|
nvidia-cufile==1.15.1.6
|
||||||
|
nvidia-curand==10.4.0.35
|
||||||
|
nvidia-cusolver==12.0.4.66
|
||||||
|
nvidia-cusparse==12.6.3.3
|
||||||
|
nvidia-cusparselt-cu13==0.8.0
|
||||||
|
nvidia-nccl-cu13==2.28.9
|
||||||
|
nvidia-nvjitlink==13.0.88
|
||||||
|
nvidia-nvshmem-cu13==3.4.5
|
||||||
|
nvidia-nvtx==13.0.85
|
||||||
|
oauthlib==3.3.1
|
||||||
|
onnxruntime==1.25.0
|
||||||
|
opentelemetry-api==1.41.1
|
||||||
|
opentelemetry-exporter-otlp-proto-common==1.41.1
|
||||||
|
opentelemetry-exporter-otlp-proto-grpc==1.41.1
|
||||||
|
opentelemetry-proto==1.41.1
|
||||||
|
opentelemetry-sdk==1.41.1
|
||||||
|
opentelemetry-semantic-conventions==0.62b1
|
||||||
|
orjson==3.11.8
|
||||||
|
overrides==7.7.0
|
||||||
|
packaging==26.1
|
||||||
|
playwright==1.58.0
|
||||||
|
protobuf==6.33.6
|
||||||
|
pybase64==1.4.3
|
||||||
|
pydantic==2.13.3
|
||||||
|
pydantic-settings==2.14.0
|
||||||
|
pydantic_core==2.46.3
|
||||||
|
pyee==13.0.1
|
||||||
|
Pygments==2.20.0
|
||||||
|
PyPika==0.51.1
|
||||||
|
pyproject_hooks==1.2.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
python-dotenv==1.2.2
|
||||||
|
PyYAML==6.0.3
|
||||||
|
referencing==0.37.0
|
||||||
|
regex==2026.4.4
|
||||||
|
requests==2.33.1
|
||||||
|
requests-oauthlib==2.0.0
|
||||||
|
requests-toolbelt==1.0.0
|
||||||
|
rich==15.0.0
|
||||||
|
rpds-py==0.30.0
|
||||||
|
safetensors==0.7.0
|
||||||
|
scikit-learn==1.8.0
|
||||||
|
scipy==1.17.1
|
||||||
|
sentence-transformers==5.4.1
|
||||||
|
setuptools==81.0.0
|
||||||
|
shellingham==1.5.4
|
||||||
|
six==1.17.0
|
||||||
|
soupsieve==2.8.3
|
||||||
|
sympy==1.14.0
|
||||||
|
tenacity==9.1.4
|
||||||
|
threadpoolctl==3.6.0
|
||||||
|
tokenizers==0.22.2
|
||||||
|
torch==2.11.0
|
||||||
|
tqdm==4.67.3
|
||||||
|
transformers==5.6.2
|
||||||
|
triton==3.6.0
|
||||||
|
typer==0.24.2
|
||||||
|
typing-inspection==0.4.2
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
uuid_utils==0.14.1
|
||||||
|
uvicorn==0.46.0
|
||||||
|
uvloop==0.22.1
|
||||||
|
watchfiles==1.1.1
|
||||||
|
websocket-client==1.9.0
|
||||||
|
websockets==16.0
|
||||||
|
xxhash==3.6.0
|
||||||
|
zipp==3.23.1
|
||||||
|
zstandard==0.25.0
|
||||||
1
scraper/__init__.py
Normal file
1
scraper/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Bu dosya scraper dizinini bir Python paketi haline getirir.
|
||||||
138
scraper/docs_crawler.py
Normal file
138
scraper/docs_crawler.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from urllib.parse import urlparse, urljoin
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from markdownify import markdownify as md
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
class DocsCrawler:
|
||||||
|
def __init__(self, start_url: str, max_pages: int = 50, headless: bool = True, allowed_path: str = None):
|
||||||
|
self.start_url = start_url
|
||||||
|
self.max_pages = max_pages
|
||||||
|
self.headless = headless
|
||||||
|
self.visited = set()
|
||||||
|
self.queue = [start_url]
|
||||||
|
|
||||||
|
parsed_url = urlparse(start_url)
|
||||||
|
self.base_domain = parsed_url.netloc
|
||||||
|
self.base_path = allowed_path if allowed_path is not None else parsed_url.path
|
||||||
|
|
||||||
|
# Sadece bu prefix ile başlayan linkleri gezeceğiz
|
||||||
|
self.url_prefix = f"{parsed_url.scheme}://{self.base_domain}{self.base_path}"
|
||||||
|
|
||||||
|
def is_valid_url(self, url: str) -> bool:
|
||||||
|
"""Sadece aynı domain ve prefix altındaki doküman linklerini geçerli sayar."""
|
||||||
|
if url in self.visited:
|
||||||
|
return False
|
||||||
|
if not url.startswith(self.url_prefix):
|
||||||
|
return False
|
||||||
|
# Gereksiz dosyaları atla
|
||||||
|
if any(url.endswith(ext) for ext in [".png", ".jpg", ".pdf", ".zip", ".json"]):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def save_markdown(self, url: str, content: str):
|
||||||
|
"""Çekilen markdown'ı URL yapısına göre klasörleyerek kaydeder."""
|
||||||
|
parsed = urlparse(url)
|
||||||
|
# Örn: /docs/app/building-your-application -> docs/app/building-your-application
|
||||||
|
path = parsed.path.strip("/")
|
||||||
|
if not path:
|
||||||
|
path = "index"
|
||||||
|
|
||||||
|
# Dosya yolu oluştur
|
||||||
|
file_path = os.path.join("data", "md_docs", self.base_domain, f"{path}.md")
|
||||||
|
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
logging.info(f"Kaydedildi: {file_path}")
|
||||||
|
|
||||||
|
def extract_main_content(self, html_content: str) -> str:
|
||||||
|
"""HTML içerisinden menüleri/sidearları atıp asıl metni alır."""
|
||||||
|
soup = BeautifulSoup(html_content, "lxml")
|
||||||
|
|
||||||
|
# Çoğu modern docs sitesi içeriği article veya main etiketinde tutar.
|
||||||
|
main_content = soup.find("article") or soup.find("main")
|
||||||
|
|
||||||
|
# Eğer bulunamazsa div id="content" gibi ortak yapıları ara
|
||||||
|
if not main_content:
|
||||||
|
main_content = soup.find("div", id="content") or soup.find("div", class_="content")
|
||||||
|
|
||||||
|
# Hiçbiri yoksa fallback olarak body kullan
|
||||||
|
if not main_content:
|
||||||
|
main_content = soup.find("body")
|
||||||
|
|
||||||
|
# Gereksiz script, style ve nav etiketlerini temizle
|
||||||
|
if main_content:
|
||||||
|
for unwanted in main_content(["script", "style", "nav", "footer", "header"]):
|
||||||
|
unwanted.decompose()
|
||||||
|
return str(main_content)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def crawl(self):
|
||||||
|
"""Kuyruktaki URL'leri sırayla gezip indirir."""
|
||||||
|
logging.info(f"Tarama başlatılıyor: {self.start_url}")
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=self.headless)
|
||||||
|
context = browser.new_context(
|
||||||
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
while self.queue and count < self.max_pages:
|
||||||
|
current_url = self.queue.pop(0)
|
||||||
|
|
||||||
|
# Sayfadaki # (anchor) linklerini temizleyerek ziyaret edildi say
|
||||||
|
clean_url = current_url.split("#")[0]
|
||||||
|
|
||||||
|
if clean_url in self.visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.visited.add(clean_url)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
logging.info(f"[{count}/{self.max_pages}] Çekiliyor: {clean_url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# networkidle JS ile yüklenen siteler için önemlidir
|
||||||
|
page.goto(clean_url, wait_until="networkidle", timeout=30000)
|
||||||
|
html_content = page.content()
|
||||||
|
|
||||||
|
# 1. Main içeriği bul ve ayıkla
|
||||||
|
content_html = self.extract_main_content(html_content)
|
||||||
|
if not content_html:
|
||||||
|
logging.warning(f"İçerik bulunamadı: {clean_url}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. HTML'i Markdown'a dönüştür
|
||||||
|
# heading_style="ATX" -> ## Başlık (daha iyi markdown formatı)
|
||||||
|
md_text = md(content_html, heading_style="ATX", default_title=True)
|
||||||
|
|
||||||
|
# URL'i başlığa ekleyebiliriz referans için
|
||||||
|
md_text = f"# Source: {clean_url}\n\n" + md_text
|
||||||
|
|
||||||
|
# 3. Kaydet
|
||||||
|
self.save_markdown(clean_url, md_text)
|
||||||
|
|
||||||
|
# 4. Sayfadaki yeni linkleri bul ve sıraya ekle
|
||||||
|
soup = BeautifulSoup(html_content, "lxml")
|
||||||
|
for a_tag in soup.find_all("a", href=True):
|
||||||
|
href = a_tag["href"]
|
||||||
|
# Tam URL'yi oluştur
|
||||||
|
full_url = urljoin(clean_url, href).split("#")[0]
|
||||||
|
if self.is_valid_url(full_url) and full_url not in self.queue:
|
||||||
|
self.queue.append(full_url)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Hata oluştu ({clean_url}): {e}")
|
||||||
|
|
||||||
|
time.sleep(1) # Aşırı yüklenmemek için ufak bekleme
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
logging.info("Tarama tamamlandı.")
|
||||||
49
scraper/dynamic_scraper.py
Normal file
49
scraper/dynamic_scraper.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import logging
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
class DynamicScraper:
|
||||||
|
def __init__(self, headless: bool = True):
|
||||||
|
self.headless = headless
|
||||||
|
|
||||||
|
def run_scraper(self, url: str) -> str | None:
|
||||||
|
"""Belirtilen URL'yi Playwright ile açar ve tam yüklenmiş HTML'i döndürür."""
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
# Chromium tarayıcısını başlat
|
||||||
|
browser = p.chromium.launch(headless=self.headless)
|
||||||
|
context = browser.new_context(
|
||||||
|
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
logging.info(f"Playwright ile sayfa açılıyor: {url}")
|
||||||
|
# networkidle: Ağ trafiği durana kadar bekle (JS ile yüklenen veriler için iyi bir seçenektir)
|
||||||
|
page.goto(url, wait_until="networkidle", timeout=30000)
|
||||||
|
|
||||||
|
# İsterseniz burada belirli bir elementin yüklenmesini bekleyebilirsiniz:
|
||||||
|
# page.wait_for_selector("h1", timeout=5000)
|
||||||
|
|
||||||
|
html_content = page.content()
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
logging.info(f"Sayfa başarıyla yüklendi: {url}")
|
||||||
|
return html_content
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Playwright ile sayfa çekilirken hata oluştu ({url}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_title_with_playwright(self, url: str) -> str | None:
|
||||||
|
"""Sadece title'ı çekmek için kısa bir örnek."""
|
||||||
|
try:
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = p.chromium.launch(headless=self.headless)
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(url)
|
||||||
|
title = page.title()
|
||||||
|
browser.close()
|
||||||
|
return title
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Title alınamadı: {e}")
|
||||||
|
return None
|
||||||
43
scraper/static_scraper.py
Normal file
43
scraper/static_scraper.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
|
|
||||||
|
class StaticScraper:
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self.base_url = base_url
|
||||||
|
# Temel header'lar ekleyerek bot korumalarını geçmeye çalışıyoruz
|
||||||
|
self.headers = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch_page(self, url_path: str = "") -> str | None:
|
||||||
|
"""Belirtilen URL'ye HTTP GET isteği atar ve HTML içeriğini döndürür."""
|
||||||
|
url = f"{self.base_url}{url_path}"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=self.headers, timeout=10)
|
||||||
|
response.raise_for_status() # Hata kodlarında exception fırlatır (örn: 404, 500)
|
||||||
|
logging.info(f"Başarılı bir şekilde çekildi: {url}")
|
||||||
|
return response.text
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Sayfa çekilirken hata oluştu ({url}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_title(self, html_content: str) -> str | None:
|
||||||
|
"""HTML içeriğinden sayfa başlığını (title) ayrıştırır."""
|
||||||
|
if not html_content:
|
||||||
|
return None
|
||||||
|
soup = BeautifulSoup(html_content, "lxml")
|
||||||
|
title_tag = soup.find("title")
|
||||||
|
return title_tag.text.strip() if title_tag else None
|
||||||
|
|
||||||
|
def extract_links(self, html_content: str) -> list[str]:
|
||||||
|
"""Sayfadaki tüm bağlantıları (a etiketlerini) çıkartır."""
|
||||||
|
if not html_content:
|
||||||
|
return []
|
||||||
|
soup = BeautifulSoup(html_content, "lxml")
|
||||||
|
links = []
|
||||||
|
for a_tag in soup.find_all("a", href=True):
|
||||||
|
links.append(a_tag["href"])
|
||||||
|
return links
|
||||||
40
sss
Normal file
40
sss
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
bash
|
||||||
|
python rag_local/ingest.py
|
||||||
|
İşlem bitip chroma_db/ klasörü oluştuğunda ise MCP Server'ı şu şekilde ayaklandırabilirsin:
|
||||||
|
|
||||||
|
bash
|
||||||
|
python rag_local/mcp_server.py
|
||||||
|
Kurulumun bitmesini bekleyip benim test etmemi ister misin yoksa sen terminalden takip edip test etmek ister misin?
|
||||||
|
|
||||||
|
|
||||||
|
Ran command: `python rag_local/ingest.py ✔ 142 02:59:09 `
|
||||||
|
Viewed README.md:1-15
|
||||||
|
|
||||||
|
Bu komutun şu an "hiçbir şey yapmıyormuş" gibi ekranda öylece asılı kalması tamamen normal ve olması gereken şeydir! Nedenini hemen açıklayayım:
|
||||||
|
|
||||||
|
Bu dosya standart bir web sunucusu (örneğin arka planda `localhost:3000`'de çalışan bir site) **değildir.** Bu bir **MCP (Model Context Protocol)** sunucusudur.
|
||||||
|
|
||||||
|
MCP sunucuları, yapay zekalarla (örneğin bilgisayarındaki Claude Desktop uygulamasıyla) standart girdi/çıktı (stdio) üzerinden "JSON" formatında gizlice konuşmak üzere tasarlanmıştır. Ekrana "Sunucu başladı" gibi bir yazı yazdırırsa bu iletişim protokolü bozulacağı için **tamamen sessiz** çalışır.
|
||||||
|
|
||||||
|
**Peki bu şu an ne işe yarıyor?**
|
||||||
|
Şu an o komut çalışırken aslında bir kulaklıkla bekliyor. Claude uygulamasına gidip *"Bana Next.js Routing ile ilgili dokümanları getir"* dediğinde, Claude arka planda bu çalışan komuta sessiz bir mesaj gönderecek. Bu komut da az önce vektörleştirdiğimiz 8664 parça belge (`chroma_db`) içinden en alakalı olanları bulup Claude'a geri verecek.
|
||||||
|
|
||||||
|
**Nasıl Kullanılır?**
|
||||||
|
Normalde bu komutu senin elinle terminale yazmana gerek yoktur. Claude Desktop uygulamasının ayarlarındaki `claude_desktop_config.json` (veya `mcp.json`) dosyasını açıp şuna benzer bir ayar eklersin:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"dokumanlarim": {
|
||||||
|
"command": "/home/beyhan/Masaüstü/mdscrap/.venv/bin/python",
|
||||||
|
"args": ["/home/beyhan/Masaüstü/mdscrap/rag_local/mcp_server.py"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Bunu yaptığında Claude'u her açtığında bu sunucuyu **arka planda kendisi sessizce başlatır**. Claude'un ekranında sağ altta bir "Priz" ikonu çıkar ve tıkladığında `search_documentation` aracını görebilirsin. Artık Claude'a kod yazdırırken indirdiğimiz Next.js, NextAuth ve Nuxt dokümanlarını kullanarak sana en güncel cevapları verecektir!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNfYWRtaW4iOnRydWUsInN1YiI6ImFkbWluIiwiZXhwIjoxODA4NDQyMDcwLCJpYXQiOjE3NzY5MDYwNzAsImp0aSI6IjFjMTRkMzFlOGE1MTQxMGRjMjQ3ZWQ5MTA5Mjg0MTAxIn0.FB8WdNCM7Hugwe2wrXxQ66KTNBWXXeweRAzzweRNMoM
|
||||||
Reference in New Issue
Block a user