Spaces:
Sleeping
Sleeping
File size: 6,809 Bytes
be18f27 838ef5a 71ca0c1 838ef5a 71ca0c1 2517621 71ca0c1 838ef5a 2517621 be18f27 71ca0c1 2517621 71ca0c1 838ef5a 48f43cd be18f27 838ef5a be18f27 838ef5a be18f27 838ef5a be18f27 838ef5a be18f27 838ef5a be18f27 71ca0c1 be18f27 71ca0c1 be18f27 71ca0c1 be18f27 |
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 |
import os
import json
import time
import base64
import hmac
import hashlib
from pathlib import Path
from typing import Any, Dict, Optional, List
# ------------------------------------------------------------
# 書き込みディレクトリの自動選択(/data → /cache → /tmp → 最後に ./data)
# ------------------------------------------------------------
_DATA_DIR: Optional[Path] = None
_EXPORT_DIR: Optional[Path] = None
_HF_INIT_DONE: bool = False
def _is_writable(p: Path) -> bool:
try:
p.mkdir(parents=True, exist_ok=True)
testfile = p / ".w_test"
with open(testfile, "w", encoding="utf-8") as f:
f.write("ok")
testfile.unlink(missing_ok=True)
return True
except Exception:
return False
def _pick_writable_dir(candidates: List[Path]) -> Path:
for p in candidates:
if _is_writable(p):
return p
# 全滅時は最終手段として /tmp に落とす
fallback = Path("/tmp/agent_studio")
fallback.mkdir(parents=True, exist_ok=True)
return fallback
def _init_hf_env(base: Path) -> None:
"""
Hugging Face / Transformers / Sentence-Transformers の
キャッシュ&ホームをすべて base 配下に固定して PermissionError を回避。
"""
global _HF_INIT_DONE
if _HF_INIT_DONE:
return
hf_home = base / "hf_home"
hf_cache = base / "hf_cache"
hf_home.mkdir(parents=True, exist_ok=True)
hf_cache.mkdir(parents=True, exist_ok=True)
# 主要な環境変数を強制設定(既存設定より優先)
os.environ.setdefault("HF_HOME", str(hf_home))
os.environ.setdefault("HUGGINGFACE_HUB_CACHE", str(hf_cache))
os.environ.setdefault("TRANSFORMERS_CACHE", str(hf_cache))
os.environ.setdefault("SENTENCE_TRANSFORMERS_HOME", str(hf_cache))
# 余計な参照を抑制
os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
# 既定のトークン探索を避けるため空文字に(公開モデルの匿名DLを想定)
os.environ.setdefault("HF_TOKEN", "")
_HF_INIT_DONE = True
def ensure_dirs() -> None:
"""書き込み可能なデータディレクトリを決定して作成。"""
global _DATA_DIR, _EXPORT_DIR
if _DATA_DIR is not None and _EXPORT_DIR is not None:
return # 既に確定済み
# 優先順: 環境変数 DATA_DIR → /data → /cache → /tmp → ./data
env_dir = os.getenv("DATA_DIR")
candidates = []
if env_dir:
candidates.append(Path(env_dir))
candidates.extend([
Path("/data/agent_studio"),
Path("/cache/agent_studio"),
Path("/tmp/agent_studio"),
Path("data"), # 最後に相対パス
])
chosen = _pick_writable_dir(candidates)
export = chosen / "exports"
chosen.mkdir(parents=True, exist_ok=True)
export.mkdir(parents=True, exist_ok=True)
_DATA_DIR = chosen
_EXPORT_DIR = export
# ★ Hugging Face 関連のホーム/キャッシュを、この書き込み可能ベースに固定
_init_hf_env(chosen)
def data_dir() -> Path:
ensure_dirs()
return _DATA_DIR # type: ignore
def export_dir() -> Path:
ensure_dirs()
return _EXPORT_DIR # type: ignore
# ====== 文字列分割ユーティリティ(rag_indexer が利用) ======
def chunk_text(text: str, max_chars: int = 1200, overlap: int = 200) -> List[str]:
"""
text を最大 max_chars のチャンクに分割する(重なり overlap 文字)。
句点・改行の境界を優先して分割し、見つからなければ生のスライスで分割。
"""
if not text:
return []
text = str(text)
max_chars = max(1, int(max_chars))
overlap = max(0, min(int(overlap), max_chars - 1))
chunks: List[str] = []
i = 0
n = len(text)
while i < n:
end = min(i + max_chars, n)
window = text[i:end]
# 末尾から句点・改行を探してそこまでを優先
cut = -1
for sep in ["\n\n", "。\n", "。", "\n", "!", "?", ".", "!", "?"]:
pos = window.rfind(sep)
if pos != -1 and (i + pos + len(sep)) - i >= max_chars * 0.6:
cut = pos + len(sep)
break
if cut == -1:
cut = len(window)
piece = window[:cut].strip()
if piece:
chunks.append(piece)
if i + cut >= n:
break
# 次の開始位置(オーバーラップあり)
i = i + cut - overlap
if i < 0:
i = 0
return chunks
# ====== 簡易トラッキングトークン(追加依存なしのHMAC方式) ======
_SECRET = os.getenv("TRACKING_SECRET", "dev-secret").encode("utf-8")
def _b64url_encode(b: bytes) -> str:
return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")
def _b64url_decode(s: str) -> bytes:
pad = "=" * (-len(s) % 4)
return base64.urlsafe_b64decode((s + pad).encode("ascii"))
def _sign(payload_bytes: bytes) -> str:
mac = hmac.new(_SECRET, payload_bytes, hashlib.sha256).digest()
return _b64url_encode(mac)
def make_tracking_token(payload: Dict[str, Any]) -> str:
"""
payload を JSON にして HMAC 署名し、'<b64json>.<sig>' 形式で返す。
例: {"company":"Test Inc.","ts":1690000000,"redirect":"/"}
"""
ensure_dirs()
payload = dict(payload or {})
if "ts" not in payload:
payload["ts"] = int(time.time())
b = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
return f"{_b64url_encode(b)}.{_sign(b)}"
def verify_tracking_token(token: str) -> Optional[Dict[str, Any]]:
"""
トークンの署名検証に成功したら payload を返す。失敗したら None。
"""
try:
part_json, part_sig = token.split(".", 1)
b = _b64url_decode(part_json)
expected = _sign(b)
if not hmac.compare_digest(part_sig, expected):
return None
return json.loads(b.decode("utf-8"))
except Exception:
return None
# ====== クリック/イベントの簡易ログ ======
def _events_path() -> Path:
return data_dir() / "events.jsonl"
def log_event(event_type: str, payload: Dict[str, Any], meta: Optional[Dict[str, Any]] = None) -> None:
"""
data/events.jsonl に1行追記。Spaceの Files タブから確認可能。
書き込み場所は ensure_dirs() により自動選択される。
"""
ensure_dirs()
rec = {
"ts": int(time.time()),
"type": event_type,
"payload": payload or {},
"meta": meta or {},
}
with open(_events_path(), "a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|