alpiecode 7.0.0__tar.gz → 7.0.2__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-7.0.0 → alpiecode-7.0.2}/PKG-INFO +1 -1
- {alpiecode-7.0.0 → alpiecode-7.0.2}/pyproject.toml +1 -1
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/SOURCES.txt +2 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/__init__.py +1 -1
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/agent.py +13 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/cli.py +21 -1
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/executor.py +90 -18
- alpiecode-7.0.2/src/codeagent/extension/alpiecode.vsix +0 -0
- alpiecode-7.0.2/src/codeagent/git_ops.py +254 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/ipython_ext.py +76 -14
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/orchestrator.py +21 -1
- alpiecode-7.0.2/src/codeagent/rephraser.py +137 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/vscode_installer.py +12 -1
- alpiecode-7.0.0/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/README.md +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/setup.cfg +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode/__init__.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/base.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/cache.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/client.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/compaction.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/config.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/context.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/discovery.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/doctor.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/github.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/guardian.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/local_model.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/media.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/memory.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/progress.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/prompt.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/server.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/session.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/tools.py +0 -0
- {alpiecode-7.0.0 → alpiecode-7.0.2}/src/codeagent/updater.py +0 -0
|
@@ -18,6 +18,7 @@ src/codeagent/context.py
|
|
|
18
18
|
src/codeagent/discovery.py
|
|
19
19
|
src/codeagent/doctor.py
|
|
20
20
|
src/codeagent/executor.py
|
|
21
|
+
src/codeagent/git_ops.py
|
|
21
22
|
src/codeagent/github.py
|
|
22
23
|
src/codeagent/guardian.py
|
|
23
24
|
src/codeagent/ipython_ext.py
|
|
@@ -27,6 +28,7 @@ src/codeagent/memory.py
|
|
|
27
28
|
src/codeagent/orchestrator.py
|
|
28
29
|
src/codeagent/progress.py
|
|
29
30
|
src/codeagent/prompt.py
|
|
31
|
+
src/codeagent/rephraser.py
|
|
30
32
|
src/codeagent/server.py
|
|
31
33
|
src/codeagent/session.py
|
|
32
34
|
src/codeagent/tools.py
|
|
@@ -303,6 +303,19 @@ def run_agent(
|
|
|
303
303
|
elif event.type == "adaptive_mode" and verbose and HAS_RICH:
|
|
304
304
|
console.print("⚡ [dim]Adaptive mode: simple task detected, skipping deep reasoning for speed[/dim]")
|
|
305
305
|
|
|
306
|
+
elif event.type == "status":
|
|
307
|
+
phase = event.data.get("phase")
|
|
308
|
+
if debug and phase == "building" and event.data.get("rephrased") and event.data.get("rephrased") != task:
|
|
309
|
+
if HAS_RICH:
|
|
310
|
+
console.print(Panel(
|
|
311
|
+
f"[bold cyan]🎯 Solidified Engineering Specification[/bold cyan]\n{event.data.get('rephrased')}",
|
|
312
|
+
title="Internal Rephraser",
|
|
313
|
+
border_style="dim blue",
|
|
314
|
+
padding=(0, 1)
|
|
315
|
+
))
|
|
316
|
+
else:
|
|
317
|
+
print(f"[Rephraser] {event.data.get('rephrased')}")
|
|
318
|
+
|
|
306
319
|
elif event.type == "turn_start":
|
|
307
320
|
current_turn = event.data["turn"]
|
|
308
321
|
if debug:
|
|
@@ -29,7 +29,7 @@ def _show_banner():
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _normalize_args():
|
|
32
|
-
known_commands = {"init", "serve", "run", "chat", "plan", "diff", "doctor", "explain", "-h", "--help", "--version"}
|
|
32
|
+
known_commands = {"init", "serve", "run", "chat", "plan", "diff", "undo", "doctor", "explain", "version", "-h", "--help", "-v", "--version"}
|
|
33
33
|
subcommand = None
|
|
34
34
|
|
|
35
35
|
# Check if a subcommand is present
|
|
@@ -83,12 +83,15 @@ def main():
|
|
|
83
83
|
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
84
84
|
common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
|
|
85
85
|
|
|
86
|
+
from . import __version__
|
|
86
87
|
parser = argparse.ArgumentParser(
|
|
87
88
|
prog="alpiecode",
|
|
88
89
|
description="AlpieCode — Autonomous AI Coding Agent powered by 169Pi Alpie VLM",
|
|
89
90
|
parents=[common],
|
|
90
91
|
)
|
|
92
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
91
93
|
sub = parser.add_subparsers(dest="command")
|
|
94
|
+
sub.add_parser("version", help="Show current version")
|
|
92
95
|
|
|
93
96
|
# ── init ──
|
|
94
97
|
sub.add_parser("init", help="Configure your VLM/OpenAI-compatible endpoint")
|
|
@@ -112,6 +115,9 @@ def main():
|
|
|
112
115
|
# ── diff ──
|
|
113
116
|
diff_p = sub.add_parser("diff", help="Show changes AlpieCode has made since last checkpoint", parents=[common])
|
|
114
117
|
|
|
118
|
+
# ── undo ──
|
|
119
|
+
undo_p = sub.add_parser("undo", help="Revert file changes made by the last AlpieCode session", parents=[common])
|
|
120
|
+
|
|
115
121
|
# ── doctor ──
|
|
116
122
|
sub.add_parser("doctor", help="Run system health checks (CUDA, Python, network, dev tools)")
|
|
117
123
|
|
|
@@ -125,6 +131,10 @@ def main():
|
|
|
125
131
|
parser.print_help()
|
|
126
132
|
return
|
|
127
133
|
|
|
134
|
+
if args.command == "version":
|
|
135
|
+
print(f"alpiecode {__version__}")
|
|
136
|
+
return
|
|
137
|
+
|
|
128
138
|
if args.command == "init":
|
|
129
139
|
interactive_init()
|
|
130
140
|
return
|
|
@@ -199,6 +209,16 @@ def main():
|
|
|
199
209
|
debug=getattr(args, "debug", False),
|
|
200
210
|
)
|
|
201
211
|
|
|
212
|
+
elif args.command == "diff":
|
|
213
|
+
from .git_ops import show_diff
|
|
214
|
+
show_diff(Path(args.workdir).resolve())
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
elif args.command == "undo":
|
|
218
|
+
from .git_ops import undo_last_session
|
|
219
|
+
undo_last_session(Path(args.workdir).resolve())
|
|
220
|
+
return
|
|
221
|
+
|
|
202
222
|
elif args.command == "doctor":
|
|
203
223
|
from .doctor import run_doctor
|
|
204
224
|
sys.exit(run_doctor())
|
|
@@ -116,24 +116,26 @@ class ToolExecutor:
|
|
|
116
116
|
on_tool_start: Optional[Callable[[str, dict], None]] = None,
|
|
117
117
|
on_tool_end: Optional[Callable[[str, str], None]] = None,
|
|
118
118
|
) -> List[ToolResult]:
|
|
119
|
-
"""Execute a list of tool calls
|
|
119
|
+
"""Execute a list of tool calls with staged DAG parallelization:
|
|
120
|
+
- Stage 1: All read-only tools run concurrently in a ThreadPool
|
|
121
|
+
- Stage 2: Mutating tools run sequentially in order
|
|
122
|
+
Results are returned matching the exact original order of tool_calls.
|
|
123
|
+
"""
|
|
120
124
|
if not tool_calls:
|
|
121
125
|
return []
|
|
122
126
|
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
results_map: Dict[str, ToolResult] = {}
|
|
128
|
+
read_calls = [tc for tc in tool_calls if tc.name in READ_ONLY_TOOLS]
|
|
129
|
+
mut_calls = [tc for tc in tool_calls if tc.name not in READ_ONLY_TOOLS]
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
results_map: Dict[str, Tuple[str, float]] = {}
|
|
130
|
-
with ThreadPoolExecutor(max_workers=min(8, len(tool_calls))) as pool:
|
|
131
|
+
# Stage 1: Parallel read-only execution
|
|
132
|
+
if len(read_calls) > 1:
|
|
133
|
+
with ThreadPoolExecutor(max_workers=min(8, len(read_calls))) as pool:
|
|
131
134
|
future_to_tc = {}
|
|
132
|
-
for tc in
|
|
135
|
+
for tc in read_calls:
|
|
133
136
|
if on_tool_start:
|
|
134
137
|
on_tool_start(tc.name, tc.arguments)
|
|
135
138
|
t0 = time.monotonic()
|
|
136
|
-
# Defensive: ensure arguments is always a dict
|
|
137
139
|
safe_args = tc.arguments if isinstance(tc.arguments, dict) else {}
|
|
138
140
|
fn = self.dispatch.get(tc.name)
|
|
139
141
|
if fn:
|
|
@@ -149,14 +151,84 @@ class ToolExecutor:
|
|
|
149
151
|
res_str = str(future.result())
|
|
150
152
|
except Exception as e:
|
|
151
153
|
res_str = f"error: {e}"
|
|
152
|
-
|
|
154
|
+
if on_tool_end:
|
|
155
|
+
on_tool_end(tc.name, res_str)
|
|
156
|
+
results_map[tc.id] = ToolResult(
|
|
157
|
+
tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed
|
|
158
|
+
)
|
|
159
|
+
elif len(read_calls) == 1:
|
|
160
|
+
tc = read_calls[0]
|
|
161
|
+
if on_tool_start:
|
|
162
|
+
on_tool_start(tc.name, tc.arguments)
|
|
163
|
+
t0 = time.monotonic()
|
|
164
|
+
safe_args = tc.arguments if isinstance(tc.arguments, dict) else {}
|
|
165
|
+
fn = self.dispatch.get(tc.name)
|
|
166
|
+
res_str = str(fn(safe_args)) if fn else f"error: Unknown tool '{tc.name}'"
|
|
167
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
168
|
+
if on_tool_end:
|
|
169
|
+
on_tool_end(tc.name, res_str)
|
|
170
|
+
results_map[tc.id] = ToolResult(
|
|
171
|
+
tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Stage 2: Sequential mutating tools (write_file, edit_file, bash)
|
|
175
|
+
for tc in mut_calls:
|
|
176
|
+
if on_tool_start:
|
|
177
|
+
on_tool_start(tc.name, tc.arguments)
|
|
178
|
+
t0 = time.monotonic()
|
|
179
|
+
safe_args = tc.arguments if isinstance(tc.arguments, dict) else {}
|
|
180
|
+
fn = self.dispatch.get(tc.name)
|
|
181
|
+
if fn:
|
|
182
|
+
try:
|
|
183
|
+
res_str = str(fn(safe_args))
|
|
184
|
+
except Exception as e:
|
|
185
|
+
res_str = f"error: {e}"
|
|
186
|
+
else:
|
|
187
|
+
res_str = f"error: Unknown tool '{tc.name}'"
|
|
188
|
+
elapsed = (time.monotonic() - t0) * 1000
|
|
189
|
+
|
|
190
|
+
# Tool loop detection guard
|
|
191
|
+
call_sig = (tc.name, json.dumps(tc.arguments, sort_keys=True))
|
|
192
|
+
self.tool_call_history.append(call_sig)
|
|
193
|
+
repeat_count = sum(1 for item in self.tool_call_history[-5:] if item == call_sig)
|
|
194
|
+
|
|
195
|
+
if repeat_count >= 3:
|
|
196
|
+
res_str += (
|
|
197
|
+
f"\n\n🛑 REPEATED TOOL CALL LOOP DETECTED (attempt #{repeat_count}). "
|
|
198
|
+
f"You have already executed '{tc.name}' with these exact parameters {repeat_count} times in a row. "
|
|
199
|
+
"All checks have passed. Do NOT run this tool again. Output your final summary starting with: DONE: <summary>."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Compilation failure recovery hint
|
|
203
|
+
if tc.name == "bash":
|
|
204
|
+
cmd = tc.arguments.get("command", "") if isinstance(tc.arguments, dict) else ""
|
|
205
|
+
is_compile = any(kw in cmd for kw in ["g++", "gcc", "clang", "make", "cmake", "cargo build", "rustc"])
|
|
206
|
+
if is_compile and "exit_code" in str(res_str):
|
|
207
|
+
try:
|
|
208
|
+
result_data = json.loads(res_str.split("\n", 1)[-1] if res_str.startswith("⚠️") else res_str)
|
|
209
|
+
if result_data.get("exit_code", 0) != 0:
|
|
210
|
+
compile_key = cmd.strip()
|
|
211
|
+
self.compile_fail_counts[compile_key] = self.compile_fail_counts.get(compile_key, 0) + 1
|
|
212
|
+
if self.compile_fail_counts[compile_key] >= 3:
|
|
213
|
+
res_str += (
|
|
214
|
+
"\n\n🛑 REPEATED COMPILATION FAILURE (attempt "
|
|
215
|
+
f"#{self.compile_fail_counts[compile_key]}). "
|
|
216
|
+
"STOP making blind edits. Re-read the ENTIRE source file with "
|
|
217
|
+
"read_file to understand its full structure, then fix ALL errors "
|
|
218
|
+
"comprehensively in one edit."
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
self.compile_fail_counts.pop(cmd.strip(), None)
|
|
222
|
+
except (json.JSONDecodeError, ValueError):
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
if on_tool_end:
|
|
226
|
+
on_tool_end(tc.name, res_str)
|
|
227
|
+
results_map[tc.id] = ToolResult(
|
|
228
|
+
tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed
|
|
229
|
+
)
|
|
153
230
|
|
|
154
|
-
|
|
155
|
-
res_str, elapsed = results_map[tc.id]
|
|
156
|
-
if on_tool_end:
|
|
157
|
-
on_tool_end(tc.name, res_str)
|
|
158
|
-
results.append(ToolResult(tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed))
|
|
159
|
-
return results
|
|
231
|
+
return [results_map[tc.id] for tc in tool_calls if tc.id in results_map]
|
|
160
232
|
|
|
161
233
|
# Sequential execution
|
|
162
234
|
for tc in tool_calls:
|
|
@@ -215,4 +287,4 @@ class ToolExecutor:
|
|
|
215
287
|
|
|
216
288
|
results.append(ToolResult(tool_call_id=tc.id, name=tc.name, content=res_str, duration_ms=elapsed))
|
|
217
289
|
|
|
218
|
-
return results
|
|
290
|
+
return results
|
|
Binary file
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git session tracking, diff inspection, and safe rollback for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Track files created / modified per session
|
|
6
|
+
- `show_diff()`: Render colorized, syntax-highlighted diffs of uncommitted agent changes
|
|
7
|
+
- `undo_last_session()`: Cleanly revert all changes made during the last agent session
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Set, Optional, Dict, Any
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.syntax import Syntax
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
|
|
20
|
+
SESSION_FILE = ".alpiecode_session.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def record_session_start(workdir: Path) -> Dict[str, Any]:
|
|
24
|
+
"""Capture initial git / filesystem state before agent execution begins."""
|
|
25
|
+
state: Dict[str, Any] = {
|
|
26
|
+
"is_git": False,
|
|
27
|
+
"initial_status": {},
|
|
28
|
+
"created_files": [],
|
|
29
|
+
"modified_files": [],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if shutil.which("git") and (workdir / ".git").exists():
|
|
33
|
+
try:
|
|
34
|
+
res = subprocess.run(
|
|
35
|
+
["git", "status", "--porcelain"],
|
|
36
|
+
cwd=workdir, capture_output=True, text=True, timeout=5
|
|
37
|
+
)
|
|
38
|
+
if res.returncode == 0:
|
|
39
|
+
state["is_git"] = True
|
|
40
|
+
status_dict = {}
|
|
41
|
+
for line in res.stdout.splitlines():
|
|
42
|
+
if len(line) >= 4:
|
|
43
|
+
st = line[:2].strip()
|
|
44
|
+
f = line[3:].strip()
|
|
45
|
+
status_dict[f] = st
|
|
46
|
+
state["initial_status"] = status_dict
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
return state
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def record_session_end(workdir: Path, state: Dict[str, Any], touched_files: Set[str]):
|
|
54
|
+
"""Save the session outcome to allow diffing and rolling back."""
|
|
55
|
+
session_data = {
|
|
56
|
+
"is_git": state.get("is_git", False),
|
|
57
|
+
"touched_files": list(touched_files),
|
|
58
|
+
"initial_status": state.get("initial_status", {}),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Determine which files were newly created vs modified
|
|
62
|
+
created = []
|
|
63
|
+
modified = []
|
|
64
|
+
|
|
65
|
+
init_status = state.get("initial_status", {})
|
|
66
|
+
for f in touched_files:
|
|
67
|
+
full_p = workdir / f
|
|
68
|
+
if not full_p.exists():
|
|
69
|
+
continue
|
|
70
|
+
if f not in init_status:
|
|
71
|
+
# Was not tracked or did not exist before
|
|
72
|
+
created.append(f)
|
|
73
|
+
else:
|
|
74
|
+
modified.append(f)
|
|
75
|
+
|
|
76
|
+
session_data["created_files"] = created
|
|
77
|
+
session_data["modified_files"] = modified
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
(workdir / SESSION_FILE).write_text(json.dumps(session_data, indent=2), encoding="utf-8")
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def show_diff(workdir: Path):
|
|
86
|
+
"""Render colorized Rich diff of changes made since last session."""
|
|
87
|
+
console = Console()
|
|
88
|
+
sess_path = workdir / SESSION_FILE
|
|
89
|
+
|
|
90
|
+
# If git repo, run git diff
|
|
91
|
+
if shutil.which("git") and (workdir / ".git").exists():
|
|
92
|
+
try:
|
|
93
|
+
res = subprocess.run(
|
|
94
|
+
["git", "diff"],
|
|
95
|
+
cwd=workdir, capture_output=True, text=True, timeout=8
|
|
96
|
+
)
|
|
97
|
+
diff_text = res.stdout.strip()
|
|
98
|
+
if diff_text:
|
|
99
|
+
console.print(Panel(
|
|
100
|
+
Syntax(diff_text, "diff", theme="monokai", line_numbers=False),
|
|
101
|
+
title="🔍 AlpieCode Session Diff",
|
|
102
|
+
border_style="cyan"
|
|
103
|
+
))
|
|
104
|
+
return
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Fallback to session record
|
|
109
|
+
if sess_path.exists():
|
|
110
|
+
try:
|
|
111
|
+
data = json.loads(sess_path.read_text(encoding="utf-8"))
|
|
112
|
+
created = data.get("created_files", [])
|
|
113
|
+
modified = data.get("modified_files", [])
|
|
114
|
+
|
|
115
|
+
if not created and not modified:
|
|
116
|
+
console.print("[green]✨ Clean workspace — no files modified by the last session.[/green]")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
console.print("[bold cyan]Session File Changes:[/bold cyan]")
|
|
120
|
+
for c in created:
|
|
121
|
+
console.print(f" [green]+ created:[/green] {c}")
|
|
122
|
+
for m in modified:
|
|
123
|
+
console.print(f" [yellow]~ modified:[/yellow] {m}")
|
|
124
|
+
return
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
console.print("[green]✨ Clean workspace — no changes found.[/green]")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def undo_last_session(workdir: Path) -> bool:
|
|
132
|
+
"""Revert changes made during the last agent session."""
|
|
133
|
+
console = Console()
|
|
134
|
+
sess_path = workdir / SESSION_FILE
|
|
135
|
+
|
|
136
|
+
if not sess_path.exists():
|
|
137
|
+
# If in a git repo, check uncommitted changes
|
|
138
|
+
if shutil.which("git") and (workdir / ".git").exists():
|
|
139
|
+
try:
|
|
140
|
+
res = subprocess.run(
|
|
141
|
+
["git", "status", "--porcelain"],
|
|
142
|
+
cwd=workdir, capture_output=True, text=True, timeout=5
|
|
143
|
+
)
|
|
144
|
+
if not res.stdout.strip():
|
|
145
|
+
console.print("[green]✨ Nothing to undo — working directory is already clean.[/green]")
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
console.print("[yellow]Found uncommitted git changes:[/yellow]")
|
|
149
|
+
for line in res.stdout.splitlines()[:10]:
|
|
150
|
+
console.print(f" {line}")
|
|
151
|
+
|
|
152
|
+
choice = input("\nRevert all uncommitted changes? [y/N]: ").strip().lower()
|
|
153
|
+
if choice in ("y", "yes"):
|
|
154
|
+
subprocess.run(["git", "restore", "."], cwd=workdir, check=True)
|
|
155
|
+
subprocess.run(["git", "clean", "-fd"], cwd=workdir, check=True)
|
|
156
|
+
console.print("[green]✅ Successfully reverted all uncommitted changes![/green]")
|
|
157
|
+
return True
|
|
158
|
+
else:
|
|
159
|
+
console.print("[dim]Undo canceled.[/dim]")
|
|
160
|
+
return False
|
|
161
|
+
except Exception as e:
|
|
162
|
+
console.print(f"[red]Error during git undo: {e}[/red]")
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
console.print("[yellow]No recent AlpieCode session found to undo.[/yellow]")
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
data = json.loads(sess_path.read_text(encoding="utf-8"))
|
|
170
|
+
created = data.get("created_files", [])
|
|
171
|
+
modified = data.get("modified_files", [])
|
|
172
|
+
|
|
173
|
+
if not created and not modified:
|
|
174
|
+
console.print("[green]✨ Nothing to undo — no files were touched in the last session.[/green]")
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
console.print("[bold red]Files to revert:[/bold red]")
|
|
178
|
+
for c in created:
|
|
179
|
+
console.print(f" [red]- remove created:[/red] {c}")
|
|
180
|
+
for m in modified:
|
|
181
|
+
console.print(f" [yellow]↺ restore modified:[/yellow] {m}")
|
|
182
|
+
|
|
183
|
+
choice = input("\nRevert these changes? [y/N]: ").strip().lower()
|
|
184
|
+
if choice not in ("y", "yes"):
|
|
185
|
+
console.print("[dim]Undo canceled.[/dim]")
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
# Revert modified files via git if possible
|
|
189
|
+
if data.get("is_git") and shutil.which("git"):
|
|
190
|
+
for m in modified:
|
|
191
|
+
try:
|
|
192
|
+
subprocess.run(["git", "checkout", "--", m], cwd=workdir, check=False)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# Remove created files
|
|
197
|
+
for c in created:
|
|
198
|
+
p = workdir / c
|
|
199
|
+
if p.exists() and p.is_file():
|
|
200
|
+
try:
|
|
201
|
+
p.unlink()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# Clean up session file
|
|
206
|
+
sess_path.unlink(missing_ok=True)
|
|
207
|
+
console.print(f"[green]✅ Successfully reverted {len(created) + len(modified)} files![/green]")
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
console.print(f"[red]Failed to undo session: {e}[/red]")
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def push_to_remote(workdir: Path, username: Optional[str] = None, branch: Optional[str] = None) -> Dict[str, Any]:
|
|
216
|
+
"""Stage, commit, and push changes to git remote origin."""
|
|
217
|
+
if not shutil.which("git") or not (workdir / ".git").exists():
|
|
218
|
+
return {"success": False, "error": "Not a git repository"}
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
# Check remote
|
|
222
|
+
rem_res = subprocess.run(["git", "remote", "get-url", "origin"], cwd=workdir, capture_output=True, text=True, timeout=5)
|
|
223
|
+
if rem_res.returncode != 0:
|
|
224
|
+
return {"success": False, "error": "No remote 'origin' configured"}
|
|
225
|
+
remote_url = rem_res.stdout.strip()
|
|
226
|
+
|
|
227
|
+
# Determine branch
|
|
228
|
+
if not branch:
|
|
229
|
+
br_res = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=workdir, capture_output=True, text=True, timeout=5)
|
|
230
|
+
branch = br_res.stdout.strip() if br_res.returncode == 0 else "main"
|
|
231
|
+
|
|
232
|
+
# Stage and commit any uncommitted changes
|
|
233
|
+
subprocess.run(["git", "add", "-A"], cwd=workdir, capture_output=True, timeout=10)
|
|
234
|
+
commit_cmd = ["git"]
|
|
235
|
+
if username:
|
|
236
|
+
commit_cmd.extend(["-c", f"user.name={username}"])
|
|
237
|
+
commit_cmd.extend(["commit", "-m", "feat: updates by AlpieCode agent", "--allow-empty"])
|
|
238
|
+
subprocess.run(commit_cmd, cwd=workdir, capture_output=True, timeout=10)
|
|
239
|
+
|
|
240
|
+
# Push to origin
|
|
241
|
+
push_res = subprocess.run(["git", "push", "-u", "origin", branch], cwd=workdir, capture_output=True, text=True, timeout=30)
|
|
242
|
+
if push_res.returncode == 0:
|
|
243
|
+
return {
|
|
244
|
+
"success": True,
|
|
245
|
+
"remote": remote_url,
|
|
246
|
+
"branch": branch,
|
|
247
|
+
"username": username or "default",
|
|
248
|
+
"output": push_res.stdout.strip() or push_res.stderr.strip(),
|
|
249
|
+
}
|
|
250
|
+
else:
|
|
251
|
+
return {"success": False, "error": push_res.stderr.strip()}
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
return {"success": False, "error": str(e)}
|
|
@@ -11,6 +11,7 @@ and Google Colab via cell/line magic commands and rich interactive displays:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import html
|
|
14
|
+
import time
|
|
14
15
|
import re
|
|
15
16
|
import sys
|
|
16
17
|
import warnings
|
|
@@ -30,14 +31,16 @@ from .agent import run_agent
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
def _is_notebook() -> bool:
|
|
33
|
-
"""Check if code is running inside a
|
|
34
|
+
"""Check if code is running inside a web browser notebook (Jupyter, Colab, VS Code Notebook)."""
|
|
34
35
|
try:
|
|
35
36
|
from IPython import get_ipython
|
|
36
37
|
shell = get_ipython()
|
|
37
38
|
if shell is None:
|
|
38
39
|
return False
|
|
39
40
|
shell_name = shell.__class__.__name__
|
|
40
|
-
if "
|
|
41
|
+
if "TerminalInteractiveShell" in shell_name:
|
|
42
|
+
return False
|
|
43
|
+
if "ZMQInteractiveShell" in shell_name or hasattr(shell, "kernel") or "google.colab" in sys.modules:
|
|
41
44
|
return True
|
|
42
45
|
return False
|
|
43
46
|
except Exception:
|
|
@@ -112,20 +115,28 @@ def _extract_primary_code_block(text: str) -> Optional[str]:
|
|
|
112
115
|
return None
|
|
113
116
|
|
|
114
117
|
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
# Active session cache for multi-cell conversational memory
|
|
119
|
+
_ACTIVE_NOTEBOOK_SESSION = None
|
|
120
|
+
|
|
121
|
+
def _render_colab_card(task: str, tool_actions: List[Dict[str, str]], status: str = "done") -> str:
|
|
122
|
+
"""Render a modern, sleek Colab/Jupyter responsive card with live pulse indicator."""
|
|
117
123
|
task_escaped = html.escape(task[:140])
|
|
118
124
|
|
|
125
|
+
badge_label = "Running..." if status == "running" else "Goal-Driven"
|
|
126
|
+
badge_color = "#38bdf8" if status == "running" else "#64748b"
|
|
127
|
+
|
|
119
128
|
actions_html = ""
|
|
120
129
|
if tool_actions:
|
|
121
130
|
actions_html = '<div style="margin-top: 8px; margin-bottom: 8px; font-family: ui-monospace, monospace; font-size: 12px;">'
|
|
122
131
|
for act in tool_actions:
|
|
123
132
|
name = html.escape(act.get("name", ""))
|
|
124
133
|
summary = html.escape(act.get("summary", ""))
|
|
125
|
-
|
|
134
|
+
st = act.get("status", "✓")
|
|
135
|
+
dot_color = "#fbbf24" if st == "running" else "#38bdf8"
|
|
136
|
+
icon = "⏳" if st == "running" else "⏺"
|
|
126
137
|
actions_html += (
|
|
127
138
|
f'<div style="display: flex; align-items: center; gap: 6px; padding: 3px 0; color: #94a3b8;">'
|
|
128
|
-
f'<span style="color:
|
|
139
|
+
f'<span style="color: {dot_color}; font-size: 11px;">{icon}</span> '
|
|
129
140
|
f'<strong style="color: #f1f5f9;">{name}</strong> '
|
|
130
141
|
f'<span style="color: #64748b;">{summary}</span>'
|
|
131
142
|
f'</div>'
|
|
@@ -141,8 +152,8 @@ def _render_colab_card(task: str, tool_actions: List[Dict[str, str]], content: s
|
|
|
141
152
|
f'<span style="font-weight: 600; font-size: 13px; color: #38bdf8; display: flex; align-items: center; gap: 6px;">'
|
|
142
153
|
f'⚡ AlpieCode Agent'
|
|
143
154
|
f'</span>'
|
|
144
|
-
f'<span style="font-size: 11px; color:
|
|
145
|
-
f'
|
|
155
|
+
f'<span style="font-size: 11px; color: {badge_color}; background: #1e293b; padding: 2px 8px; border-radius: 9999px;">'
|
|
156
|
+
f'{badge_label}'
|
|
146
157
|
f'</span>'
|
|
147
158
|
f'</div>'
|
|
148
159
|
f'<div style="font-size: 13px; color: #cbd5e1; margin-top: 8px; font-weight: 500;">{task_escaped}</div>'
|
|
@@ -184,17 +195,37 @@ def alpie_magic(line: str, cell: Optional[str] = None):
|
|
|
184
195
|
run_agent(full_task, workdir, cfg, verbose=True)
|
|
185
196
|
return
|
|
186
197
|
|
|
187
|
-
# In Jupyter or Google Colab: run with
|
|
198
|
+
# In Jupyter or Google Colab: run with live in-place streaming & UI
|
|
199
|
+
global _ACTIVE_NOTEBOOK_SESSION
|
|
188
200
|
from IPython import get_ipython
|
|
201
|
+
from IPython.display import HTML, Markdown, display, update_display
|
|
189
202
|
ip = get_ipython()
|
|
190
203
|
|
|
191
204
|
backend = resolve_backend(cfg)
|
|
192
205
|
orchestrator = AgentOrchestrator(backend)
|
|
193
206
|
session_mgr = SessionManager()
|
|
194
|
-
|
|
207
|
+
|
|
208
|
+
# Support --reset or --new flag for cross-cell memory reset
|
|
209
|
+
if raw_task.startswith("--reset") or raw_task.startswith("--new"):
|
|
210
|
+
_ACTIVE_NOTEBOOK_SESSION = None
|
|
211
|
+
raw_task = re.sub(r"^(--reset|--new)\s*", "", raw_task).strip()
|
|
212
|
+
full_task = f"{raw_task}\n{nb_context}" if nb_context else raw_task
|
|
213
|
+
|
|
214
|
+
if _ACTIVE_NOTEBOOK_SESSION is None:
|
|
215
|
+
_ACTIVE_NOTEBOOK_SESSION = session_mgr.create_session(
|
|
216
|
+
workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144
|
|
217
|
+
)
|
|
218
|
+
session = _ACTIVE_NOTEBOOK_SESSION
|
|
195
219
|
|
|
196
220
|
tool_actions: List[Dict[str, str]] = []
|
|
197
221
|
final_content = ""
|
|
222
|
+
display_id = f"alpie_card_{int(time.time() * 1000)}"
|
|
223
|
+
|
|
224
|
+
# Initial in-place widget render
|
|
225
|
+
try:
|
|
226
|
+
display(HTML(_render_colab_card(raw_task, tool_actions, status="running")), display_id=display_id)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
198
229
|
|
|
199
230
|
for event in orchestrator.run_task(session=session, task=full_task, cfg=cfg):
|
|
200
231
|
if event.type == "tool_call":
|
|
@@ -210,10 +241,22 @@ def alpie_magic(line: str, cell: Optional[str] = None):
|
|
|
210
241
|
summary = ""
|
|
211
242
|
if t_name == "bash":
|
|
212
243
|
cmd = t_args.get("command", "")
|
|
213
|
-
summary = f"$ {cmd[:
|
|
244
|
+
summary = f"$ {cmd[:50]}..." if len(cmd) > 50 else f"$ {cmd}"
|
|
214
245
|
elif t_name in ("write_file", "edit_file", "read_file"):
|
|
215
246
|
summary = str(t_args.get("path", ""))
|
|
216
|
-
tool_actions.append({"name": t_name, "summary": summary})
|
|
247
|
+
tool_actions.append({"name": t_name, "summary": summary, "status": "running"})
|
|
248
|
+
try:
|
|
249
|
+
update_display(HTML(_render_colab_card(raw_task, tool_actions, status="running")), display_id=display_id)
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
elif event.type == "tool_result":
|
|
254
|
+
if tool_actions:
|
|
255
|
+
tool_actions[-1]["status"] = "✓"
|
|
256
|
+
try:
|
|
257
|
+
update_display(HTML(_render_colab_card(raw_task, tool_actions, status="running")), display_id=display_id)
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
217
260
|
|
|
218
261
|
elif event.type == "message":
|
|
219
262
|
final_content = event.data.get("content", "")
|
|
@@ -227,8 +270,12 @@ def alpie_magic(line: str, cell: Optional[str] = None):
|
|
|
227
270
|
if cleaned_content.upper().startswith("DONE:"):
|
|
228
271
|
cleaned_content = cleaned_content[5:].strip()
|
|
229
272
|
|
|
230
|
-
#
|
|
231
|
-
|
|
273
|
+
# Final in-place update with done state
|
|
274
|
+
try:
|
|
275
|
+
update_display(HTML(_render_colab_card(raw_task, tool_actions, status="done")), display_id=display_id)
|
|
276
|
+
except Exception:
|
|
277
|
+
_display_html(_render_colab_card(raw_task, tool_actions, status="done"))
|
|
278
|
+
|
|
232
279
|
if cleaned_content:
|
|
233
280
|
_display_markdown(cleaned_content)
|
|
234
281
|
|
|
@@ -297,6 +344,20 @@ def alpie_explain_magic(line: str):
|
|
|
297
344
|
alpie_magic(explain_task)
|
|
298
345
|
|
|
299
346
|
|
|
347
|
+
|
|
348
|
+
def alpie_reset_magic(line: str = ""):
|
|
349
|
+
"""%alpie_reset — Reset conversation memory for the notebook session."""
|
|
350
|
+
global _ACTIVE_NOTEBOOK_SESSION
|
|
351
|
+
_ACTIVE_NOTEBOOK_SESSION = None
|
|
352
|
+
if _is_notebook():
|
|
353
|
+
_display_html(
|
|
354
|
+
'<div style="background: #0f172a; border-left: 3px solid #10b981; border-radius: 6px; padding: 6px 12px; font-family: sans-serif; font-size: 12px; color: #10b981;">'
|
|
355
|
+
'🔄 AlpieCode notebook conversation memory cleared.'
|
|
356
|
+
'</div>'
|
|
357
|
+
)
|
|
358
|
+
else:
|
|
359
|
+
print("🔄 AlpieCode notebook conversation memory cleared.")
|
|
360
|
+
|
|
300
361
|
def alpie_doctor_magic(line: str = ""):
|
|
301
362
|
"""%alpie_doctor — Run system health diagnostic checks."""
|
|
302
363
|
from .doctor import run_doctor
|
|
@@ -309,6 +370,7 @@ def load_ipython_extension(ipython):
|
|
|
309
370
|
ipython.register_magic_function(alpie_plan_magic, magic_kind="line_cell", magic_name="alpie_plan")
|
|
310
371
|
ipython.register_magic_function(alpie_explain_magic, magic_kind="line", magic_name="alpie_explain")
|
|
311
372
|
ipython.register_magic_function(alpie_doctor_magic, magic_kind="line", magic_name="alpie_doctor")
|
|
373
|
+
ipython.register_magic_function(alpie_reset_magic, magic_kind="line", magic_name="alpie_reset")
|
|
312
374
|
if _is_notebook():
|
|
313
375
|
_display_html(
|
|
314
376
|
'<div style="background: #0f172a; border-left: 4px solid #38bdf8; border-radius: 6px; padding: 8px 12px; margin: 4px 0; font-family: sans-serif; font-size: 13px; color: #e2e8f0;">'
|
|
@@ -16,6 +16,7 @@ from .config import Config, is_server_reachable
|
|
|
16
16
|
from .memory import extract_and_save_memories
|
|
17
17
|
from .discovery import build_task_context, COMPLEXITY_CONFIG
|
|
18
18
|
from .progress import ProgressMonitor
|
|
19
|
+
from .rephraser import PromptRephraser
|
|
19
20
|
from .prompt import PromptBuilder, classify_task
|
|
20
21
|
from .session import Session, SessionManager
|
|
21
22
|
|
|
@@ -43,6 +44,7 @@ class AgentOrchestrator:
|
|
|
43
44
|
):
|
|
44
45
|
self.backend = backend
|
|
45
46
|
self.prompt_builder = prompt_builder or PromptBuilder()
|
|
47
|
+
self.rephraser = PromptRephraser()
|
|
46
48
|
|
|
47
49
|
def run_task(
|
|
48
50
|
self,
|
|
@@ -116,9 +118,26 @@ class AgentOrchestrator:
|
|
|
116
118
|
task_context=task_context,
|
|
117
119
|
)
|
|
118
120
|
session.context.set_system_prompt(system_prompt)
|
|
121
|
+
# ── Phase 0.5: Internal Prompt Rephraser (first step, internal only) ──
|
|
122
|
+
yield AgentEvent("status", {
|
|
123
|
+
"phase": "rephrasing",
|
|
124
|
+
"message": "Solidifying task requirements...",
|
|
125
|
+
"task": task,
|
|
126
|
+
})
|
|
127
|
+
rephrased_task = task
|
|
128
|
+
try:
|
|
129
|
+
rephrased_task = self.rephraser.rephrase(task, self.backend, task_context)
|
|
130
|
+
except Exception:
|
|
131
|
+
rephrased_task = task
|
|
132
|
+
|
|
133
|
+
yield AgentEvent("status", {
|
|
134
|
+
"phase": "building",
|
|
135
|
+
"message": "Starting build...",
|
|
136
|
+
"rephrased": rephrased_task,
|
|
137
|
+
})
|
|
119
138
|
|
|
120
139
|
user_content = self.prompt_builder.build_user_content(
|
|
121
|
-
task=
|
|
140
|
+
task=rephrased_task,
|
|
122
141
|
image_path=image_path,
|
|
123
142
|
video_path=video_path,
|
|
124
143
|
url=url,
|
|
@@ -134,6 +153,7 @@ class AgentOrchestrator:
|
|
|
134
153
|
"is_offline": is_offline,
|
|
135
154
|
"tool_count": len(active_tools),
|
|
136
155
|
"complexity": complexity,
|
|
156
|
+
"rephrased_task": rephrased_task,
|
|
137
157
|
})
|
|
138
158
|
|
|
139
159
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt rephraser service for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Internally solidifies raw user prompts into high-grade software engineering
|
|
5
|
+
specifications before agent execution begins. Operates silently behind the scenes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
REPHRASER_SYSTEM_PROMPT = """\
|
|
15
|
+
You are an expert AI prompt engineer and principal software architect.
|
|
16
|
+
Your job is to transform the user's raw prompt into a clear, solid, and unambiguous software engineering specification.
|
|
17
|
+
|
|
18
|
+
Guidelines:
|
|
19
|
+
1. If the prompt is a simple Q&A, greeting, or direct conceptual question (e.g. "what is X?", "explain Y", "hi", "how does Z work"):
|
|
20
|
+
- Keep it clean, direct, and concise. Do NOT add unnecessary software development boilerplate.
|
|
21
|
+
2. If the prompt is a coding, debugging, refactoring, testing, or feature creation task:
|
|
22
|
+
- Expand it into a solid, structured engineering prompt with:
|
|
23
|
+
* Objective: Explicit statement of what needs to be created, modified, or fixed
|
|
24
|
+
* Scope & Architecture: Target files, patterns, and module structure
|
|
25
|
+
* Technical Requirements: Key functionality, edge cases, error handling
|
|
26
|
+
* Quality & Implementation: Complete, production-ready code on Turn 1 with no stubs, placeholders, or missing imports
|
|
27
|
+
* Verification Criteria: Specific commands or tests to run to verify that the implementation works
|
|
28
|
+
3. Output ONLY the refined prompt text. Do NOT include greetings, conversational remarks, thinking blocks, or preambles like "Here is the refined prompt:".
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _clean_rephrased_text(text: str) -> str:
|
|
33
|
+
"""Clean model output to extract only the actual refined specification."""
|
|
34
|
+
if not text:
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
cleaned = text.strip()
|
|
38
|
+
|
|
39
|
+
# 1. Remove <think>...</think> if present
|
|
40
|
+
cleaned = re.sub(r"<think>.*?</think>", "", cleaned, flags=re.DOTALL).strip()
|
|
41
|
+
|
|
42
|
+
# 2. Remove "Thinking Process: ... \n\n" if model output chain-of-thought
|
|
43
|
+
if "thinking process:" in cleaned.lower():
|
|
44
|
+
# Look for double newline or start of markdown header after thinking process
|
|
45
|
+
match = re.search(r"(?:thinking process:.*?\n\n|\bobjective\b|\bscope\b)", cleaned, flags=re.DOTALL | re.IGNORECASE)
|
|
46
|
+
if match:
|
|
47
|
+
# If matched starting with objective/scope, slice from there
|
|
48
|
+
obj_match = re.search(r"(\*\*objective\*\*|objective:|#\s+objective|\*\*goal\*\*)", cleaned, flags=re.IGNORECASE)
|
|
49
|
+
if obj_match:
|
|
50
|
+
cleaned = cleaned[obj_match.start():].strip()
|
|
51
|
+
|
|
52
|
+
# 3. Clean any leading conversational fluff
|
|
53
|
+
fluff_prefixes = [
|
|
54
|
+
"here is the refined prompt:",
|
|
55
|
+
"here is the solidified prompt:",
|
|
56
|
+
"refined prompt:",
|
|
57
|
+
"solidified prompt:",
|
|
58
|
+
"here is a solid prompt:",
|
|
59
|
+
"here is the specification:",
|
|
60
|
+
"refined specification:",
|
|
61
|
+
]
|
|
62
|
+
for prefix in fluff_prefixes:
|
|
63
|
+
if cleaned.lower().startswith(prefix):
|
|
64
|
+
cleaned = cleaned[len(prefix):].strip()
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
# 4. Strip outer code fences if accidentally wrapped
|
|
68
|
+
if cleaned.startswith("```") and cleaned.endswith("```"):
|
|
69
|
+
lines = cleaned.splitlines()
|
|
70
|
+
if len(lines) >= 3:
|
|
71
|
+
cleaned = "\n".join(lines[1:-1]).strip()
|
|
72
|
+
|
|
73
|
+
return cleaned.strip()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PromptRephraser:
|
|
77
|
+
"""Refines and solidifies tasks into rigorous engineering specifications."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, system_prompt: str = REPHRASER_SYSTEM_PROMPT):
|
|
80
|
+
self.system_prompt = system_prompt
|
|
81
|
+
|
|
82
|
+
def rephrase(
|
|
83
|
+
self,
|
|
84
|
+
task: str,
|
|
85
|
+
backend: Any,
|
|
86
|
+
task_context: Optional[Any] = None,
|
|
87
|
+
timeout: float = 6.0,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Solidify user task into a high-grade prompt internally.
|
|
90
|
+
|
|
91
|
+
Fails open: returns original task if rephrasing fails, times out,
|
|
92
|
+
or if backend is unavailable.
|
|
93
|
+
"""
|
|
94
|
+
if not task or not task.strip():
|
|
95
|
+
return task
|
|
96
|
+
|
|
97
|
+
# If pure trivial greeting, skip model call for instant response
|
|
98
|
+
clean_task = task.strip().lower()
|
|
99
|
+
if clean_task in ("hi", "hello", "hey", "help", "ping", "exit", "quit"):
|
|
100
|
+
return task
|
|
101
|
+
|
|
102
|
+
# Check if backend is available
|
|
103
|
+
if backend is None or not getattr(backend, "is_available", True):
|
|
104
|
+
return task
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
messages = [
|
|
108
|
+
{"role": "system", "content": self.system_prompt},
|
|
109
|
+
{"role": "user", "content": f"User Task:\n{task.strip()}"},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
# Invoke backend with enable_thinking=False for ultra-fast, deterministic response
|
|
113
|
+
kwargs = {
|
|
114
|
+
"messages": messages,
|
|
115
|
+
"temperature": 0.0,
|
|
116
|
+
"max_tokens": 1024,
|
|
117
|
+
}
|
|
118
|
+
try:
|
|
119
|
+
response = backend.chat_completion(**kwargs, enable_thinking=False)
|
|
120
|
+
except TypeError:
|
|
121
|
+
response = backend.chat_completion(**kwargs)
|
|
122
|
+
|
|
123
|
+
refined = ""
|
|
124
|
+
if hasattr(response, "content") and response.content:
|
|
125
|
+
refined = response.content.strip()
|
|
126
|
+
elif isinstance(response, dict):
|
|
127
|
+
refined = response.get("content", "").strip()
|
|
128
|
+
|
|
129
|
+
if not refined:
|
|
130
|
+
return task
|
|
131
|
+
|
|
132
|
+
cleaned = _clean_rephrased_text(refined)
|
|
133
|
+
return cleaned if cleaned else task
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.debug(f"Prompt rephrasing skipped due to exception: {e}")
|
|
137
|
+
return task
|
|
@@ -110,7 +110,18 @@ def ensure_vscode_extension(auto_confirm: bool = False, quiet: bool = False) ->
|
|
|
110
110
|
try:
|
|
111
111
|
win_path = subprocess.check_output(["wslpath", "-w", str(vsix_path)], text=True).strip()
|
|
112
112
|
if win_path:
|
|
113
|
-
|
|
113
|
+
# VS Code CLI on Windows disallows UNC paths (\wsl.localhost\...). Copy to Windows temp.
|
|
114
|
+
if win_path.startswith("\\"):
|
|
115
|
+
try:
|
|
116
|
+
win_temp = subprocess.check_output(["cmd.exe", "/c", "echo %TEMP%"], text=True).strip()
|
|
117
|
+
if win_temp and not win_temp.startswith("%"):
|
|
118
|
+
dest = f"{win_temp}\\alpiecode.vsix"
|
|
119
|
+
subprocess.run(["cmd.exe", "/c", f'copy /y "{win_path}" "{dest}"'], capture_output=True)
|
|
120
|
+
target_arg = dest
|
|
121
|
+
except Exception:
|
|
122
|
+
target_arg = win_path
|
|
123
|
+
else:
|
|
124
|
+
target_arg = win_path
|
|
114
125
|
except Exception:
|
|
115
126
|
pass
|
|
116
127
|
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|