alpiecode 9.0.6__tar.gz → 10.0.1__tar.gz
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-9.0.6 → alpiecode-10.0.1}/PKG-INFO +2 -2
- {alpiecode-9.0.6 → alpiecode-10.0.1}/README.md +1 -1
- {alpiecode-9.0.6 → alpiecode-10.0.1}/pyproject.toml +2 -2
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/PKG-INFO +2 -2
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/__init__.py +2 -1
- alpiecode-10.0.1/src/codeagent/backends/local_backend.py +314 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/config.py +4 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/discovery.py +6 -1
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/doctor.py +72 -32
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/local_model.py +16 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/orchestrator.py +90 -13
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/server.py +5 -6
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/tools.py +6 -2
- alpiecode-10.0.1/test/test_offline_mode.py +314 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_phase1.py +42 -0
- alpiecode-9.0.6/src/codeagent/backends/local_backend.py +0 -167
- alpiecode-9.0.6/test/test_offline_mode.py +0 -78
- {alpiecode-9.0.6 → alpiecode-10.0.1}/setup.cfg +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode/__init__.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/agent.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/backends/base.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/cache.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/cli.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/client.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/compaction.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/context.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/executor.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/git_ops.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/github.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/guardian.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/guardrails.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/media.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/memory.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/progress.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/prompt.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/rephraser.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/session.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/updater.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_audit_fixes.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_chat_mode.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_claude_codex_capabilities.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_phase2.py +0 -0
- {alpiecode-9.0.6 → alpiecode-10.0.1}/test/test_phase3.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version:
|
|
4
|
-
Summary: AlpieCode
|
|
3
|
+
Version: 10.0.1
|
|
4
|
+
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
7
7
|
Requires-Dist: rich>=13.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AlpieCode
|
|
2
2
|
|
|
3
3
|
[](https://www.python.org/)
|
|
4
4
|
[](https://github.com/169Pi/codeagent-poc)
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpiecode"
|
|
7
|
-
version = "
|
|
8
|
-
description = "AlpieCode
|
|
7
|
+
version = "10.0.1"
|
|
8
|
+
description = "AlpieCode"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"huggingface_hub>=0.20.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version:
|
|
4
|
-
Summary: AlpieCode
|
|
3
|
+
Version: 10.0.1
|
|
4
|
+
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
7
7
|
Requires-Dist: rich>=13.0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
3
3
|
warnings.filterwarnings("ignore", category=FutureWarning)
|
|
4
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, message=r".*invalid escape sequence.*")
|
|
4
5
|
warnings.filterwarnings("ignore", module="jupyter_client.*")
|
|
5
6
|
|
|
6
7
|
"""
|
|
@@ -16,7 +17,7 @@ Supports:
|
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Optional, Any, Dict
|
|
18
19
|
|
|
19
|
-
__version__ = "
|
|
20
|
+
__version__ = "10.0.1"
|
|
20
21
|
|
|
21
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
22
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local GGUF model inference backend.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from ..config import Config
|
|
8
|
+
from ..local_model import LocalModel
|
|
9
|
+
from .base import ChatResponse, ToolCall
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalBackend:
|
|
13
|
+
"""Backend for local GGUF model inference via llama-cpp-python."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cfg: Config):
|
|
16
|
+
self._cfg = cfg
|
|
17
|
+
self._model: Optional[LocalModel] = None
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return f"Local GGUF ({self._cfg.model_repo})"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
return True # Local model is always available (downloads if missing)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def context_window(self) -> int:
|
|
29
|
+
return self._cfg.n_ctx
|
|
30
|
+
|
|
31
|
+
def _ensure_model(self) -> LocalModel:
|
|
32
|
+
if self._model is None:
|
|
33
|
+
self._model = LocalModel(
|
|
34
|
+
repo_id=self._cfg.model_repo,
|
|
35
|
+
n_ctx=self._cfg.n_ctx,
|
|
36
|
+
n_gpu_layers=self._cfg.n_gpu_layers,
|
|
37
|
+
token=self._cfg.hf_token,
|
|
38
|
+
)
|
|
39
|
+
return self._model
|
|
40
|
+
|
|
41
|
+
def load_model(self) -> None:
|
|
42
|
+
"""Explicitly load model into VRAM/RAM (warmup)."""
|
|
43
|
+
model = self._ensure_model()
|
|
44
|
+
model.load()
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _sanitize_messages_for_gguf(messages: List[dict]) -> List[dict]:
|
|
48
|
+
"""
|
|
49
|
+
Sanitize OpenAI-format messages for GGUF Jinja2 chat templates.
|
|
50
|
+
Fixes 'Can only get item pairs from a mapping' errors by:
|
|
51
|
+
- Ensuring content is always a string (never None or list)
|
|
52
|
+
- Stripping tool_calls and tool messages (GGUF templates don't support them)
|
|
53
|
+
- Removing tool-related role messages
|
|
54
|
+
"""
|
|
55
|
+
sanitized = []
|
|
56
|
+
for msg in messages:
|
|
57
|
+
role = msg.get("role", "")
|
|
58
|
+
if role == "tool":
|
|
59
|
+
# Bridge tool execution results into ChatML user turns so local model sees outputs
|
|
60
|
+
name = msg.get("name") or "tool"
|
|
61
|
+
raw_content = msg.get("content") or ""
|
|
62
|
+
if not isinstance(raw_content, str):
|
|
63
|
+
raw_content = str(raw_content)
|
|
64
|
+
# Smart failure-preserving truncation for local offline context
|
|
65
|
+
from ..tools import _smart_truncate
|
|
66
|
+
raw_content = _smart_truncate(raw_content, max_chars=1800)
|
|
67
|
+
|
|
68
|
+
tool_text = f"[Tool Result for {name}]:\n{raw_content}"
|
|
69
|
+
if sanitized and sanitized[-1].get("role") == "user":
|
|
70
|
+
sanitized[-1]["content"] += "\n\n" + tool_text
|
|
71
|
+
else:
|
|
72
|
+
sanitized.append({"role": "user", "content": tool_text})
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
clean = {"role": role}
|
|
76
|
+
|
|
77
|
+
# Ensure content is always a string
|
|
78
|
+
content = msg.get("content")
|
|
79
|
+
if content is None:
|
|
80
|
+
content = ""
|
|
81
|
+
elif isinstance(content, list):
|
|
82
|
+
# Multimodal content (image_url + text blocks) — extract text parts
|
|
83
|
+
text_parts = []
|
|
84
|
+
for part in content:
|
|
85
|
+
if isinstance(part, dict):
|
|
86
|
+
if part.get("type") == "text":
|
|
87
|
+
text_parts.append(part.get("text", ""))
|
|
88
|
+
elif isinstance(part, str):
|
|
89
|
+
text_parts.append(part)
|
|
90
|
+
content = "\n".join(text_parts) if text_parts else ""
|
|
91
|
+
elif not isinstance(content, str):
|
|
92
|
+
content = str(content)
|
|
93
|
+
|
|
94
|
+
# For assistant messages with tool_calls, embed tool call info in content text
|
|
95
|
+
if role == "assistant" and msg.get("tool_calls"):
|
|
96
|
+
tc_descriptions = []
|
|
97
|
+
for tc in msg["tool_calls"]:
|
|
98
|
+
if isinstance(tc, dict):
|
|
99
|
+
fn = tc.get("function", {})
|
|
100
|
+
name = fn.get("name", "unknown")
|
|
101
|
+
args = fn.get("arguments", "{}")
|
|
102
|
+
tc_descriptions.append(f"[Called tool: {name}({args})]")
|
|
103
|
+
if tc_descriptions:
|
|
104
|
+
content = (content + "\n" + "\n".join(tc_descriptions)).strip()
|
|
105
|
+
# Do NOT include tool_calls key — GGUF template will crash on it
|
|
106
|
+
|
|
107
|
+
clean["content"] = content
|
|
108
|
+
sanitized.append(clean)
|
|
109
|
+
|
|
110
|
+
return sanitized
|
|
111
|
+
|
|
112
|
+
def chat_completion(
|
|
113
|
+
self,
|
|
114
|
+
messages: List[dict],
|
|
115
|
+
tools: Optional[List[dict]] = None,
|
|
116
|
+
temperature: float = 0.2,
|
|
117
|
+
max_tokens: int = 2048,
|
|
118
|
+
enable_thinking: bool = True,
|
|
119
|
+
) -> ChatResponse:
|
|
120
|
+
model = self._ensure_model()
|
|
121
|
+
sanitized_msgs = self._sanitize_messages_for_gguf(messages)
|
|
122
|
+
resp = model.create_chat_completion(
|
|
123
|
+
messages=sanitized_msgs,
|
|
124
|
+
tools=tools,
|
|
125
|
+
tool_choice="auto",
|
|
126
|
+
temperature=temperature,
|
|
127
|
+
max_tokens=max_tokens,
|
|
128
|
+
enable_thinking=enable_thinking,
|
|
129
|
+
)
|
|
130
|
+
msg = resp.choices[0].message
|
|
131
|
+
|
|
132
|
+
tool_calls = None
|
|
133
|
+
if msg.tool_calls:
|
|
134
|
+
import json
|
|
135
|
+
tool_calls = []
|
|
136
|
+
for tc in msg.tool_calls:
|
|
137
|
+
args = tc.function.arguments
|
|
138
|
+
if isinstance(args, str):
|
|
139
|
+
try:
|
|
140
|
+
args = json.loads(args or "{}")
|
|
141
|
+
except Exception:
|
|
142
|
+
args = {}
|
|
143
|
+
tool_calls.append(
|
|
144
|
+
ToolCall(
|
|
145
|
+
id=tc.id,
|
|
146
|
+
name=tc.function.name,
|
|
147
|
+
arguments=args if isinstance(args, dict) else {},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
|
|
152
|
+
|
|
153
|
+
return ChatResponse(
|
|
154
|
+
content=msg.content,
|
|
155
|
+
reasoning=reasoning,
|
|
156
|
+
tool_calls=tool_calls,
|
|
157
|
+
raw=resp,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def chat_completion_stream(
|
|
161
|
+
self,
|
|
162
|
+
messages: List[dict],
|
|
163
|
+
tools: Optional[List[dict]] = None,
|
|
164
|
+
temperature: Optional[float] = None,
|
|
165
|
+
max_tokens: Optional[int] = None,
|
|
166
|
+
enable_thinking: bool = True,
|
|
167
|
+
):
|
|
168
|
+
"""
|
|
169
|
+
Stream local GGUF completion yielding progressive thinking deltas,
|
|
170
|
+
content tokens, and the final ChatResponse.
|
|
171
|
+
"""
|
|
172
|
+
import time
|
|
173
|
+
start_time = time.time()
|
|
174
|
+
temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.2)
|
|
175
|
+
m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 2048)
|
|
176
|
+
|
|
177
|
+
model = self._ensure_model()
|
|
178
|
+
sanitized_msgs = self._sanitize_messages_for_gguf(messages)
|
|
179
|
+
|
|
180
|
+
chunks = model.create_chat_completion(
|
|
181
|
+
messages=sanitized_msgs,
|
|
182
|
+
tools=tools,
|
|
183
|
+
tool_choice="auto" if tools else None,
|
|
184
|
+
temperature=temp,
|
|
185
|
+
max_tokens=m_tokens,
|
|
186
|
+
enable_thinking=enable_thinking,
|
|
187
|
+
stream=True,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
from .openai_backend import _repair_and_parse_json_arguments
|
|
192
|
+
except Exception:
|
|
193
|
+
import json
|
|
194
|
+
def _repair_and_parse_json_arguments(raw_args: str) -> dict:
|
|
195
|
+
try:
|
|
196
|
+
return json.loads(raw_args or "{}")
|
|
197
|
+
except Exception:
|
|
198
|
+
return {}
|
|
199
|
+
|
|
200
|
+
in_think = enable_thinking
|
|
201
|
+
think_started = False
|
|
202
|
+
full_content = []
|
|
203
|
+
full_reasoning = []
|
|
204
|
+
tool_calls_dict = {}
|
|
205
|
+
|
|
206
|
+
for chunk in chunks:
|
|
207
|
+
if not chunk or not chunk.get("choices"):
|
|
208
|
+
continue
|
|
209
|
+
choice = chunk["choices"][0]
|
|
210
|
+
delta = choice.get("delta") or {}
|
|
211
|
+
|
|
212
|
+
explicit_reasoning = delta.get("reasoning") or delta.get("reasoning_content")
|
|
213
|
+
if explicit_reasoning:
|
|
214
|
+
if not think_started:
|
|
215
|
+
think_started = True
|
|
216
|
+
yield ("thinking_start", {"time": start_time})
|
|
217
|
+
full_reasoning.append(explicit_reasoning)
|
|
218
|
+
yield ("thinking_delta", {"delta": explicit_reasoning})
|
|
219
|
+
|
|
220
|
+
if delta.get("tool_calls"):
|
|
221
|
+
for tc in delta["tool_calls"]:
|
|
222
|
+
idx = tc.get("index", 0)
|
|
223
|
+
if idx not in tool_calls_dict:
|
|
224
|
+
fn = tc.get("function") or {}
|
|
225
|
+
tool_calls_dict[idx] = {
|
|
226
|
+
"id": tc.get("id") or f"call_{idx}",
|
|
227
|
+
"name": fn.get("name", ""),
|
|
228
|
+
"arguments": fn.get("arguments", ""),
|
|
229
|
+
}
|
|
230
|
+
else:
|
|
231
|
+
if tc.get("id"):
|
|
232
|
+
tool_calls_dict[idx]["id"] = tc["id"]
|
|
233
|
+
fn = tc.get("function") or {}
|
|
234
|
+
if fn.get("name"):
|
|
235
|
+
tool_calls_dict[idx]["name"] += fn["name"]
|
|
236
|
+
if fn.get("arguments"):
|
|
237
|
+
tool_calls_dict[idx]["arguments"] += fn["arguments"]
|
|
238
|
+
|
|
239
|
+
text = delta.get("content")
|
|
240
|
+
if text:
|
|
241
|
+
if in_think:
|
|
242
|
+
if not think_started:
|
|
243
|
+
think_started = True
|
|
244
|
+
yield ("thinking_start", {"time": start_time})
|
|
245
|
+
|
|
246
|
+
if "</think>" in text:
|
|
247
|
+
before, after = text.split("</think>", 1)
|
|
248
|
+
if before:
|
|
249
|
+
full_reasoning.append(before)
|
|
250
|
+
yield ("thinking_delta", {"delta": before})
|
|
251
|
+
in_think = False
|
|
252
|
+
elapsed = time.time() - start_time
|
|
253
|
+
yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
|
|
254
|
+
if after.strip():
|
|
255
|
+
full_content.append(after)
|
|
256
|
+
yield ("token", {"delta": after})
|
|
257
|
+
elif "DONE:" in text:
|
|
258
|
+
before, after = text.split("DONE:", 1)
|
|
259
|
+
if before:
|
|
260
|
+
full_reasoning.append(before)
|
|
261
|
+
yield ("thinking_delta", {"delta": before})
|
|
262
|
+
in_think = False
|
|
263
|
+
elapsed = time.time() - start_time
|
|
264
|
+
yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
|
|
265
|
+
done_chunk = "DONE:" + after
|
|
266
|
+
full_content.append(done_chunk)
|
|
267
|
+
yield ("token", {"delta": done_chunk})
|
|
268
|
+
else:
|
|
269
|
+
clean_text = text.replace("<think>", "")
|
|
270
|
+
full_reasoning.append(clean_text)
|
|
271
|
+
yield ("thinking_delta", {"delta": clean_text})
|
|
272
|
+
else:
|
|
273
|
+
full_content.append(text)
|
|
274
|
+
yield ("token", {"delta": text})
|
|
275
|
+
|
|
276
|
+
if in_think and think_started:
|
|
277
|
+
elapsed = time.time() - start_time
|
|
278
|
+
yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
|
|
279
|
+
|
|
280
|
+
tool_calls = None
|
|
281
|
+
if tool_calls_dict:
|
|
282
|
+
tool_calls = []
|
|
283
|
+
for idx in sorted(tool_calls_dict.keys()):
|
|
284
|
+
item = tool_calls_dict[idx]
|
|
285
|
+
args = _repair_and_parse_json_arguments(item.get("arguments", ""))
|
|
286
|
+
tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
|
|
287
|
+
|
|
288
|
+
final_content_str = "".join(full_content).strip()
|
|
289
|
+
final_reasoning_str = "".join(full_reasoning).strip()
|
|
290
|
+
|
|
291
|
+
if not final_content_str and final_reasoning_str:
|
|
292
|
+
if "DONE:" in final_reasoning_str:
|
|
293
|
+
idx = final_reasoning_str.find("DONE:")
|
|
294
|
+
final_content_str = final_reasoning_str[idx:].strip()
|
|
295
|
+
final_reasoning_str = final_reasoning_str[:idx].strip()
|
|
296
|
+
yield ("token", {"delta": final_content_str})
|
|
297
|
+
elif "</think>" in final_reasoning_str:
|
|
298
|
+
parts = final_reasoning_str.split("</think>", 1)
|
|
299
|
+
final_reasoning_str = parts[0].strip()
|
|
300
|
+
final_content_str = parts[1].strip()
|
|
301
|
+
yield ("token", {"delta": final_content_str})
|
|
302
|
+
|
|
303
|
+
chat_resp = ChatResponse(
|
|
304
|
+
content=final_content_str,
|
|
305
|
+
reasoning=final_reasoning_str or None,
|
|
306
|
+
tool_calls=tool_calls,
|
|
307
|
+
raw=None,
|
|
308
|
+
)
|
|
309
|
+
yield ("done", chat_resp)
|
|
310
|
+
|
|
311
|
+
def shutdown(self) -> None:
|
|
312
|
+
if self._model:
|
|
313
|
+
self._model._llm = None
|
|
314
|
+
self._model = None
|
|
@@ -30,6 +30,8 @@ DEFAULTS = {
|
|
|
30
30
|
"temperature": 0.1,
|
|
31
31
|
"max_tokens": 8192,
|
|
32
32
|
"enable_thinking": False, # Reasoning OFF by default
|
|
33
|
+
"adaptive_thinking": True, # Adaptive turn-level reasoning dynamics
|
|
34
|
+
"force_thinking": False, # Force reasoning on every single turn
|
|
33
35
|
"n_ctx": 32768, # 32k context window
|
|
34
36
|
"n_gpu_layers": None, # None = auto-detect GPU
|
|
35
37
|
"config_version": CONFIG_VERSION,
|
|
@@ -92,6 +94,8 @@ class Config:
|
|
|
92
94
|
temperature: float = 0.1
|
|
93
95
|
max_tokens: int = 8192
|
|
94
96
|
enable_thinking: bool = False
|
|
97
|
+
adaptive_thinking: bool = True
|
|
98
|
+
force_thinking: bool = False
|
|
95
99
|
n_ctx: int = 32768
|
|
96
100
|
n_gpu_layers: Optional[int] = None
|
|
97
101
|
|
|
@@ -220,6 +220,11 @@ def scan_repository(workdir: Path) -> dict:
|
|
|
220
220
|
Designed to run in under 30ms by checking only key indicator files.
|
|
221
221
|
"""
|
|
222
222
|
workdir = workdir.resolve()
|
|
223
|
+
if not workdir.exists():
|
|
224
|
+
try:
|
|
225
|
+
workdir.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
223
228
|
|
|
224
229
|
has_git = (workdir / ".git").is_dir()
|
|
225
230
|
has_venv = (workdir / ".venv").is_dir() or (workdir / "venv").is_dir()
|
|
@@ -407,7 +412,7 @@ def _has_test_files(workdir: Path) -> bool:
|
|
|
407
412
|
for f in workdir.iterdir():
|
|
408
413
|
if f.is_file() and (f.name.startswith("test_") or f.name.endswith("_test.py")):
|
|
409
414
|
return True
|
|
410
|
-
except PermissionError:
|
|
415
|
+
except (PermissionError, FileNotFoundError):
|
|
411
416
|
pass
|
|
412
417
|
return False
|
|
413
418
|
|
|
@@ -12,7 +12,7 @@ import shutil
|
|
|
12
12
|
import socket
|
|
13
13
|
import subprocess
|
|
14
14
|
from pathlib import Path
|
|
15
|
-
from typing import Dict, List, Tuple
|
|
15
|
+
from typing import Any, Dict, List, Tuple
|
|
16
16
|
from urllib.parse import urlparse
|
|
17
17
|
|
|
18
18
|
from .config import load_config, is_server_reachable, is_internet_available
|
|
@@ -45,24 +45,54 @@ def _check_command(cmd: str) -> Tuple[bool, str]:
|
|
|
45
45
|
return True, "installed"
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def _check_gpu() ->
|
|
49
|
-
"""Check
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
def _check_gpu() -> Dict[str, Any]:
|
|
49
|
+
"""Check both physical GPU hardware and llama-cpp runtime offload support."""
|
|
50
|
+
hardware_gpu = None
|
|
51
|
+
nvidia_smi = (
|
|
52
|
+
shutil.which("nvidia-smi")
|
|
53
|
+
or shutil.which("nvidia-smi.exe")
|
|
54
|
+
or (shutil.which("/usr/lib/wsl/lib/nvidia-smi") if Path("/usr/lib/wsl/lib/nvidia-smi").exists() else None)
|
|
55
|
+
)
|
|
56
|
+
if nvidia_smi:
|
|
57
|
+
try:
|
|
58
|
+
res = subprocess.run(
|
|
59
|
+
[str(nvidia_smi), "--query-gpu=name,memory.total", "--format=csv,noheader"],
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True,
|
|
62
|
+
timeout=3,
|
|
63
|
+
)
|
|
64
|
+
if res.returncode == 0 and res.stdout.strip():
|
|
65
|
+
hardware_gpu = f"NVIDIA GPU: {res.stdout.strip().splitlines()[0]}"
|
|
66
|
+
else:
|
|
67
|
+
hardware_gpu = "NVIDIA GPU driver present"
|
|
68
|
+
except Exception:
|
|
69
|
+
hardware_gpu = "NVIDIA GPU driver present"
|
|
70
|
+
elif sys.platform == "darwin":
|
|
71
|
+
import platform
|
|
72
|
+
if "arm" in platform.processor().lower():
|
|
73
|
+
hardware_gpu = "Apple Silicon (Metal)"
|
|
74
|
+
|
|
75
|
+
runtime_offload = False
|
|
76
|
+
runtime_backend = "CPU only"
|
|
53
77
|
try:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
import llama_cpp
|
|
79
|
+
runtime_offload = bool(getattr(llama_cpp, "llama_supports_gpu_offload", lambda: False)())
|
|
80
|
+
if runtime_offload:
|
|
81
|
+
if hardware_gpu and "NVIDIA" in hardware_gpu:
|
|
82
|
+
runtime_backend = "CUDA"
|
|
83
|
+
elif hardware_gpu and "Apple" in hardware_gpu:
|
|
84
|
+
runtime_backend = "Metal"
|
|
85
|
+
else:
|
|
86
|
+
runtime_backend = "GPU Offload"
|
|
63
87
|
except Exception:
|
|
64
88
|
pass
|
|
65
|
-
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"has_hardware": hardware_gpu is not None,
|
|
92
|
+
"hardware_info": hardware_gpu or "No dedicated GPU detected (CPU mode)",
|
|
93
|
+
"runtime_offload": runtime_offload,
|
|
94
|
+
"runtime_backend": runtime_backend,
|
|
95
|
+
}
|
|
66
96
|
|
|
67
97
|
|
|
68
98
|
def _check_remote_latency(base_url: str) -> Tuple[bool, str]:
|
|
@@ -131,13 +161,23 @@ def run_doctor() -> int:
|
|
|
131
161
|
# 2. CUDA / Hardware Acceleration
|
|
132
162
|
print()
|
|
133
163
|
print(_colorize(" 🖥️ Hardware & GPU Acceleration", "cyan"))
|
|
134
|
-
|
|
135
|
-
if
|
|
136
|
-
print(f" {_colorize('✅', 'green')} {
|
|
164
|
+
gpu_diag = _check_gpu()
|
|
165
|
+
if gpu_diag["has_hardware"]:
|
|
166
|
+
print(f" {_colorize('✅', 'green')} Hardware: {gpu_diag['hardware_info']}")
|
|
137
167
|
passed_checks += 1
|
|
168
|
+
if gpu_diag["runtime_offload"]:
|
|
169
|
+
print(f" {_colorize('✅', 'green')} Python Runtime: llama-cpp-python ({gpu_diag['runtime_backend']} acceleration enabled)")
|
|
170
|
+
passed_checks += 1
|
|
171
|
+
else:
|
|
172
|
+
print(f" {_colorize('⚠️ ', 'yellow')} Python Runtime: llama-cpp-python is running in CPU mode (no CUDA build)")
|
|
173
|
+
print(f" {_colorize('💡', 'cyan')} To enable 5-10x faster GPU acceleration, install the CUDA wheel:")
|
|
174
|
+
print(f" {_colorize('pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu124', 'bold')}")
|
|
138
175
|
else:
|
|
139
|
-
print(f" {_colorize('ℹ️ ', 'blue')} {
|
|
140
|
-
|
|
176
|
+
print(f" {_colorize('ℹ️ ', 'blue')} {gpu_diag['hardware_info']}")
|
|
177
|
+
if gpu_diag["runtime_offload"]:
|
|
178
|
+
print(f" {_colorize('ℹ️ ', 'dim')} Python Runtime: llama-cpp-python GPU offload ready")
|
|
179
|
+
passed_checks += 1
|
|
180
|
+
total_checks += 2
|
|
141
181
|
|
|
142
182
|
# 3. Network & Connectivity
|
|
143
183
|
print()
|
|
@@ -166,26 +206,26 @@ def run_doctor() -> int:
|
|
|
166
206
|
print()
|
|
167
207
|
print(_colorize(" 🧰 Development Tools & Compilers", "cyan"))
|
|
168
208
|
tools_to_check = [
|
|
169
|
-
("
|
|
170
|
-
("
|
|
171
|
-
("
|
|
172
|
-
("
|
|
173
|
-
("
|
|
174
|
-
("go", "Go Programming Language (optional)"),
|
|
175
|
-
("rustc", "Rust Compiler (optional)"),
|
|
176
|
-
("java", "Java JDK (optional)"),
|
|
209
|
+
("git", "Git Version Control", False),
|
|
210
|
+
("gcc", "GNU C Compiler", False),
|
|
211
|
+
("g++", "GNU C++ Compiler", False),
|
|
212
|
+
("node", "Node.js (JavaScript/TypeScript runtime, optional)", True),
|
|
213
|
+
("npm", "Node Package Manager (optional)", True),
|
|
214
|
+
("go", "Go Programming Language (optional)", True),
|
|
215
|
+
("rustc", "Rust Compiler (optional)", True),
|
|
216
|
+
("java", "Java JDK (optional)", True),
|
|
177
217
|
]
|
|
178
|
-
for tool_name, desc in tools_to_check:
|
|
218
|
+
for tool_name, desc, is_optional in tools_to_check:
|
|
179
219
|
ok, msg = _check_command(tool_name)
|
|
180
|
-
is_optional = "optional" in desc
|
|
181
220
|
if ok:
|
|
182
221
|
print(f" {_colorize('✅', 'green')} {tool_name:8s} : {msg}")
|
|
183
222
|
passed_checks += 1
|
|
223
|
+
total_checks += 1
|
|
184
224
|
elif is_optional:
|
|
185
225
|
print(f" {_colorize('ℹ️ ', 'dim')} {tool_name:8s} : not installed (optional)")
|
|
186
226
|
else:
|
|
187
227
|
print(f" {_colorize('⚠️ ', 'yellow')} {tool_name:8s} : not installed")
|
|
188
|
-
|
|
228
|
+
total_checks += 1
|
|
189
229
|
|
|
190
230
|
# 5. VS Code Integration
|
|
191
231
|
print()
|
|
@@ -313,6 +313,7 @@ class LocalModel:
|
|
|
313
313
|
|
|
314
314
|
if self.n_gpu_layers != 0 and not has_gpu_offload:
|
|
315
315
|
print(" ⚠️ Warning: Active Python environment has no CUDA llama-cpp support. Running on CPU.")
|
|
316
|
+
print(" 💡 Tip: Run 'alpiecode doctor' for the one-line command to install the CUDA GPU wheel.")
|
|
316
317
|
accel = "CPU (No CUDA in Python env)"
|
|
317
318
|
self.n_gpu_layers = 0
|
|
318
319
|
else:
|
|
@@ -362,7 +363,9 @@ class LocalModel:
|
|
|
362
363
|
model_path=str(model_path),
|
|
363
364
|
n_ctx=self.n_ctx,
|
|
364
365
|
n_batch=2048 if self.n_gpu_layers == 0 else 4096,
|
|
366
|
+
n_ubatch=512,
|
|
365
367
|
n_threads=n_threads,
|
|
368
|
+
n_threads_batch=max(1, os.cpu_count() or 4),
|
|
366
369
|
n_gpu_layers=self.n_gpu_layers,
|
|
367
370
|
use_mmap=True,
|
|
368
371
|
flash_attn=(self.n_gpu_layers != 0), # Flash Attention only for GPU
|
|
@@ -381,6 +384,14 @@ class LocalModel:
|
|
|
381
384
|
|
|
382
385
|
try:
|
|
383
386
|
self._llm = Llama(**llama_kwargs)
|
|
387
|
+
# In-Memory KV Prompt Prefix Caching for instant Turn 2+ speedup
|
|
388
|
+
try:
|
|
389
|
+
import llama_cpp
|
|
390
|
+
if hasattr(llama_cpp, "LlamaRAMCache") and hasattr(self._llm, "set_cache"):
|
|
391
|
+
cache = llama_cpp.LlamaRAMCache(capacity_bytes=512 * 1024 * 1024)
|
|
392
|
+
self._llm.set_cache(cache)
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
384
395
|
finally:
|
|
385
396
|
# Restore stderr
|
|
386
397
|
if _stderr_fd is not None:
|
|
@@ -425,15 +436,20 @@ class LocalModel:
|
|
|
425
436
|
except Exception:
|
|
426
437
|
pass
|
|
427
438
|
|
|
439
|
+
stream = kwargs.get("stream", False)
|
|
428
440
|
params = {
|
|
429
441
|
"messages": msgs,
|
|
430
442
|
"temperature": 0.1 if not enable_thinking else temperature,
|
|
431
443
|
"max_tokens": max_tokens,
|
|
444
|
+
"stream": stream,
|
|
432
445
|
}
|
|
433
446
|
if tools:
|
|
434
447
|
params["tools"] = tools
|
|
435
448
|
params["tool_choice"] = kwargs.get("tool_choice", "auto")
|
|
436
449
|
|
|
450
|
+
if stream:
|
|
451
|
+
return llm.create_chat_completion(**params)
|
|
452
|
+
|
|
437
453
|
try:
|
|
438
454
|
result_dict = llm.create_chat_completion(**params)
|
|
439
455
|
except ValueError as ve:
|