Feature Extraction
sentence-transformers
Safetensors
German
French
Italian
qwen3
sentence-similarity
swiss-law
legal-retrieval
dense-retrieval
text-embeddings-inference
Instructions to use ArneH/harrier-semantic-v1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use ArneH/harrier-semantic-v1 with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("ArneH/harrier-semantic-v1") sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Notebooks
- Google Colab
- Kaggle
File size: 11,300 Bytes
bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed 67cff6a bf552ed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 | """
Swiss Legal β MCP Server Patch
================================
Ersetzt Hertner's stock search_fts5 mit einem hybriden System:
1. Harness v21 (FTS5) β MRR ~0.87 auf Benchmark, sehr schnell
2. harrier-semantic-v1 β Semantisches Fallback fΓΌr konzeptuelle &
cross-linguale Queries (DE/FR/IT)
3. RRF-Kombination β Beide Signale verschmelzen zu einem Score
Resultat: Kein Overfitting-Risiko, volle Corpus-Abdeckung, cross-lingual.
Verwendung:
# Statt mcp_server.py direkt:
python3 patch_mcp_server.py
# Claude Desktop config:
{
"mcpServers": {
"swiss-caselaw": {
"command": "/path/to/.venv/bin/python3",
"args": ["/path/to/patch_mcp_server.py"]
}
}
}
"""
from __future__ import annotations
import sys, os, logging, math
from pathlib import Path
log = logging.getLogger("harness-patch")
logging.basicConfig(level=logging.INFO, stream=sys.stderr,
format="%(asctime)s %(levelname)s %(message)s")
# ββ Locate caselaw-repo-1 ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_REPO_CANDIDATES = [
Path.home() / "caselaw-repo-1",
Path.home() / "swiss-legal" / "caselaw-repo-1",
Path(__file__).parent,
Path(__file__).parent.parent,
Path("/root/caselaw-repo-1"),
]
REPO_DIR = next((p for p in _REPO_CANDIDATES if (p / "mcp_server.py").exists()), None)
if REPO_DIR is None:
sys.exit("ERROR: mcp_server.py nicht gefunden. git clone https://github.com/jonashertner/caselaw-repo-1")
sys.path.insert(0, str(REPO_DIR))
log.info(f"Repo: {REPO_DIR}")
# ββ Locate harness_v21.py ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_HARNESS_CANDIDATES = [
Path(__file__).parent / "harness_v21.py",
Path.home() / "swiss-legal" / "patch" / "harness_v21.py",
REPO_DIR / "harness_v21.py",
REPO_DIR / "harnesses" / "harness_no_inject_021.py",
]
HARNESS_PATH = next((p for p in _HARNESS_CANDIDATES if p.exists()), None)
if HARNESS_PATH is None:
sys.exit("ERROR: harness_v21.py nicht gefunden. Download von ArneH/harrier-semantic-v1 (HuggingFace).")
import importlib.util
spec = importlib.util.spec_from_file_location("harness_v21", str(HARNESS_PATH))
harness = importlib.util.module_from_spec(spec)
spec.loader.exec_module(harness)
log.info(f"Harness v21: {HARNESS_PATH}")
# ββ Semantic engine (harrier-semantic-v1) ββββββββββββββββββββββββββββββββββββββ
DATA_DIR = Path(os.environ.get("SWISS_CASELAW_DIR", Path.home() / ".swiss-caselaw"))
SEMANTIC_DIR = DATA_DIR / "semantic"
MODEL_DIR = SEMANTIC_DIR / "harrier-semantic-v1"
EMB_DIR = SEMANTIC_DIR / "embeddings"
HF_MODEL_REPO = "ArneH/harrier-semantic-v1"
HF_EMBS_REPO = "ArneH/swiss-caselaw-embeddings"
import numpy as np
_sem_model = None
_corpus_ids = []
_corpus_mat = None
_sem_ready = False
def _get_device():
try:
import torch
if torch.cuda.is_available(): return "cuda"
if torch.backends.mps.is_available(): return "mps"
except Exception: pass
return "cpu"
def _load_semantic_engine():
global _sem_model, _corpus_ids, _corpus_mat, _sem_ready
if _sem_ready:
return True
# Model
try:
from sentence_transformers import SentenceTransformer
device = _get_device()
if MODEL_DIR.exists() and (MODEL_DIR / "model.safetensors").exists():
_sem_model = SentenceTransformer(str(MODEL_DIR), device=device)
else:
log.info(f"Downloading harrier-semantic-v1 from HuggingFace...")
MODEL_DIR.mkdir(parents=True, exist_ok=True)
_sem_model = SentenceTransformer(HF_MODEL_REPO, device=device,
cache_folder=str(SEMANTIC_DIR))
log.info(f"Semantic model loaded on {device}")
except Exception as e:
log.warning(f"Semantic model unavailable: {e}")
return False
# Embeddings
npz_files = sorted(EMB_DIR.glob("*.npz"))
if not npz_files:
log.info(f"Downloading corpus embeddings from HuggingFace (~740MB)...")
try:
from huggingface_hub import snapshot_download
EMB_DIR.mkdir(parents=True, exist_ok=True)
snapshot_download(repo_id=HF_EMBS_REPO, repo_type="dataset",
local_dir=str(EMB_DIR),
allow_patterns=["harrier_semantic_v1_*.npz"])
npz_files = sorted(EMB_DIR.glob("*.npz"))
except Exception as e:
log.warning(f"Embeddings download failed: {e}")
return False
log.info(f"Loading {len(npz_files)} embedding shards...")
ids, mats = [], []
for f in npz_files:
d = np.load(f)
ids.extend(d["ids"].tolist())
mats.append(d["embeddings"].astype(np.float32))
_corpus_ids = ids
mat = np.concatenate(mats, axis=0)
norms = np.linalg.norm(mat, axis=1, keepdims=True)
_corpus_mat = mat / np.where(norms < 1e-8, 1e-8, norms)
_sem_ready = True
log.info(f"Semantic engine ready: {len(_corpus_ids):,} vectors")
return True
def _semantic_search(query: str, top_k: int = 50) -> list[tuple[str, float]]:
"""Returns list of (decision_id, cosine_score)."""
if not _load_semantic_engine():
return []
try:
q_emb = _sem_model.encode([query], normalize_embeddings=True, show_progress_bar=False)
scores = (q_emb @ _corpus_mat.T)[0]
top_idx = np.argsort(-scores)[:top_k]
return [(_corpus_ids[i], float(scores[i])) for i in top_idx]
except Exception as e:
log.warning(f"Semantic search error: {e}")
return []
# ββ Import and patch mcp_server ββββββββββββββββββββββββββββββββββββββββββββββββ
import mcp_server
_original_search_fts5 = mcp_server.search_fts5
def _hybrid_search(query: str, limit: int, filters: dict) -> tuple[list[dict], int]:
"""
Hybrid search: harness_v21 FTS5 + harrier-semantic-v1, combined via RRF.
Falls back to original search_fts5 for pure filter queries.
"""
RRF_K = 60
# 1. Harness v21 (FTS5)
try:
fts_raw = harness.search(query, k=limit * 4)
except Exception as e:
log.warning(f"Harness search failed: {e}")
fts_raw = []
# 2. Semantic search (harrier-semantic-v1)
sem_raw = _semantic_search(query, top_k=limit * 3)
# 3. RRF fusion
rrf: dict[str, float] = {}
for rank, r in enumerate(fts_raw, 1):
did = r["decision_id"]
rrf[did] = rrf.get(did, 0.0) + 0.7 / (RRF_K + rank) # FTS5 weight: 0.7
for rank, (did, _) in enumerate(sem_raw, 1):
rrf[did] = rrf.get(did, 0.0) + 0.3 / (RRF_K + rank) # Semantic weight: 0.3
# If semantic has results but FTS5 doesn't β boost semantic weight
if not fts_raw and sem_raw:
rrf = {}
for rank, (did, score) in enumerate(sem_raw, 1):
rrf[did] = 1.0 / (RRF_K + rank)
sorted_ids = sorted(rrf, key=lambda x: -rrf[x])
# 4. Apply filters post-hoc
if any(filters.values()):
try:
db = mcp_server.get_db()
id_list = ",".join(f"'{i.replace(chr(39), '')}'" for i in sorted_ids[:500])
clauses, params = [], []
for col in ("court", "canton", "language"):
if filters.get(col):
clauses.append(f"{col} = ?"); params.append(filters[col])
if filters.get("date_from"):
clauses.append("decision_date >= ?"); params.append(filters["date_from"])
if filters.get("date_to"):
clauses.append("decision_date <= ?"); params.append(filters["date_to"])
where = " AND ".join(clauses)
allowed = {
row[0] for row in db.execute(
f"SELECT decision_id FROM decisions WHERE decision_id IN ({id_list}) AND {where}",
params
).fetchall()
}
sorted_ids = [d for d in sorted_ids if d in allowed]
except Exception as e:
log.warning(f"Filter error: {e}")
total = len(sorted_ids)
page_ids = sorted_ids[:limit]
if not page_ids:
return [], 0
# 5. Fetch full rows from DB
try:
db = mcp_server.get_db()
id_list = ",".join(f"'{i.replace(chr(39), '')}'" for i in page_ids)
rows = {
r["decision_id"]: dict(r)
for r in db.execute(
f"SELECT * FROM decisions WHERE decision_id IN ({id_list})"
).fetchall()
}
except Exception as e:
log.warning(f"DB fetch error: {e}")
rows = {}
results = []
for did in page_ids:
row = rows.get(did)
if not row:
continue
row["relevance_score"] = rrf.get(did, 0.0)
row["snippet"] = (row.get("regeste") or row.get("title") or "")[:400]
row["citation_count"] = row.get("citation_count", 0) or 0
results.append(row)
return results, total
def _patched_search_fts5(query: str = "", limit: int = 50,
court=None, canton=None, language=None,
date_from=None, date_to=None,
chamber=None, decision_type=None,
legal_area=None, offset: int = 0, sort=None,
**kwargs):
q = (query or "").strip()
filters = dict(court=court, canton=canton, language=language,
date_from=date_from, date_to=date_to)
# Pure filter query (no text) β use original
if not q:
return _original_search_fts5(
query=query, limit=limit, court=court, canton=canton,
language=language, date_from=date_from, date_to=date_to,
chamber=chamber, decision_type=decision_type,
legal_area=legal_area, offset=offset, sort=sort, **kwargs
)
results, total = _hybrid_search(q, limit=limit + offset, filters=filters)
return results[offset:offset + limit], total
mcp_server.search_fts5 = _patched_search_fts5
log.info("β search_fts5 β Hybrid (Harness v21 FTS5 + harrier-semantic-v1)")
# Pre-load semantic engine in background
import threading
threading.Thread(target=_load_semantic_engine, daemon=True).start()
# ββ Run MCP server βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
import asyncio
if "--remote" in sys.argv:
mcp_server.REMOTE_MODE = True
host, port = "0.0.0.0", 8000
for i, arg in enumerate(sys.argv):
if arg == "--host" and i + 1 < len(sys.argv): host = sys.argv[i + 1]
if arg == "--port" and i + 1 < len(sys.argv): port = int(sys.argv[i + 1])
mcp_server.main_remote(host, port)
else:
asyncio.run(mcp_server.main_stdio())
|