alpiecode 9.0.4__tar.gz → 10.0.0__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.4 → alpiecode-10.0.0}/PKG-INFO +1 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/pyproject.toml +1 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/SOURCES.txt +3 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/__init__.py +2 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/agent.py +65 -10
- alpiecode-10.0.0/src/codeagent/backends/local_backend.py +314 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/cli.py +16 -2
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/client.py +1 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/config.py +10 -4
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/discovery.py +7 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/doctor.py +72 -32
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/executor.py +18 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/local_model.py +45 -1
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/orchestrator.py +91 -13
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/server.py +11 -6
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/tools.py +6 -2
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/updater.py +8 -0
- alpiecode-10.0.0/test/test_audit_fixes.py +71 -0
- alpiecode-10.0.0/test/test_chat_mode.py +85 -0
- alpiecode-10.0.0/test/test_offline_mode.py +314 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_phase1.py +42 -0
- alpiecode-9.0.4/src/codeagent/backends/local_backend.py +0 -150
- {alpiecode-9.0.4 → alpiecode-10.0.0}/README.md +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/setup.cfg +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode/__init__.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/backends/base.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/cache.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/compaction.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/context.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/git_ops.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/github.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/guardian.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/guardrails.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/media.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/memory.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/progress.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/prompt.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/rephraser.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/session.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_claude_codex_capabilities.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_phase2.py +0 -0
- {alpiecode-9.0.4 → alpiecode-10.0.0}/test/test_phase3.py +0 -0
|
@@ -40,7 +40,10 @@ src/codeagent/backends/base.py
|
|
|
40
40
|
src/codeagent/backends/local_backend.py
|
|
41
41
|
src/codeagent/backends/openai_backend.py
|
|
42
42
|
src/codeagent/extension/alpiecode.vsix
|
|
43
|
+
test/test_audit_fixes.py
|
|
44
|
+
test/test_chat_mode.py
|
|
43
45
|
test/test_claude_codex_capabilities.py
|
|
46
|
+
test/test_offline_mode.py
|
|
44
47
|
test/test_phase1.py
|
|
45
48
|
test/test_phase2.py
|
|
46
49
|
test/test_phase3.py
|
|
@@ -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.0"
|
|
20
21
|
|
|
21
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
22
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -213,11 +213,15 @@ def run_agent(
|
|
|
213
213
|
# CLI-as-Client check: auto-detect if local/remote server is running
|
|
214
214
|
from .client import AlpieCodeClient
|
|
215
215
|
target_server = server_url or getattr(cfg, "server_url", None)
|
|
216
|
-
if not target_server:
|
|
217
|
-
# Check default local server
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
216
|
+
if not target_server and cfg.base_url != "offline" and not getattr(cfg, "offline", False):
|
|
217
|
+
# Check default local server (online mode only)
|
|
218
|
+
try:
|
|
219
|
+
local_client = AlpieCodeClient("http://127.0.0.1:7169")
|
|
220
|
+
h = local_client.health()
|
|
221
|
+
if h.get("status") == "online" and h.get("mode") != "offline":
|
|
222
|
+
target_server = "http://127.0.0.1:7169"
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
221
225
|
|
|
222
226
|
if target_server:
|
|
223
227
|
client = AlpieCodeClient(target_server)
|
|
@@ -234,7 +238,7 @@ def run_agent(
|
|
|
234
238
|
backend = resolve_backend(cfg)
|
|
235
239
|
orchestrator = AgentOrchestrator(backend)
|
|
236
240
|
session_mgr = SessionManager()
|
|
237
|
-
session = session_mgr.create_session(workdir, max_tokens=
|
|
241
|
+
session = session_mgr.create_session(workdir, max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144))
|
|
238
242
|
event_stream = orchestrator.run_task(
|
|
239
243
|
session=session,
|
|
240
244
|
task=task,
|
|
@@ -467,7 +471,7 @@ def run_agent(
|
|
|
467
471
|
return session.context.messages if "session" in locals() else []
|
|
468
472
|
|
|
469
473
|
|
|
470
|
-
def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
474
|
+
def run_chat(workdir: Path, cfg: Config, verbose: bool = True, debug: bool = False) -> None:
|
|
471
475
|
"""Run interactive chat session with Rich presentation."""
|
|
472
476
|
workdir = workdir.resolve()
|
|
473
477
|
_ensure_git(workdir)
|
|
@@ -475,7 +479,7 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
475
479
|
backend = resolve_backend(cfg)
|
|
476
480
|
orchestrator = AgentOrchestrator(backend)
|
|
477
481
|
session_mgr = SessionManager()
|
|
478
|
-
session = session_mgr.create_session(workdir, max_tokens=
|
|
482
|
+
session = session_mgr.create_session(workdir, max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144))
|
|
479
483
|
|
|
480
484
|
is_offline = isinstance(backend, LocalBackend) or not backend.is_available
|
|
481
485
|
active_tools = PromptBuilder().get_tools(is_offline=is_offline)
|
|
@@ -513,7 +517,50 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
513
517
|
console.print("Goodbye! 👋")
|
|
514
518
|
break
|
|
515
519
|
|
|
520
|
+
# In-chat slash commands
|
|
521
|
+
if user_input.lower() in ("/clear", "/reset"):
|
|
522
|
+
session = session_mgr.create_session(
|
|
523
|
+
workdir,
|
|
524
|
+
max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144)
|
|
525
|
+
)
|
|
526
|
+
if HAS_RICH:
|
|
527
|
+
console.print("[bold green]✨ Conversation context reset.[/bold green]")
|
|
528
|
+
else:
|
|
529
|
+
print("✨ Conversation context reset.")
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
if user_input.lower() in ("/info", "/status"):
|
|
533
|
+
mode_str = "OFFLINE (Local GGUF)" if is_offline else "ONLINE (Cloud API)"
|
|
534
|
+
ctx_tokens = backend.context_window if hasattr(backend, 'context_window') and backend.context_window else cfg.n_ctx
|
|
535
|
+
user_turns = len([m for m in session.context.messages if m.get("role") == "user"])
|
|
536
|
+
info_msg = (
|
|
537
|
+
f"[bold]AlpieCode Session Info[/bold]\n"
|
|
538
|
+
f" • Mode: {mode_str}\n"
|
|
539
|
+
f" • Working Dir: {workdir}\n"
|
|
540
|
+
f" • Context Window: {ctx_tokens} tokens\n"
|
|
541
|
+
f" • Session Turns: {user_turns}"
|
|
542
|
+
)
|
|
543
|
+
if HAS_RICH:
|
|
544
|
+
console.print(Panel(info_msg, border_style="cyan", title="ℹ️ Status"))
|
|
545
|
+
else:
|
|
546
|
+
print(info_msg)
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
if user_input.lower() in ("/help", "/?"):
|
|
550
|
+
help_msg = (
|
|
551
|
+
"Available commands:\n"
|
|
552
|
+
" • /clear, /reset - Reset conversation memory\n"
|
|
553
|
+
" • /info, /status - View current model and session diagnostics\n"
|
|
554
|
+
" • exit, quit, q - Exit interactive chat"
|
|
555
|
+
)
|
|
556
|
+
if HAS_RICH:
|
|
557
|
+
console.print(Panel(help_msg, border_style="dim", title="Help"))
|
|
558
|
+
else:
|
|
559
|
+
print(help_msg)
|
|
560
|
+
continue
|
|
561
|
+
|
|
516
562
|
has_streamed_chat = False
|
|
563
|
+
has_printed_chat = False
|
|
517
564
|
for event in orchestrator.run_task(session, user_input, cfg):
|
|
518
565
|
if event.type == "token":
|
|
519
566
|
delta = event.data.get("delta", "")
|
|
@@ -521,6 +568,7 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
521
568
|
sys.stdout.write(delta)
|
|
522
569
|
sys.stdout.flush()
|
|
523
570
|
has_streamed_chat = True
|
|
571
|
+
has_printed_chat = True
|
|
524
572
|
|
|
525
573
|
elif event.type == "turn_start" and debug:
|
|
526
574
|
if HAS_RICH:
|
|
@@ -532,7 +580,12 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
532
580
|
_print_reasoning(event.data["content"])
|
|
533
581
|
|
|
534
582
|
elif event.type == "tool_call" and verbose:
|
|
583
|
+
if has_streamed_chat:
|
|
584
|
+
sys.stdout.write("\n")
|
|
585
|
+
sys.stdout.flush()
|
|
586
|
+
has_streamed_chat = False
|
|
535
587
|
_print_tool_call(event.data["turn"], event.data["name"], event.data["arguments"])
|
|
588
|
+
has_printed_chat = True
|
|
536
589
|
|
|
537
590
|
elif event.type == "tool_result":
|
|
538
591
|
if verbose:
|
|
@@ -543,8 +596,9 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
543
596
|
if has_streamed_chat:
|
|
544
597
|
sys.stdout.write("\n")
|
|
545
598
|
has_streamed_chat = False
|
|
546
|
-
|
|
599
|
+
elif not has_printed_chat:
|
|
547
600
|
_print_assistant_message(event.data["content"])
|
|
601
|
+
has_printed_chat = True
|
|
548
602
|
_checkpoint(workdir, "checkpoint: done")
|
|
549
603
|
|
|
550
604
|
elif event.type == "error":
|
|
@@ -557,6 +611,7 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
557
611
|
if has_streamed_chat:
|
|
558
612
|
sys.stdout.write("\n")
|
|
559
613
|
has_streamed_chat = False
|
|
560
|
-
elif event.data.get("summary"):
|
|
614
|
+
elif not has_printed_chat and event.data.get("summary"):
|
|
561
615
|
_print_assistant_message(event.data["summary"])
|
|
616
|
+
has_printed_chat = True
|
|
562
617
|
break
|
|
@@ -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
|
|
@@ -41,6 +41,10 @@ def _normalize_args():
|
|
|
41
41
|
if not subcommand and len(sys.argv) > 1:
|
|
42
42
|
subcommand = "run"
|
|
43
43
|
sys.argv.insert(1, "run")
|
|
44
|
+
elif subcommand and subcommand not in ("-h", "--help", "-v", "--version") and sys.argv[1] != subcommand:
|
|
45
|
+
# Move subcommand to index 1 so subparsers parse arguments correctly without default override
|
|
46
|
+
sys.argv.remove(subcommand)
|
|
47
|
+
sys.argv.insert(1, subcommand)
|
|
44
48
|
|
|
45
49
|
if subcommand in ("run", "plan", "explain") and len(sys.argv) > 2:
|
|
46
50
|
cmd_idx = sys.argv.index(subcommand)
|
|
@@ -82,6 +86,7 @@ def main():
|
|
|
82
86
|
common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
|
|
83
87
|
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
84
88
|
common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
|
|
89
|
+
common.add_argument("--offline", "--local", dest="offline", action="store_true", help="Run using local offline GGUF model on GPU without internet")
|
|
85
90
|
|
|
86
91
|
from . import __version__
|
|
87
92
|
parser = argparse.ArgumentParser(
|
|
@@ -145,13 +150,16 @@ def main():
|
|
|
145
150
|
|
|
146
151
|
if args.command == "serve":
|
|
147
152
|
_show_banner()
|
|
153
|
+
if getattr(args, "offline", False):
|
|
154
|
+
import os
|
|
155
|
+
os.environ["ALPIECODE_BASE_URL"] = "offline"
|
|
148
156
|
try:
|
|
149
157
|
from .vscode_installer import ensure_vscode_extension
|
|
150
158
|
ensure_vscode_extension()
|
|
151
159
|
except Exception as e:
|
|
152
160
|
pass
|
|
153
161
|
from .server import run_server
|
|
154
|
-
run_server(host=args.host, port=args.port)
|
|
162
|
+
run_server(host=args.host, port=args.port, offline=getattr(args, "offline", False))
|
|
155
163
|
return
|
|
156
164
|
|
|
157
165
|
# Check for auto-updates from GitHub in background
|
|
@@ -164,6 +172,12 @@ def main():
|
|
|
164
172
|
|
|
165
173
|
cfg = load_config()
|
|
166
174
|
|
|
175
|
+
if getattr(args, "offline", False):
|
|
176
|
+
cfg.base_url = "offline"
|
|
177
|
+
cfg.n_ctx = 16384
|
|
178
|
+
import os
|
|
179
|
+
os.environ["ALPIECODE_BASE_URL"] = "offline"
|
|
180
|
+
|
|
167
181
|
if getattr(args, "thinking", False):
|
|
168
182
|
cfg.enable_thinking = True
|
|
169
183
|
elif getattr(args, "no_thinking", False):
|
|
@@ -188,7 +202,7 @@ def main():
|
|
|
188
202
|
cfg.max_turns = args.max_turns
|
|
189
203
|
_show_banner()
|
|
190
204
|
from .agent import run_chat
|
|
191
|
-
run_chat(Path(args.workdir), cfg, verbose=not args.quiet)
|
|
205
|
+
run_chat(Path(args.workdir), cfg, verbose=not args.quiet, debug=getattr(args, 'debug', False))
|
|
192
206
|
|
|
193
207
|
elif args.command == "plan":
|
|
194
208
|
plan_task = (
|
|
@@ -11,7 +11,7 @@ import urllib.parse
|
|
|
11
11
|
import urllib.request
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Dict, Iterator, Optional
|
|
14
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
15
15
|
|
|
16
16
|
from .orchestrator import AgentEvent
|
|
17
17
|
|
|
@@ -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
|
|
|
@@ -151,11 +155,13 @@ def load_config() -> Config:
|
|
|
151
155
|
except ValueError:
|
|
152
156
|
pass
|
|
153
157
|
|
|
154
|
-
# For offline mode,
|
|
155
|
-
#
|
|
158
|
+
# For offline mode, set high-performance 16k context (16384 tokens) for big multi-file projects
|
|
159
|
+
# Fits completely in 6GB VRAM on RTX 3050 with zero paging
|
|
156
160
|
if data.get("base_url") == "offline" and not any(os.environ.get(v) for v in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE")):
|
|
157
|
-
if data.get("n_ctx")
|
|
158
|
-
data["n_ctx"] =
|
|
161
|
+
if data.get("n_ctx") in (32768, 4096):
|
|
162
|
+
data["n_ctx"] = 16384
|
|
163
|
+
if data.get("max_tokens", 8192) > 4096:
|
|
164
|
+
data["max_tokens"] = 4096
|
|
159
165
|
|
|
160
166
|
# Filter data to only keys present in Config fields
|
|
161
167
|
valid_keys = {f.name for f in fields(Config)}
|
|
@@ -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
|
|
|
@@ -559,6 +564,7 @@ def gather_relevant_context(
|
|
|
559
564
|
if getattr(task_context, "intent", "") in ("qa",):
|
|
560
565
|
return None
|
|
561
566
|
|
|
567
|
+
task_lower = task.lower().strip()
|
|
562
568
|
sections: List[str] = []
|
|
563
569
|
chars_used = 0
|
|
564
570
|
|