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")