alpiecode 0.6.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.
- alpiecode-0.6.0.dist-info/METADATA +14 -0
- alpiecode-0.6.0.dist-info/RECORD +17 -0
- alpiecode-0.6.0.dist-info/WHEEL +5 -0
- alpiecode-0.6.0.dist-info/entry_points.txt +3 -0
- alpiecode-0.6.0.dist-info/top_level.txt +1 -0
- codeagent/__init__.py +1 -0
- codeagent/agent.py +989 -0
- codeagent/cli.py +215 -0
- codeagent/compaction.py +163 -0
- codeagent/config.py +195 -0
- codeagent/github.py +241 -0
- codeagent/guardian.py +160 -0
- codeagent/local_model.py +460 -0
- codeagent/media.py +286 -0
- codeagent/memory.py +130 -0
- codeagent/tools.py +718 -0
- codeagent/updater.py +126 -0
codeagent/local_model.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local GGUF model engine for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Downloads and caches GGUF models from HuggingFace (`169Pi/Alpie_learn_prototype_GGUF_NEW`).
|
|
5
|
+
Runs local inference via `llama-cpp-python` with automatic GPU acceleration / CPU fallback.
|
|
6
|
+
Provides an OpenAI-compatible `create_chat_completion` interface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
CACHE_DIR = Path.home() / ".alpiecode" / "models"
|
|
15
|
+
DEFAULT_REPO = "169Pi/Alpie_learn_prototype_GGUF_NEW"
|
|
16
|
+
DEFAULT_CTX_SIZE = 32768 # 32k tokens default for fast loading (supports up to 256k)
|
|
17
|
+
|
|
18
|
+
# Preload WSL NVIDIA CUDA driver if running under WSL
|
|
19
|
+
if sys.platform.startswith("linux"):
|
|
20
|
+
try:
|
|
21
|
+
import ctypes
|
|
22
|
+
wsl_cuda = Path("/usr/lib/wsl/lib/libcuda.so.1")
|
|
23
|
+
if wsl_cuda.exists():
|
|
24
|
+
ctypes.CDLL(str(wsl_cuda), mode=ctypes.RTLD_GLOBAL)
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── GPU Auto-Detection ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
def detect_gpu() -> int:
|
|
32
|
+
"""
|
|
33
|
+
Detect GPU hardware and return optimal layer offload count.
|
|
34
|
+
Returns: -1 (all layers to GPU), 26 (6GB VRAM), 0 (CPU only).
|
|
35
|
+
|
|
36
|
+
Priority: NVIDIA (nvidia-smi) > macOS Metal > Vulkan discrete GPU > CPU.
|
|
37
|
+
Skips Intel/AMD integrated GPUs (UHD, Iris, Vega) — too weak for LLM inference.
|
|
38
|
+
"""
|
|
39
|
+
if os.environ.get("CUDA_VISIBLE_DEVICES") == "-1":
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
import llama_cpp
|
|
44
|
+
if not getattr(llama_cpp, "llama_supports_gpu_offload", lambda: False)():
|
|
45
|
+
return 0 # Build doesn't support GPU
|
|
46
|
+
except Exception:
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
# Build supports GPU — now check if actual GPU hardware exists
|
|
50
|
+
import subprocess
|
|
51
|
+
import platform
|
|
52
|
+
|
|
53
|
+
# ── 1. NVIDIA GPU (highest priority — most common dedicated GPU) ──
|
|
54
|
+
nvidia_smi_candidates = [
|
|
55
|
+
"nvidia-smi",
|
|
56
|
+
"nvidia-smi.exe",
|
|
57
|
+
"/usr/lib/wsl/lib/nvidia-smi",
|
|
58
|
+
r"C:\Windows\System32\nvidia-smi.exe",
|
|
59
|
+
r"C:\Program Files\NVIDIA Corporation\NVSMI\nvidia-smi.exe",
|
|
60
|
+
]
|
|
61
|
+
for smi_bin in nvidia_smi_candidates:
|
|
62
|
+
try:
|
|
63
|
+
res = subprocess.run(
|
|
64
|
+
[smi_bin, "--query-gpu=memory.total", "--format=csv,noheader,nounits"],
|
|
65
|
+
capture_output=True, text=True, timeout=3
|
|
66
|
+
)
|
|
67
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
68
|
+
vram_mb = int(res.stdout.strip().split("\n")[0].strip())
|
|
69
|
+
if vram_mb <= 6500: # 6GB VRAM (e.g. RTX 3050 Laptop)
|
|
70
|
+
return 26 # Partial offload — keep 2GB VRAM free
|
|
71
|
+
return -1 # 8GB+ VRAM — offload all layers
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# Windows fallback: check via wmic or powershell for NVIDIA controllers
|
|
76
|
+
if platform.system() == "Windows":
|
|
77
|
+
try:
|
|
78
|
+
res = subprocess.run(
|
|
79
|
+
["powershell", "-NoProfile", "-Command", "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name"],
|
|
80
|
+
capture_output=True, text=True, timeout=3
|
|
81
|
+
)
|
|
82
|
+
if res.returncode == 0 and "NVIDIA" in res.stdout.upper():
|
|
83
|
+
return -1 # NVIDIA GPU found on Windows
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
# ── 2. macOS Metal (Apple Silicon — unified memory, always fast) ──
|
|
88
|
+
if platform.system() == "Darwin":
|
|
89
|
+
try:
|
|
90
|
+
res = subprocess.run(["sysctl", "-n", "hw.memsize"],
|
|
91
|
+
capture_output=True, text=True, timeout=2)
|
|
92
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
93
|
+
return -1 # Apple Silicon — offload all layers
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# ── 3. No dedicated GPU found — use CPU ──
|
|
98
|
+
# NOTE: We intentionally skip Vulkan-only GPUs (Intel UHD, Intel Iris,
|
|
99
|
+
# AMD Vega integrated) because they are too slow for LLM inference.
|
|
100
|
+
# Users with AMD discrete GPUs can set ALPIECODE_GPU_LAYERS=-1 to force.
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ── Model Downloader ─────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
def download_model(repo_id: str = DEFAULT_REPO, token: Optional[str] = None) -> Path:
|
|
107
|
+
"""
|
|
108
|
+
Download single GGUF model from HuggingFace using huggingface_hub.
|
|
109
|
+
Saves to ~/.alpiecode/models/ and returns the local Path.
|
|
110
|
+
|
|
111
|
+
OFFLINE-FIRST: Checks local cache before any network call.
|
|
112
|
+
"""
|
|
113
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
hf_token = token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
115
|
+
|
|
116
|
+
# ── STEP 1: Check local cache FIRST (zero network) ───────────────
|
|
117
|
+
local_gguf = list(CACHE_DIR.glob("*.gguf"))
|
|
118
|
+
# Filter out mmproj files for main model
|
|
119
|
+
local_main = [f for f in local_gguf if "mmproj" not in f.name.lower()]
|
|
120
|
+
if local_main:
|
|
121
|
+
return local_main[0]
|
|
122
|
+
if local_gguf:
|
|
123
|
+
return local_gguf[0]
|
|
124
|
+
|
|
125
|
+
# ── STEP 2: Model not cached — must download from HuggingFace ────
|
|
126
|
+
try:
|
|
127
|
+
from huggingface_hub import hf_hub_download, list_repo_files
|
|
128
|
+
except ImportError:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"huggingface_hub package is missing. Install it with: pip install huggingface_hub"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# List files in repo to find the GGUF file
|
|
134
|
+
try:
|
|
135
|
+
files = list_repo_files(repo_id=repo_id, token=hf_token)
|
|
136
|
+
gguf_files = [f for f in files if f.endswith(".gguf")]
|
|
137
|
+
except Exception as e:
|
|
138
|
+
err_str = str(e).lower()
|
|
139
|
+
# Differentiate auth/access errors vs network errors
|
|
140
|
+
if "401" in err_str or "unauthorized" in err_str:
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
f"\n❌ HuggingFace Authentication Failed for '{repo_id}'.\n"
|
|
143
|
+
" Your HF token is invalid or expired.\n"
|
|
144
|
+
" → Go to https://huggingface.co/settings/tokens to generate a new token.\n"
|
|
145
|
+
" → Then run: alpiecode init"
|
|
146
|
+
) from e
|
|
147
|
+
elif "403" in err_str or "forbidden" in err_str or "storage limit" in err_str:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"\n❌ HuggingFace Access Denied (403) for '{repo_id}'.\n"
|
|
150
|
+
" Your token does not have access to this private repo.\n"
|
|
151
|
+
" → Ask the 169Pi team to grant you access, or check your token permissions."
|
|
152
|
+
) from e
|
|
153
|
+
elif "404" in err_str or "not found" in err_str:
|
|
154
|
+
raise RuntimeError(
|
|
155
|
+
f"\n❌ HuggingFace repo '{repo_id}' not found (404).\n"
|
|
156
|
+
" The model repository may not exist or is private.\n"
|
|
157
|
+
" → Verify the repo name and ensure your HF token has read access."
|
|
158
|
+
) from e
|
|
159
|
+
else:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
"\n╭──────────────────────────────────────────────────────────────╮\n"
|
|
162
|
+
"│ GGUF model not found locally and could not download. │\n"
|
|
163
|
+
"│ │\n"
|
|
164
|
+
f"│ Error: {str(e)[:52]:<53}│\n"
|
|
165
|
+
"│ │\n"
|
|
166
|
+
"│ Connect to internet and run: │\n"
|
|
167
|
+
"│ alpiecode init │\n"
|
|
168
|
+
"│ │\n"
|
|
169
|
+
"│ After the one-time download, offline mode works forever. │\n"
|
|
170
|
+
"╰──────────────────────────────────────────────────────────────╯"
|
|
171
|
+
) from e
|
|
172
|
+
|
|
173
|
+
# Separate main model file from mmproj (vision projector) file
|
|
174
|
+
main_gguf_files = [f for f in gguf_files if "mmproj" not in f.lower()]
|
|
175
|
+
mmproj_files = [f for f in gguf_files if "mmproj" in f.lower()]
|
|
176
|
+
|
|
177
|
+
if not main_gguf_files:
|
|
178
|
+
main_gguf_files = gguf_files
|
|
179
|
+
|
|
180
|
+
target_file = main_gguf_files[0]
|
|
181
|
+
cached_file = CACHE_DIR / target_file
|
|
182
|
+
|
|
183
|
+
if not cached_file.exists():
|
|
184
|
+
print(f"📥 Downloading local model from HuggingFace ({repo_id}/{target_file})...")
|
|
185
|
+
print(" This is a one-time download. Subsequent runs will work 100% offline.")
|
|
186
|
+
try:
|
|
187
|
+
hf_hub_download(
|
|
188
|
+
repo_id=repo_id,
|
|
189
|
+
filename=target_file,
|
|
190
|
+
local_dir=CACHE_DIR,
|
|
191
|
+
token=hf_token,
|
|
192
|
+
)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
err_str = str(e)
|
|
195
|
+
if "403" in err_str or "storage limit" in err_str.lower() or "forbidden" in err_str.lower():
|
|
196
|
+
raise RuntimeError(
|
|
197
|
+
f"HuggingFace 403 Forbidden Error for repo '{repo_id}':\n"
|
|
198
|
+
" 'Private repository storage limit reached for 169Pi account.'\n"
|
|
199
|
+
" 💡 Please ask your 169Pi organization admin to upgrade the HF storage plan or free up space on HuggingFace."
|
|
200
|
+
) from e
|
|
201
|
+
raise RuntimeError(f"HuggingFace Download Failed for '{repo_id}/{target_file}': {e}") from e
|
|
202
|
+
|
|
203
|
+
# Download mmproj file if available (for vision features)
|
|
204
|
+
if mmproj_files:
|
|
205
|
+
mmproj_target = mmproj_files[0]
|
|
206
|
+
mmproj_cached = CACHE_DIR / mmproj_target
|
|
207
|
+
if not mmproj_cached.exists():
|
|
208
|
+
try:
|
|
209
|
+
print(f"📥 Downloading vision projector ({repo_id}/{mmproj_target})...")
|
|
210
|
+
hf_hub_download(
|
|
211
|
+
repo_id=repo_id,
|
|
212
|
+
filename=mmproj_target,
|
|
213
|
+
local_dir=CACHE_DIR,
|
|
214
|
+
token=hf_token,
|
|
215
|
+
)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
print(f"⚠️ Could not download vision projector ({mmproj_target}): {e}")
|
|
218
|
+
|
|
219
|
+
return cached_file
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ── Local Model Engine Class ──────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
def _ensure_llama_cpp():
|
|
225
|
+
"""Auto-install pre-compiled binary wheel for llama-cpp-python if missing."""
|
|
226
|
+
try:
|
|
227
|
+
from llama_cpp import Llama
|
|
228
|
+
return Llama
|
|
229
|
+
except ImportError:
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
print("⚙️ Auto-installing pre-compiled local GGUF engine (one-time setup)...")
|
|
233
|
+
import sys
|
|
234
|
+
import subprocess
|
|
235
|
+
|
|
236
|
+
is_win = sys.platform == "win32"
|
|
237
|
+
is_mac = sys.platform == "darwin"
|
|
238
|
+
if is_win:
|
|
239
|
+
wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-vulkan/llama_cpp_python-0.3.34-py3-none-win_amd64.whl"
|
|
240
|
+
elif is_mac:
|
|
241
|
+
wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-metal/llama_cpp_python-0.3.34-py3-none-macosx_11_0_arm64.whl"
|
|
242
|
+
else:
|
|
243
|
+
wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-vulkan/llama_cpp_python-0.3.34-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"
|
|
244
|
+
|
|
245
|
+
installed = False
|
|
246
|
+
last_error = ""
|
|
247
|
+
for cmd_base in [["uv", "pip", "install", wheel_url], [sys.executable, "-m", "pip", "install", wheel_url]]:
|
|
248
|
+
try:
|
|
249
|
+
res = subprocess.run(cmd_base, capture_output=True, text=True, timeout=120)
|
|
250
|
+
if res.returncode == 0:
|
|
251
|
+
installed = True
|
|
252
|
+
break
|
|
253
|
+
else:
|
|
254
|
+
last_error = res.stderr.strip() or res.stdout.strip()
|
|
255
|
+
except Exception as e:
|
|
256
|
+
last_error = str(e)
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
from llama_cpp import Llama
|
|
261
|
+
print("✅ Pre-compiled local GGUF engine installed successfully!")
|
|
262
|
+
return Llama
|
|
263
|
+
except ImportError:
|
|
264
|
+
err_detail = f"\n Last error: {last_error[:200]}" if last_error else ""
|
|
265
|
+
raise RuntimeError(
|
|
266
|
+
"\n╭────────────────────────────────────────────────────────────╮\n"
|
|
267
|
+
"│ Failed to auto-install local GGUF engine. │\n"
|
|
268
|
+
"│ Internet connection required for first-time setup. │\n"
|
|
269
|
+
"╰────────────────────────────────────────────────────────────╯"
|
|
270
|
+
f"{err_detail}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class LocalModel:
|
|
275
|
+
def __init__(self, repo_id: str = DEFAULT_REPO, n_ctx: int = DEFAULT_CTX_SIZE,
|
|
276
|
+
n_gpu_layers: Optional[int] = None, token: Optional[str] = None):
|
|
277
|
+
self.repo_id = repo_id
|
|
278
|
+
self.n_ctx = n_ctx
|
|
279
|
+
self.n_gpu_layers = n_gpu_layers if n_gpu_layers is not None else detect_gpu()
|
|
280
|
+
self.token = token
|
|
281
|
+
self.model_path: Optional[Path] = None
|
|
282
|
+
self._llm = None
|
|
283
|
+
|
|
284
|
+
def ensure_model(self) -> Path:
|
|
285
|
+
"""Download model if not present."""
|
|
286
|
+
if not self.model_path or not self.model_path.exists():
|
|
287
|
+
self.model_path = download_model(self.repo_id, token=self.token)
|
|
288
|
+
return self.model_path
|
|
289
|
+
|
|
290
|
+
def load(self):
|
|
291
|
+
"""Load the model into memory via llama-cpp-python."""
|
|
292
|
+
if self._llm is not None:
|
|
293
|
+
return self._llm
|
|
294
|
+
|
|
295
|
+
model_path = self.ensure_model()
|
|
296
|
+
Llama = _ensure_llama_cpp()
|
|
297
|
+
|
|
298
|
+
accel = "GPU" if self.n_gpu_layers != 0 else "CPU"
|
|
299
|
+
print(f"🧠 Loading local GGUF model: {model_path.name}")
|
|
300
|
+
print(f" Context: {self.n_ctx} tokens | Mode: {accel} | Threads: {max(1, (os.cpu_count() or 4) - 1)}")
|
|
301
|
+
if accel == "CPU":
|
|
302
|
+
print(f" ⏳ CPU loading ~4GB model — this takes 30-90 seconds on first run...")
|
|
303
|
+
else:
|
|
304
|
+
print(f" ⚡ GPU-accelerated loading...")
|
|
305
|
+
|
|
306
|
+
n_threads = max(1, (os.cpu_count() or 4) - 1)
|
|
307
|
+
|
|
308
|
+
# Suppress noisy ggml_vulkan/ggml_cuda stderr messages from C library
|
|
309
|
+
# These confuse users ("ggml_vulkan: Found 1 Vulkan devices: Intel UHD...")
|
|
310
|
+
import time
|
|
311
|
+
t0 = time.monotonic()
|
|
312
|
+
_stderr_fd = None
|
|
313
|
+
_devnull_fd = None
|
|
314
|
+
try:
|
|
315
|
+
_stderr_fd = os.dup(2)
|
|
316
|
+
_devnull_fd = os.open(os.devnull, os.O_WRONLY)
|
|
317
|
+
os.dup2(_devnull_fd, 2)
|
|
318
|
+
except Exception:
|
|
319
|
+
_stderr_fd = None # Fallback: don't suppress if dup2 fails
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
self._llm = Llama(
|
|
323
|
+
model_path=str(model_path),
|
|
324
|
+
n_ctx=self.n_ctx,
|
|
325
|
+
n_batch=2048 if self.n_gpu_layers == 0 else 4096,
|
|
326
|
+
n_threads=n_threads,
|
|
327
|
+
n_gpu_layers=self.n_gpu_layers,
|
|
328
|
+
use_mmap=True,
|
|
329
|
+
flash_attn=(self.n_gpu_layers != 0), # Flash Attention only for GPU
|
|
330
|
+
verbose=False,
|
|
331
|
+
)
|
|
332
|
+
finally:
|
|
333
|
+
# Restore stderr
|
|
334
|
+
if _stderr_fd is not None:
|
|
335
|
+
try:
|
|
336
|
+
os.dup2(_stderr_fd, 2)
|
|
337
|
+
os.close(_stderr_fd)
|
|
338
|
+
except Exception:
|
|
339
|
+
pass
|
|
340
|
+
if _devnull_fd is not None:
|
|
341
|
+
try:
|
|
342
|
+
os.close(_devnull_fd)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
elapsed = time.monotonic() - t0
|
|
347
|
+
print(f" ✅ Model loaded in {elapsed:.1f}s — ready!")
|
|
348
|
+
return self._llm
|
|
349
|
+
|
|
350
|
+
def create_chat_completion(self, messages: List[Dict[str, Any]], tools: Optional[List[Dict[str, Any]]] = None,
|
|
351
|
+
temperature: float = 0.2, max_tokens: int = 4096, **kwargs) -> Any:
|
|
352
|
+
"""
|
|
353
|
+
Run inference matching OpenAI chat completions interface.
|
|
354
|
+
Returns a dot-accessible structure (resp.choices[0].message).
|
|
355
|
+
"""
|
|
356
|
+
llm = self.load()
|
|
357
|
+
|
|
358
|
+
enable_thinking = kwargs.get("enable_thinking", True)
|
|
359
|
+
|
|
360
|
+
# When thinking is disabled, inject assistant prefill to skip reasoning tokens
|
|
361
|
+
# This eliminates ~6s of thinking token generation per turn
|
|
362
|
+
if not enable_thinking:
|
|
363
|
+
msgs = list(messages) + [{"role": "assistant", "content": "<think>\n\n</think>\n\n"}]
|
|
364
|
+
else:
|
|
365
|
+
msgs = messages
|
|
366
|
+
|
|
367
|
+
params = {
|
|
368
|
+
"messages": msgs,
|
|
369
|
+
"temperature": 0.1 if not enable_thinking else temperature,
|
|
370
|
+
"max_tokens": max_tokens,
|
|
371
|
+
}
|
|
372
|
+
if tools:
|
|
373
|
+
params["tools"] = tools
|
|
374
|
+
params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
375
|
+
|
|
376
|
+
result_dict = llm.create_chat_completion(**params)
|
|
377
|
+
return _DictWrapper(result_dict)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ── OpenAI-style Object Wrapper ───────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
class _DictWrapper:
|
|
383
|
+
"""Wraps dictionary output from llama-cpp to allow dot-notation (resp.choices[0].message)."""
|
|
384
|
+
def __init__(self, data: dict):
|
|
385
|
+
self._data = data
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def choices(self):
|
|
389
|
+
return [_ChoiceWrapper(c) for c in self._data.get("choices", [])]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class _ChoiceWrapper:
|
|
393
|
+
def __init__(self, data: dict):
|
|
394
|
+
self._data = data
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def message(self):
|
|
398
|
+
return _MessageWrapper(self._data.get("message", {}))
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class _MessageWrapper:
|
|
402
|
+
def __init__(self, data: dict):
|
|
403
|
+
self._data = data
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def content(self):
|
|
407
|
+
return self._data.get("content")
|
|
408
|
+
|
|
409
|
+
@property
|
|
410
|
+
def role(self):
|
|
411
|
+
return self._data.get("role", "assistant")
|
|
412
|
+
|
|
413
|
+
@property
|
|
414
|
+
def reasoning(self):
|
|
415
|
+
return self._data.get("reasoning") or self._data.get("reasoning_content")
|
|
416
|
+
|
|
417
|
+
@property
|
|
418
|
+
def reasoning_content(self):
|
|
419
|
+
return self.reasoning
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def tool_calls(self):
|
|
423
|
+
raw_tc = self._data.get("tool_calls")
|
|
424
|
+
if not raw_tc:
|
|
425
|
+
return None
|
|
426
|
+
return [_ToolCallWrapper(tc) for tc in raw_tc]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class _ToolCallWrapper:
|
|
430
|
+
def __init__(self, data: dict):
|
|
431
|
+
self._data = data
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def id(self):
|
|
435
|
+
return self._data.get("id", "call_local")
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def type(self):
|
|
439
|
+
return self._data.get("type", "function")
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def function(self):
|
|
443
|
+
return _FunctionWrapper(self._data.get("function", {}))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class _FunctionWrapper:
|
|
447
|
+
def __init__(self, data: dict):
|
|
448
|
+
self._data = data
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def name(self):
|
|
452
|
+
return self._data.get("name")
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def arguments(self):
|
|
456
|
+
args = self._data.get("arguments")
|
|
457
|
+
if isinstance(args, dict):
|
|
458
|
+
import json
|
|
459
|
+
return json.dumps(args)
|
|
460
|
+
return args or "{}"
|