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 ADDED
@@ -0,0 +1,4 @@
1
+ """SarfChat (صرف-شات): Algebraic Root×Pattern Transformer & Scaling-Law Harness."""
2
+
3
+ __version__ = "2.0.0"
4
+
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()