sarfchat 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sarfchat/__init__.py +4 -0
- sarfchat/chat_cli.py +193 -0
- sarfchat/cli_train.py +528 -0
- sarfchat/common.py +95 -0
- sarfchat/dataloader.py +513 -0
- sarfchat/loss_eval.py +523 -0
- sarfchat/model.py +1653 -0
- sarfchat/morph_codec.py +1542 -0
- sarfchat/optim.py +153 -0
- sarfchat/plots.py +296 -0
- sarfchat/priors.py +471 -0
- sarfchat/static/app.js +414 -0
- sarfchat/static/index.html +203 -0
- sarfchat/static/styles.css +443 -0
- sarfchat/web_arena.py +560 -0
- sarfchat-2.0.0.dist-info/METADATA +115 -0
- sarfchat-2.0.0.dist-info/RECORD +21 -0
- sarfchat-2.0.0.dist-info/WHEEL +4 -0
- sarfchat-2.0.0.dist-info/entry_points.txt +4 -0
- sarfchat-2.0.0.dist-info/licenses/LICENSE +176 -0
- sarfchat-2.0.0.dist-info/licenses/NOTICE +19 -0
sarfchat/__init__.py
ADDED
sarfchat/chat_cli.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Interactive CLI & Multi-Turn Arabic Chat REPL for SarfChat (`sarfchat/chat_cli.py`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import time
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from sarfchat.cli_train import generate_arabic_text, load_sarfchat_checkpoint
|
|
14
|
+
from sarfchat.common import (
|
|
15
|
+
CHECKPOINT_DIR,
|
|
16
|
+
PRIORS_PATH,
|
|
17
|
+
PRIORS_V2_PATH,
|
|
18
|
+
TOKENIZER_DIR,
|
|
19
|
+
detect_device_and_dtype,
|
|
20
|
+
)
|
|
21
|
+
from sarfchat.morph_codec import ByteBPETokenizer, SarfBPECodec
|
|
22
|
+
from sarfchat.priors import TarkibPriorsBundle
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DEFAULT_SYSTEM_PROMPT = (
|
|
26
|
+
"أنت «صرف-شات» (SarfChat)، مساعد ذكي فصيح متقن للغة العربية وعلومها التراثية والمعاصرة، "
|
|
27
|
+
"مبني على هندسة الجذور والأوزان الصرفية الفيزيائية. أجب بدقة ووضوح وبيان عربي سليم."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_default_checkpoint() -> Path:
|
|
32
|
+
for cand in ("sarfchat_v2_d16_sft", "sarfchat_v2_d16_base", "sarfchat_full_d12"):
|
|
33
|
+
p = CHECKPOINT_DIR / cand
|
|
34
|
+
if (p / "model.safetensors").exists():
|
|
35
|
+
return p
|
|
36
|
+
return CHECKPOINT_DIR / "sarfchat_v2_d16_sft"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
parser = argparse.ArgumentParser(description="Chat with or prompt a trained SarfChat checkpoint.")
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--checkpoint",
|
|
43
|
+
type=str,
|
|
44
|
+
default="",
|
|
45
|
+
help="Path or tag under checkpoints/ (defaults to sarfchat_v2_d16_sft if available)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument("-p", "--prompt", type=str, default="")
|
|
48
|
+
parser.add_argument("--chat", action="store_true", help="Launch interactive multi-turn Arabic chat REPL")
|
|
49
|
+
parser.add_argument("--raw", action="store_true", help="Use raw document continuation instead of chat template")
|
|
50
|
+
parser.add_argument("--system", type=str, default=DEFAULT_SYSTEM_PROMPT)
|
|
51
|
+
parser.add_argument("--max-steps", type=int, default=192)
|
|
52
|
+
parser.add_argument("--temperature", type=float, default=0.65)
|
|
53
|
+
parser.add_argument("--no-guard", action="store_true", help="Disable Tarkīb GPU phonotactic/anti-taṣḥīf shield")
|
|
54
|
+
parser.add_argument("--device", type=str, default="")
|
|
55
|
+
parser.add_argument("prompt_words", nargs="*", help="Optional prompt text without -p quotes")
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
joined_pos = " ".join(args.prompt_words).strip()
|
|
58
|
+
if joined_pos and not args.prompt:
|
|
59
|
+
args.prompt = joined_pos
|
|
60
|
+
|
|
61
|
+
if args.checkpoint:
|
|
62
|
+
ckpt_path = Path(args.checkpoint)
|
|
63
|
+
if not ckpt_path.exists():
|
|
64
|
+
ckpt_path = CHECKPOINT_DIR / args.checkpoint
|
|
65
|
+
else:
|
|
66
|
+
ckpt_path = _resolve_default_checkpoint()
|
|
67
|
+
|
|
68
|
+
cfg_file = ckpt_path / "config.json"
|
|
69
|
+
if not cfg_file.exists():
|
|
70
|
+
raise FileNotFoundError(f"Checkpoint config not found at {cfg_file}")
|
|
71
|
+
|
|
72
|
+
cfg_raw = json.loads(cfg_file.read_text(encoding="utf-8"))
|
|
73
|
+
arm = cfg_raw.get("model_config", {}).get("arm", "sarfchat_full")
|
|
74
|
+
dev, _, _ = detect_device_and_dtype(args.device)
|
|
75
|
+
|
|
76
|
+
if arm == "sarfchat_v2":
|
|
77
|
+
p_path = PRIORS_V2_PATH if PRIORS_V2_PATH.exists() else PRIORS_PATH
|
|
78
|
+
priors = TarkibPriorsBundle.load(p_path)
|
|
79
|
+
bpe_tok = ByteBPETokenizer.load(TOKENIZER_DIR / "bpe32k.json")
|
|
80
|
+
v2_map = dict(np.load(TOKENIZER_DIR / "bpe32k_tarkib_map.npz"))
|
|
81
|
+
bpe4_path = TOKENIZER_DIR / "bpe4k_fallback.json"
|
|
82
|
+
codec = SarfBPECodec(ByteBPETokenizer.load(bpe4_path), priors) if bpe4_path.exists() else None
|
|
83
|
+
model, meta = load_sarfchat_checkpoint(
|
|
84
|
+
ckpt_path,
|
|
85
|
+
priors=priors,
|
|
86
|
+
bpe_v2_map=v2_map,
|
|
87
|
+
device=dev,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
priors = TarkibPriorsBundle.load(PRIORS_PATH)
|
|
91
|
+
bpe_tok = ByteBPETokenizer.load(TOKENIZER_DIR / "bpe16k.json")
|
|
92
|
+
bpe4 = ByteBPETokenizer.load(TOKENIZER_DIR / "bpe4k_fallback.json")
|
|
93
|
+
codec = SarfBPECodec(bpe4, priors)
|
|
94
|
+
rmap = np.load(TOKENIZER_DIR / "bpe16k_root_map.npz")
|
|
95
|
+
model, meta = load_sarfchat_checkpoint(
|
|
96
|
+
ckpt_path,
|
|
97
|
+
priors=priors,
|
|
98
|
+
bpe_root_cids=rmap["root_cids"],
|
|
99
|
+
bpe_sem_ops=rmap["sem_ops"],
|
|
100
|
+
device=dev,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
is_sft = ckpt_path.name.endswith("_sft") or meta.get("stage") == "sft"
|
|
104
|
+
guard_on = not args.no_guard
|
|
105
|
+
|
|
106
|
+
if args.chat or not args.prompt:
|
|
107
|
+
use_raw_repl = args.raw or (not is_sft) or (model.arm == "sarfchat_full")
|
|
108
|
+
messages: List[Dict[str, str]] = []
|
|
109
|
+
if not use_raw_repl and args.system.strip():
|
|
110
|
+
messages.append({"role": "system", "content": args.system.strip()})
|
|
111
|
+
mode_lbl = "Raw Completion REPL" if use_raw_repl else "Interactive Chat"
|
|
112
|
+
print(
|
|
113
|
+
f"=== صرف-شات (SarfChat) {mode_lbl} | {ckpt_path.name} ({dev}) | Guard={'ON' if guard_on else 'OFF'} ===\n"
|
|
114
|
+
"اكتب نصاً أو سؤالاً بالعربية (أو '/reset' لمسح السياق، '/quit' للخروج)."
|
|
115
|
+
)
|
|
116
|
+
while True:
|
|
117
|
+
try:
|
|
118
|
+
user_in = input("\nأنت > ").strip()
|
|
119
|
+
except (EOFError, KeyboardInterrupt):
|
|
120
|
+
print("\nوداعاً!")
|
|
121
|
+
break
|
|
122
|
+
if not user_in:
|
|
123
|
+
continue
|
|
124
|
+
if user_in.lower() in ("/quit", "/exit", "exit", "quit"):
|
|
125
|
+
break
|
|
126
|
+
if user_in.lower() == "/reset":
|
|
127
|
+
messages = [{"role": "system", "content": args.system.strip()}] if (not use_raw_repl and args.system.strip()) else []
|
|
128
|
+
print("[تم مسح سجل المحادثة]")
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
if use_raw_repl:
|
|
132
|
+
out = generate_arabic_text(
|
|
133
|
+
model,
|
|
134
|
+
user_in,
|
|
135
|
+
max_new_steps=args.max_steps,
|
|
136
|
+
temperature=args.temperature,
|
|
137
|
+
guard_enabled=guard_on,
|
|
138
|
+
bpe_tok=bpe_tok if model.arm != "sarfchat_full" else None,
|
|
139
|
+
sarf_codec=codec,
|
|
140
|
+
)
|
|
141
|
+
print(f"\nصرف-شات [raw | {out['latency_ms']} ms] >\n{out['full_text']}")
|
|
142
|
+
else:
|
|
143
|
+
messages.append({"role": "user", "content": user_in})
|
|
144
|
+
t0 = time.perf_counter()
|
|
145
|
+
res = model.generate_chat(
|
|
146
|
+
messages,
|
|
147
|
+
bpe_tok=bpe_tok,
|
|
148
|
+
max_new_tokens=args.max_steps,
|
|
149
|
+
temperature=args.temperature,
|
|
150
|
+
guard_enabled=guard_on,
|
|
151
|
+
sarf_codec=codec,
|
|
152
|
+
)
|
|
153
|
+
dt_ms = (time.perf_counter() - t0) * 1000.0
|
|
154
|
+
reply = res["reply"]
|
|
155
|
+
messages.append({"role": "assistant", "content": reply})
|
|
156
|
+
print(f"\nصرف-شات [{res['tokens_generated']} tok | {dt_ms:.0f} ms] >\n{reply}")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
prompt_text = args.prompt or "اللغة العربية تمتاز بنظام اشتقاقي"
|
|
160
|
+
if is_sft and not args.raw and model.arm != "sarfchat_full":
|
|
161
|
+
msgs: List[Dict[str, str]] = []
|
|
162
|
+
if args.system.strip():
|
|
163
|
+
msgs.append({"role": "system", "content": args.system.strip()})
|
|
164
|
+
msgs.append({"role": "user", "content": prompt_text})
|
|
165
|
+
t0 = time.perf_counter()
|
|
166
|
+
res = model.generate_chat(
|
|
167
|
+
msgs,
|
|
168
|
+
bpe_tok=bpe_tok,
|
|
169
|
+
max_new_tokens=args.max_steps,
|
|
170
|
+
temperature=args.temperature,
|
|
171
|
+
guard_enabled=guard_on,
|
|
172
|
+
sarf_codec=codec,
|
|
173
|
+
)
|
|
174
|
+
dt_ms = (time.perf_counter() - t0) * 1000.0
|
|
175
|
+
print(f"[{ckpt_path.name} | chat | guard={'ON' if guard_on else 'OFF'} | {dt_ms:.1f} ms]")
|
|
176
|
+
print(res["reply"])
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
out = generate_arabic_text(
|
|
180
|
+
model,
|
|
181
|
+
prompt_text,
|
|
182
|
+
max_new_steps=args.max_steps,
|
|
183
|
+
temperature=args.temperature,
|
|
184
|
+
guard_enabled=guard_on,
|
|
185
|
+
bpe_tok=bpe_tok if model.arm != "sarfchat_full" else None,
|
|
186
|
+
sarf_codec=codec,
|
|
187
|
+
)
|
|
188
|
+
print(f"[{model.arm} | guard={'ON' if out['guard_enabled'] else 'OFF'} | {out['latency_ms']} ms]")
|
|
189
|
+
print(out["full_text"])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|