6 Commits
Author SHA1 Message Date
mstfyldz 47ca4cc962 fix: replace exact-phrase wrap with AND-required-terms in /search
Wrapping every multi-word phrase in exact-phrase quotes (added for the
"uyuşturucu madde ticareti" false-positive problem) was too strict for
independent keyword queries like "bıçak yaralaması beraat" or the AI
query-optimizer's extracted keywords — those words rarely appear
verbatim adjacent to each other, so exact-phrase returned 0 results.

Now each word is prefixed with + instead (AND semantics: all words
must appear somewhere in the decision, not necessarily adjacent).
This still fixes the original "madde" noise problem while no longer
breaking loose multi-keyword searches.
2026-08-09 10:55:34 +03:00
mstfyldz 2131e3c71d feat: setup rest_api and prepare coolify deployment 2026-08-08 17:31:25 +03:00
Said Sürücü 2ead0b455c Merge pull request #42 from Stauding/feat/orcarouter-embedder
feat: add OrcaRouter as a hosted embedding provider
2026-08-06 15:33:18 +03:00
jinhao.songandClaude 5a5c21e01b feat: add OrcaRouter embedder for hosted semantic search
Add a named OrcaRouterEmbedder mirroring the existing OpenRouterEmbedder:
a production AI gateway that proxies 200+ models on one OpenAI-compatible
endpoint (https://api.orcarouter.ai/v1). Selecting it is a one-line switch:
set ORCAROUTER_API_KEY instead of OPENROUTER_API_KEY.

- get_embedder() prefers OrcaRouter when ORCAROUTER_API_KEY is present
- is_semantic_search_available() now also enables on OrcaRouter keys
- document the new option in README (Alternatif 3) and .env.example

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: jinhao.song <jinhao.song@myflashcloud.com>
2026-08-06 19:28:02 +08:00
Said Sürücü e50f109021 Merge pull request #39 from chrstphe/mcp-toplist-badge
Add MCP Toplist rank badge
2026-07-27 16:05:24 +03:00
Christophe 8b32f9a4e0 Add MCP Toplist rank badge 2026-07-27 12:42:53 +02:00
7 changed files with 368 additions and 12 deletions
+14 -2
View File
@@ -75,11 +75,12 @@ JWT_SECRET_KEY=your_jwt_secret_key_here
# ============================================================================= # =============================================================================
# Embedding provider for the semantic_search tool. # Embedding provider for the semantic_search tool.
# Pick exactly one of: OpenRouter (hosted) or Local (your own server). # Pick exactly one of: OpenRouter (hosted), OrcaRouter (hosted), or Local.
# --- Option A: OpenRouter (hosted, default) ----------------------------------- # --- Option A: OpenRouter (hosted, default) -----------------------------------
# Get your API key from: https://openrouter.ai/keys # Get your API key from: https://openrouter.ai/keys
# If neither this nor EMBEDDING_PROVIDER=local is set, semantic search is off. # If neither this, nor ORCAROUTER_API_KEY, nor EMBEDDING_PROVIDER=local is set,
# semantic search is off.
OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here
# Optional: override the OpenRouter embedding model and dimension. # Optional: override the OpenRouter embedding model and dimension.
@@ -89,6 +90,17 @@ OPENROUTER_API_KEY=sk-or-v1-your_openrouter_api_key_here
# OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001 # OPENROUTER_EMBEDDING_MODEL=google/gemini-embedding-001
# OPENROUTER_EMBEDDING_DIMENSION=3072 # OPENROUTER_EMBEDDING_DIMENSION=3072
# --- Option A2: OrcaRouter (hosted) ------------------------------------------
# OrcaRouter is a production AI gateway with one OpenAI-compatible endpoint
# (https://api.orcarouter.ai/v1). Get your API key from: https://www.orcarouter.ai
# Set ORCAROUTER_API_KEY instead of OPENROUTER_API_KEY to use it.
# ORCAROUTER_API_KEY=sk-orca-your_orcarouter_api_key_here
# Optional: override the OrcaRouter embedding model and dimension.
# Defaults: google/gemini-embedding-001 at 3072 dims (multilingual).
# ORCAROUTER_EMBEDDING_MODEL=google/gemini-embedding-001
# ORCAROUTER_EMBEDDING_DIMENSION=3072
# --- Option B: Local OpenAI-compatible server (no API key required) ---------- # --- Option B: Local OpenAI-compatible server (no API key required) ----------
# Recommended for Turkish: intfloat/multilingual-e5-large served by HuggingFace # Recommended for Turkish: intfloat/multilingual-e5-large served by HuggingFace
# Text Embeddings Inference (TEI). One-line setup: # Text Embeddings Inference (TEI). One-line setup:
+2 -1
View File
@@ -18,6 +18,7 @@ COPY README.md ./
COPY app.py ./ COPY app.py ./
COPY asgi_app.py ./ COPY asgi_app.py ./
COPY mcp_server_main.py ./ COPY mcp_server_main.py ./
COPY rest_api.py ./
# Copy MCP modules and shared packages # Copy MCP modules and shared packages
COPY anayasa_mcp_module ./anayasa_mcp_module COPY anayasa_mcp_module ./anayasa_mcp_module
@@ -51,4 +52,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1 CMD python -c "import httpx; httpx.get('http://localhost:8000/health', timeout=5)" || exit 1
# Run the ASGI application # Run the ASGI application
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "rest_api:app", "--host", "0.0.0.0", "--port", "8000"]
+21 -2
View File
@@ -1,5 +1,7 @@
# Yargı MCP: Türk Hukuk Kaynakları için MCP Sunucusu # Yargı MCP: Türk Hukuk Kaynakları için MCP Sunucusu
[![MCP Toplist](https://mcptoplist.com/badge/glama%2Fsaidsurucu%2Fyargi-mcp.svg)](https://mcptoplist.com/server/glama%2Fsaidsurucu%2Fyargi-mcp)
> ## ✨ Profesyonel Sürüm Hazır: Yargı MCP Pro > ## ✨ Profesyonel Sürüm Hazır: Yargı MCP Pro
> >
> **Mevzuat ve içtihatı tek bir MCP sunucusunda birleştiren** profesyonel sürüm yayında: > **Mevzuat ve içtihatı tek bir MCP sunucusunda birleştiren** profesyonel sürüm yayında:
@@ -275,7 +277,7 @@ Yargı MCP'yi Gemini CLI ile kullanmak için:
Yargı MCP, **semantik arama** özelliği ile kararları anlamsal olarak sıralayabilir. Opsiyoneldir; iki yoldan biri yapılandırıldığında otomatik etkinleşir: Yargı MCP, **semantik arama** özelliği ile kararları anlamsal olarak sıralayabilir. Opsiyoneldir; iki yoldan biri yapılandırıldığında otomatik etkinleşir:
- **Yerel** (önerilen, ücretsiz): kendi makinenizdeki OpenAI-uyumlu embedding sunucusu (HuggingFace TEI, llama.cpp, Ollama, vLLM, LM Studio…) - **Yerel** (önerilen, ücretsiz): kendi makinenizdeki OpenAI-uyumlu embedding sunucusu (HuggingFace TEI, llama.cpp, Ollama, vLLM, LM Studio…)
- **Hosted**: OpenRouter API anahtarı - **Hosted**: OpenRouter ya da [OrcaRouter](https://www.orcarouter.ai) API anahtarı
### Semantik Arama Nasıl Çalışır? ### Semantik Arama Nasıl Çalışır?
1. `initial_keyword` ile Bedesten API'den 100 karar çekilir 1. `initial_keyword` ile Bedesten API'den 100 karar çekilir
@@ -351,11 +353,25 @@ OPENROUTER_API_KEY=sk-or-v1-xxx...
API anahtarınızı [openrouter.ai/keys](https://openrouter.ai/keys) adresinden alın. Varsayılan model `google/gemini-embedding-001` artık ücretli — ücretsiz bir model seçerseniz `OPENROUTER_EMBEDDING_MODEL`, `OPENROUTER_EMBEDDING_DIMENSION` ve uygun `EMBEDDING_PROMPT_STYLE` değerlerini birlikte ayarlayın. API anahtarınızı [openrouter.ai/keys](https://openrouter.ai/keys) adresinden alın. Varsayılan model `google/gemini-embedding-001` artık ücretli — ücretsiz bir model seçerseniz `OPENROUTER_EMBEDDING_MODEL`, `OPENROUTER_EMBEDDING_DIMENSION` ve uygun `EMBEDDING_PROMPT_STYLE` değerlerini birlikte ayarlayın.
### Alternatif 3: OrcaRouter (hosted)
[OrcaRouter](https://www.orcarouter.ai), 200+ modeli tek OpenAI-uyumlu uçta toplayan bir üretim AI ağ geçididir (ağ geçidi seviyesinde, sıfır-güven AI ajan güvenliği de içerir). Mevcut SDK kodu `base_url` değiştirilerek aynen çalışır.
```bash
ORCAROUTER_API_KEY=sk-orca-xxx...
# İsteğe bağlı — varsayılan google/gemini-embedding-001 (3072 dim, çok dilli)
# ORCAROUTER_EMBEDDING_MODEL=...
# ORCAROUTER_EMBEDDING_DIMENSION=...
# EMBEDDING_PROMPT_STYLE=gemini # varsayılan
```
API anahtarınızı [www.orcarouter.ai](https://www.orcarouter.ai) adresinden alın. `OPENROUTER_API_KEY` yerine `ORCAROUTER_API_KEY` ayarlamanız yeterli — semantik arama aynı OpenAI-uyumlu akışı OrcaRouter ucu üzerinden kullanır.
### Yapılandırma Referansı ### Yapılandırma Referansı
| Env Var | Açıklama | Örnek | | Env Var | Açıklama | Örnek |
|---|---|---| |---|---|---|
| `EMBEDDING_PROVIDER` | `local` ise yerel sunucu, boş ise OpenRouter | `local` | | `EMBEDDING_PROVIDER` | `local` ise yerel sunucu, boş ise hosted (OpenRouter/OrcaRouter) | `local` |
| `EMBEDDING_PROMPT_STYLE` | `gemini` / `e5` / `raw` — modelin beklediği önek | `e5` | | `EMBEDDING_PROMPT_STYLE` | `gemini` / `e5` / `raw` — modelin beklediği önek | `e5` |
| `LOCAL_EMBEDDING_BASE_URL` | Yerel sunucunun OpenAI-uyumlu URL'i | `http://localhost:8080/v1` | | `LOCAL_EMBEDDING_BASE_URL` | Yerel sunucunun OpenAI-uyumlu URL'i | `http://localhost:8080/v1` |
| `LOCAL_EMBEDDING_MODEL` | Model adı | `intfloat/multilingual-e5-large` | | `LOCAL_EMBEDDING_MODEL` | Model adı | `intfloat/multilingual-e5-large` |
@@ -363,6 +379,9 @@ API anahtarınızı [openrouter.ai/keys](https://openrouter.ai/keys) adresinden
| `OPENROUTER_API_KEY` | OpenRouter anahtarı (sadece hosted için) | `sk-or-v1-…` | | `OPENROUTER_API_KEY` | OpenRouter anahtarı (sadece hosted için) | `sk-or-v1-…` |
| `OPENROUTER_EMBEDDING_MODEL` | OpenRouter model id'si | `google/gemini-embedding-001` | | `OPENROUTER_EMBEDDING_MODEL` | OpenRouter model id'si | `google/gemini-embedding-001` |
| `OPENROUTER_EMBEDDING_DIMENSION` | OpenRouter modelinin çıktı boyutu | `3072` | | `OPENROUTER_EMBEDDING_DIMENSION` | OpenRouter modelinin çıktı boyutu | `3072` |
| `ORCAROUTER_API_KEY` | OrcaRouter anahtarı (sadece hosted için) | `sk-orca-…` |
| `ORCAROUTER_EMBEDDING_MODEL` | OrcaRouter model id'si | `google/gemini-embedding-001` |
| `ORCAROUTER_EMBEDDING_DIMENSION` | OrcaRouter modelinin çıktı boyutu | `3072` |
> 💡 **Not:** Hiçbir embedding sağlayıcı yapılandırılmazsa semantik arama aracı görünmez, diğer 28 araç normal şekilde çalışır. > 💡 **Not:** Hiçbir embedding sağlayıcı yapılandırılmazsa semantik arama aracı görünmez, diğer 28 araç normal şekilde çalışır.
+12 -2
View File
@@ -266,14 +266,24 @@ from bedesten_mcp_module.models import (
from bedesten_mcp_module.enums import BirimAdiEnum from bedesten_mcp_module.enums import BirimAdiEnum
# Semantic Search Module Imports (enabled if any embedding provider is configured) # Semantic Search Module Imports (enabled if any embedding provider is configured)
from semantic_search.embedder import is_semantic_search_available, is_local_embedding_configured from semantic_search.embedder import (
is_semantic_search_available,
is_local_embedding_configured,
is_openrouter_available,
is_orcarouter_available,
)
SEMANTIC_SEARCH_AVAILABLE = is_semantic_search_available() SEMANTIC_SEARCH_AVAILABLE = is_semantic_search_available()
if SEMANTIC_SEARCH_AVAILABLE: if SEMANTIC_SEARCH_AVAILABLE:
from semantic_search.embedder import get_embedder from semantic_search.embedder import get_embedder
from semantic_search.vector_store import VectorStore from semantic_search.vector_store import VectorStore
from semantic_search.processor import DocumentProcessor from semantic_search.processor import DocumentProcessor
provider = "local" if is_local_embedding_configured() else "openrouter" if is_local_embedding_configured():
provider = "local"
elif is_orcarouter_available():
provider = "orcarouter"
elif is_openrouter_available():
provider = "openrouter"
logger.info(f"Semantic search enabled (provider={provider})") logger.info(f"Semantic search enabled (provider={provider})")
else: else:
logger.info("Semantic search disabled (no embedding provider configured)") logger.info("Semantic search disabled (no embedding provider configured)")
+243
View File
@@ -0,0 +1,243 @@
# rest_api.py
#
# Bu dosya, yargi-mcp fork'unun kok dizinine eklenir (mcp_server_main.py ile ayni seviyede).
# Amac: FastMCP/MCP protokolunu (JSON-RPC) tamamen atlayip, mcp_server_main.py'deki
# gercek client nesnelerini (bedesten_client_instance vb.) dogrudan cagiran duz bir
# REST API sunmak. Boylece Node backend, MCP istemcisi konusmadan basit HTTP ile
# bu servise istek atabilir.
#
# Calistirma:
# uvicorn rest_api:app --host 0.0.0.0 --port 8001
#
# NOT: mcp_server_main.py'deki bedesten_client_instance nesnesini yeniden olusturmak
# yerine dogrudan ayni siniflari import edip burada kendi instance'imizi kuruyoruz.
# Boylece mcp_server_main.py'yi (FastMCP/app.tool decorator'lariyla) hic calistirmaya
# gerek kalmiyor, sadece alttaki client katmanini kullaniyoruz.
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field
from typing import List, Optional
import logging
import re
from bedesten_mcp_module.client import BedestenApiClient, BedestenRateLimited
from bedesten_mcp_module.models import (
BedestenSearchRequest,
BedestenSearchData,
BedestenCourtTypeEnum,
)
from bedesten_mcp_module.enums import BirimAdiEnum
import httpx
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("rest_api")
app = FastAPI(title="LegalOS - Yargi Karar Arama REST API")
bedesten_client = BedestenApiClient()
# --- Request/Response semalari ---
class SearchRequest(BaseModel):
phrase: str = Field(..., description="Arama ifadesi (Turkce). Ornek: 'mulkiyet hakki'")
court_types: List[str] = Field(
default=["YARGITAYKARARI", "DANISTAYKARAR"],
description="YARGITAYKARARI, DANISTAYKARAR, YERELHUKUK, ISTINAFHUKUK, KYB",
)
page_number: int = Field(default=1, ge=1)
birim_adi: str = Field(default="ALL", description="Daire filtresi, orn. H1, C3, HGK")
karar_tarihi_start: Optional[str] = Field(default=None, description="YYYY-MM-DD")
karar_tarihi_end: Optional[str] = Field(default=None, description="YYYY-MM-DD")
exact_phrase: bool = Field(
default=True,
description=(
"True ise coklu kelimeli phrase'deki her kelime otomatik olarak '+' ile zorunlu "
"kilinir (orn. 'a b' -> '+a +b') — tum kelimeler karar icinde gecmeli ama yan yana "
"olmalari gerekmez. Bedesten API'de tirnaksiz coklu kelime aramasi kelimeleri gevsek "
"eslestirir (orn. 'madde' gibi her kararda gecen ortak kelimeler alakasiz sonuclari "
"one cikarir); bu ayar bu gurultuyu onler. Kullanici zaten tirnak/AND/OR/NOT/+/- "
"kullaniyorsa dokunulmaz."
),
)
class SearchResultItem(BaseModel):
document_id: str
raw: dict
class SearchResponse(BaseModel):
decisions: List[dict]
total_records: int
requested_page: int
page_size: int
searched_courts: List[str]
error: Optional[str] = None
retry_after: Optional[float] = None
class DocumentResponse(BaseModel):
document_id: str
markdown_content: Optional[str]
source_url: Optional[str]
mime_type: Optional[str]
# --- Yardimci fonksiyon: tarih formatlama (mcp_server_main.py'deki mantikla ayni) ---
def _format_date(value: Optional[str], end_of_day: bool = False) -> str:
if not value:
return ""
if value.endswith("Z"):
return value
if "T" not in value:
suffix = "T23:59:59.999Z" if end_of_day else "T00:00:00.000Z"
return f"{value}{suffix}"
return value
# --- Yardimci fonksiyon: coklu kelimeli aramalari daha isabetli hale getirme ---
#
# Bedesten API'de tirnaksiz coklu kelime aramasi kelimeleri ayri ayri (gevsek) eslestiriyor.
# Ornek: "uyusturucu madde ticareti" -> "madde" gibi her kararda gecen (kanun maddesi
# anlaminda) ortak bir kelime yuzunden alakasiz sonuclar (ic icra/iflas kararlari) one
# cikabiliyor.
#
# Once tum ifadeyi tirnaklayip "tam bitisik ifade" aramasi denendi, ama bu cok kati
# cikti: "bicak yaralamasi beraat" gibi bagimsiz anahtar kelimelerden olusan (AI'nin
# uzun sorulardan cikardigi turden) aramalar, bu 3 kelime kararlarda hic yan yana/aynen
# gecmedigi icin 0 sonuc donduruyordu. Bunun yerine her kelimeyi "+" ile ayri ayri
# zorunlu kiliyoruz (AND semantigi): tum kelimeler karar icinde herhangi bir yerde
# gecmeli ama bitisik/ayni sirada olmalari gerekmiyor. Bu hem orijinal "madde" sorununu
# cozuyor (uyusturucu VE madde VE ticaret hepsi gecmeli) hem de bagimsiz anahtar kelime
# aramalarini kirmiyor.
_OPERATOR_PATTERN = re.compile(r'"|\bAND\b|\bOR\b|\bNOT\b|(?:^|\s)[+-]\S', re.IGNORECASE)
def _apply_required_terms(phrase: str, exact_phrase: bool) -> str:
stripped = phrase.strip()
if not exact_phrase or not stripped:
return phrase
words = stripped.split()
if len(words) < 2:
return phrase
if _OPERATOR_PATTERN.search(stripped):
return phrase
return " ".join(f"+{w}" for w in words)
# --- Endpoint 1: Arama ---
@app.post("/search", response_model=SearchResponse)
async def search(req: SearchRequest):
"""
Yargitay / Danistay / yerel mahkeme / istinaf / KYB kararlarinda arama yapar.
mcp_server_main.py'deki search_bedesten_unified aracinin dogrudan REST karsiligi.
"""
karar_tarihi_start = _format_date(req.karar_tarihi_start)
karar_tarihi_end = _format_date(req.karar_tarihi_end, end_of_day=True)
phrase = _apply_required_terms(req.phrase, req.exact_phrase)
search_data = BedestenSearchData(
pageSize=10,
pageNumber=req.page_number,
itemTypeList=req.court_types,
phrase=phrase,
birimAdi=req.birim_adi,
kararTarihiStart=karar_tarihi_start,
kararTarihiEnd=karar_tarihi_end,
)
search_request = BedestenSearchRequest(data=search_data)
logger.info(f"search: phrase={req.phrase!r} -> sent={phrase!r} courts={req.court_types} page={req.page_number}")
try:
response = await bedesten_client.search_documents(search_request)
if response.data is None:
return SearchResponse(
decisions=[],
total_records=0,
requested_page=req.page_number,
page_size=10,
searched_courts=req.court_types,
error="no_data",
)
decisions = response.data.emsalKararList or []
total = response.data.total or 0
return SearchResponse(
decisions=[d.model_dump() for d in decisions],
total_records=total,
requested_page=req.page_number,
page_size=10,
searched_courts=req.court_types,
)
except BedestenRateLimited as e:
logger.warning(f"local rate limit hit, retry_after={e.retry_after}")
raise HTTPException(
status_code=429,
detail={"error": "rate_limit_exceeded", "retry_after": e.retry_after},
)
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
retry_after = e.response.headers.get("Retry-After", "")
raise HTTPException(
status_code=429,
detail={"error": "upstream_rate_limit", "retry_after": retry_after},
)
logger.exception("Bedesten search error")
raise HTTPException(status_code=502, detail="Bedesten API hatasi")
except Exception:
logger.exception("Beklenmeyen arama hatasi")
raise HTTPException(status_code=500, detail="Sunucu hatasi")
# --- Endpoint 2: Belge getirme (tam metin, Markdown) ---
@app.get("/document/{document_id}", response_model=DocumentResponse)
async def get_document(document_id: str):
"""
Bir kararin tam metnini Markdown formatinda getirir.
mcp_server_main.py'deki get_bedesten_document_markdown aracinin REST karsiligi.
"""
if not document_id.strip():
raise HTTPException(status_code=400, detail="document_id bos olamaz")
logger.info(f"get_document: id={document_id}")
try:
doc = await bedesten_client.get_document_as_markdown(document_id)
return DocumentResponse(
document_id=document_id,
markdown_content=doc.markdown_content,
source_url=doc.source_url,
mime_type=doc.mime_type,
)
except BedestenRateLimited as e:
raise HTTPException(
status_code=429,
detail={"error": "rate_limit_exceeded", "retry_after": e.retry_after},
)
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
retry_after = e.response.headers.get("Retry-After", "")
raise HTTPException(
status_code=429,
detail={"error": "upstream_rate_limit", "retry_after": retry_after},
)
logger.exception("Bedesten document fetch error")
raise HTTPException(status_code=502, detail="Bedesten API hatasi")
except Exception:
logger.exception("Beklenmeyen belge getirme hatasi")
raise HTTPException(status_code=500, detail="Sunucu hatasi")
# --- Endpoint 3: Health check (Coolify icin) ---
@app.get("/health")
async def health():
return {"status": "ok"}
+4
View File
@@ -2,9 +2,11 @@
from .embedder import ( from .embedder import (
OpenRouterEmbedder, OpenRouterEmbedder,
OrcaRouterEmbedder,
LocalEmbedder, LocalEmbedder,
get_embedder, get_embedder,
is_openrouter_available, is_openrouter_available,
is_orcarouter_available,
is_local_embedding_configured, is_local_embedding_configured,
is_semantic_search_available, is_semantic_search_available,
) )
@@ -13,9 +15,11 @@ from .processor import DocumentProcessor
__all__ = [ __all__ = [
'OpenRouterEmbedder', 'OpenRouterEmbedder',
'OrcaRouterEmbedder',
'LocalEmbedder', 'LocalEmbedder',
'get_embedder', 'get_embedder',
'is_openrouter_available', 'is_openrouter_available',
'is_orcarouter_available',
'is_local_embedding_configured', 'is_local_embedding_configured',
'is_semantic_search_available', 'is_semantic_search_available',
'VectorStore', 'VectorStore',
+72 -5
View File
@@ -64,6 +64,11 @@ def is_openrouter_available() -> bool:
return bool(os.getenv("OPENROUTER_API_KEY")) return bool(os.getenv("OPENROUTER_API_KEY"))
def is_orcarouter_available() -> bool:
"""Check if OrcaRouter API key is available."""
return bool(os.getenv("ORCAROUTER_API_KEY"))
def is_local_embedding_configured() -> bool: def is_local_embedding_configured() -> bool:
"""Check if the user opted into a local embedding endpoint.""" """Check if the user opted into a local embedding endpoint."""
return os.getenv("EMBEDDING_PROVIDER", "").strip().lower() == "local" return os.getenv("EMBEDDING_PROVIDER", "").strip().lower() == "local"
@@ -71,7 +76,11 @@ def is_local_embedding_configured() -> bool:
def is_semantic_search_available() -> bool: def is_semantic_search_available() -> bool:
"""Returns True if any embedding provider is configured.""" """Returns True if any embedding provider is configured."""
return is_local_embedding_configured() or is_openrouter_available() return (
is_local_embedding_configured()
or is_openrouter_available()
or is_orcarouter_available()
)
def _coerce_dimension(value, env_name: str, default: int) -> int: def _coerce_dimension(value, env_name: str, default: int) -> int:
@@ -261,6 +270,59 @@ class OpenRouterEmbedder(_BaseOpenAICompatibleEmbedder):
) )
class OrcaRouterEmbedder(_BaseOpenAICompatibleEmbedder):
"""
Embedder using OrcaRouter's OpenAI-compatible embedding API.
OrcaRouter is a production AI gateway that proxies 200+ models on a single
OpenAI-compatible endpoint. The model and dimension are configurable so
users can pick any embedding model the gateway routes. Configuration
precedence: explicit constructor args > environment variables > defaults.
Environment variables:
ORCAROUTER_API_KEY (required): OrcaRouter credential (sk-orca-...)
ORCAROUTER_EMBEDDING_MODEL (optional): override the embedding model id
ORCAROUTER_EMBEDDING_DIMENSION (optional): override the vector size
Defaults: ``google/gemini-embedding-001`` at 3072 dimensions (multilingual,
matches the OpenRouter default — good for Turkish legal text).
"""
def __init__(
self,
model: Optional[str] = None,
dimension: Optional[int] = None,
prompt_style: Optional[str] = None,
):
api_key = os.getenv("ORCAROUTER_API_KEY")
if not api_key:
raise ValueError("ORCAROUTER_API_KEY environment variable is not set")
try:
from openai import OpenAI
except ImportError:
raise ImportError("openai package is required. Install with: pip install openai")
self.client = OpenAI(
base_url="https://api.orcarouter.ai/v1",
api_key=api_key,
)
self.model = model or os.getenv("ORCAROUTER_EMBEDDING_MODEL") or DEFAULT_MODEL
self.dimension = _coerce_dimension(
dimension if dimension is not None else os.getenv("ORCAROUTER_EMBEDDING_DIMENSION"),
"ORCAROUTER_EMBEDDING_DIMENSION",
DEFAULT_DIMENSION,
)
# Same gemini-style default as the OpenRouter embedder — matches the
# multilingual google/gemini-embedding-001 default model.
self.prompt_style = _resolve_prompt_style(prompt_style, "gemini")
logger.info(
f"OrcaRouter Embedder initialized with model: {self.model} "
f"(dimension={self.dimension}, prompt_style={self.prompt_style})"
)
class LocalEmbedder(_BaseOpenAICompatibleEmbedder): class LocalEmbedder(_BaseOpenAICompatibleEmbedder):
""" """
Embedder for a local OpenAI-compatible embedding server — Ollama, Embedder for a local OpenAI-compatible embedding server — Ollama,
@@ -332,17 +394,22 @@ def get_embedder():
Factory that picks the embedder based on EMBEDDING_PROVIDER. Factory that picks the embedder based on EMBEDDING_PROVIDER.
- ``EMBEDDING_PROVIDER=local`` -> ``LocalEmbedder`` - ``EMBEDDING_PROVIDER=local`` -> ``LocalEmbedder``
- ``ORCAROUTER_API_KEY`` set -> ``OrcaRouterEmbedder``
- otherwise -> ``OpenRouterEmbedder`` (requires OPENROUTER_API_KEY) - otherwise -> ``OpenRouterEmbedder`` (requires OPENROUTER_API_KEY)
Raises: Raises:
ValueError: If no provider is configured (neither local nor OpenRouter). ValueError: If no provider is configured (neither local, OpenRouter,
nor OrcaRouter).
""" """
if is_local_embedding_configured(): if is_local_embedding_configured():
return LocalEmbedder() return LocalEmbedder()
if is_orcarouter_available():
return OrcaRouterEmbedder()
if is_openrouter_available(): if is_openrouter_available():
return OpenRouterEmbedder() return OpenRouterEmbedder()
raise ValueError( raise ValueError(
"No embedding provider configured. Set OPENROUTER_API_KEY for hosted " "No embedding provider configured. Set OPENROUTER_API_KEY or "
"embeddings, or EMBEDDING_PROVIDER=local (with LOCAL_EMBEDDING_* " "ORCAROUTER_API_KEY for hosted embeddings, or EMBEDDING_PROVIDER=local "
"env vars) for a local OpenAI-compatible server like Ollama." "(with LOCAL_EMBEDDING_* env vars) for a local OpenAI-compatible "
"server like Ollama."
) )