alpiecode 7.0.1__tar.gz → 7.0.3__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.1 → alpiecode-7.0.3}/PKG-INFO +1 -1
- {alpiecode-7.0.1 → alpiecode-7.0.3}/pyproject.toml +1 -1
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode.egg-info/SOURCES.txt +2 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/__init__.py +1 -1
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/agent.py +13 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/backends/openai_backend.py +30 -1
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/cli.py +21 -1
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/executor.py +90 -18
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/extension/alpiecode.vsix +0 -0
- alpiecode-7.0.3/src/codeagent/git_ops.py +254 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/ipython_ext.py +72 -12
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/orchestrator.py +29 -6
- alpiecode-7.0.3/src/codeagent/rephraser.py +137 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/README.md +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/setup.cfg +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode/__init__.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/backends/base.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/cache.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/client.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/compaction.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/config.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/context.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/discovery.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/doctor.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/github.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/guardian.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/local_model.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/media.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/memory.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/progress.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/prompt.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/server.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/session.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/tools.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/updater.py +0 -0
- {alpiecode-7.0.1 → alpiecode-7.0.3}/src/codeagent/vscode_installer.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:
|
|
@@ -111,9 +111,38 @@ class OpenAIBackend:
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
|
|
114
|
+
content = msg.content or ""
|
|
115
|
+
|
|
116
|
+
# Extract embedded thinking/reasoning if not provided in separate fields
|
|
117
|
+
if not reasoning and content:
|
|
118
|
+
import re
|
|
119
|
+
if "</think>" in content:
|
|
120
|
+
# Handled when model starts thinking implicitly or with <think>
|
|
121
|
+
parts = content.split("</think>", 1)
|
|
122
|
+
reasoning = parts[0].replace("<think>", "").strip()
|
|
123
|
+
content = parts[1].strip()
|
|
124
|
+
elif "<think>" in content:
|
|
125
|
+
think_match = re.search(r"<think>(.*?)</think>", content, flags=re.DOTALL)
|
|
126
|
+
if think_match:
|
|
127
|
+
reasoning = think_match.group(1).strip()
|
|
128
|
+
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
|
|
129
|
+
elif content.strip().lower().startswith("thinking process:") or "\nthinking process:" in content.lower():
|
|
130
|
+
if tool_calls:
|
|
131
|
+
reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
|
|
132
|
+
content = ""
|
|
133
|
+
else:
|
|
134
|
+
code_match = re.search(r"\n(?=```|Here is|Below is|I will|The following|DONE:|#!/usr|/\*|package\s|import\s[a-z]|from\s[a-z])", content, flags=re.IGNORECASE)
|
|
135
|
+
if code_match:
|
|
136
|
+
think_part = content[:code_match.start()]
|
|
137
|
+
content_part = content[code_match.start():]
|
|
138
|
+
reasoning = re.sub(r"(?i)^thinking process:\s*", "", think_part).strip()
|
|
139
|
+
content = content_part.strip()
|
|
140
|
+
else:
|
|
141
|
+
reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
|
|
142
|
+
content = ""
|
|
114
143
|
|
|
115
144
|
return ChatResponse(
|
|
116
|
-
content=
|
|
145
|
+
content=content,
|
|
117
146
|
reasoning=reasoning,
|
|
118
147
|
tool_calls=tool_calls,
|
|
119
148
|
raw=resp,
|
|
@@ -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
|
|
@@ -114,20 +115,28 @@ def _extract_primary_code_block(text: str) -> Optional[str]:
|
|
|
114
115
|
return None
|
|
115
116
|
|
|
116
117
|
|
|
117
|
-
|
|
118
|
-
|
|
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."""
|
|
119
123
|
task_escaped = html.escape(task[:140])
|
|
120
124
|
|
|
125
|
+
badge_label = "Running..." if status == "running" else "Goal-Driven"
|
|
126
|
+
badge_color = "#38bdf8" if status == "running" else "#64748b"
|
|
127
|
+
|
|
121
128
|
actions_html = ""
|
|
122
129
|
if tool_actions:
|
|
123
130
|
actions_html = '<div style="margin-top: 8px; margin-bottom: 8px; font-family: ui-monospace, monospace; font-size: 12px;">'
|
|
124
131
|
for act in tool_actions:
|
|
125
132
|
name = html.escape(act.get("name", ""))
|
|
126
133
|
summary = html.escape(act.get("summary", ""))
|
|
127
|
-
|
|
134
|
+
st = act.get("status", "✓")
|
|
135
|
+
dot_color = "#fbbf24" if st == "running" else "#38bdf8"
|
|
136
|
+
icon = "⏳" if st == "running" else "⏺"
|
|
128
137
|
actions_html += (
|
|
129
138
|
f'<div style="display: flex; align-items: center; gap: 6px; padding: 3px 0; color: #94a3b8;">'
|
|
130
|
-
f'<span style="color:
|
|
139
|
+
f'<span style="color: {dot_color}; font-size: 11px;">{icon}</span> '
|
|
131
140
|
f'<strong style="color: #f1f5f9;">{name}</strong> '
|
|
132
141
|
f'<span style="color: #64748b;">{summary}</span>'
|
|
133
142
|
f'</div>'
|
|
@@ -143,8 +152,8 @@ def _render_colab_card(task: str, tool_actions: List[Dict[str, str]], content: s
|
|
|
143
152
|
f'<span style="font-weight: 600; font-size: 13px; color: #38bdf8; display: flex; align-items: center; gap: 6px;">'
|
|
144
153
|
f'⚡ AlpieCode Agent'
|
|
145
154
|
f'</span>'
|
|
146
|
-
f'<span style="font-size: 11px; color:
|
|
147
|
-
f'
|
|
155
|
+
f'<span style="font-size: 11px; color: {badge_color}; background: #1e293b; padding: 2px 8px; border-radius: 9999px;">'
|
|
156
|
+
f'{badge_label}'
|
|
148
157
|
f'</span>'
|
|
149
158
|
f'</div>'
|
|
150
159
|
f'<div style="font-size: 13px; color: #cbd5e1; margin-top: 8px; font-weight: 500;">{task_escaped}</div>'
|
|
@@ -186,17 +195,37 @@ def alpie_magic(line: str, cell: Optional[str] = None):
|
|
|
186
195
|
run_agent(full_task, workdir, cfg, verbose=True)
|
|
187
196
|
return
|
|
188
197
|
|
|
189
|
-
# 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
|
|
190
200
|
from IPython import get_ipython
|
|
201
|
+
from IPython.display import HTML, Markdown, display, update_display
|
|
191
202
|
ip = get_ipython()
|
|
192
203
|
|
|
193
204
|
backend = resolve_backend(cfg)
|
|
194
205
|
orchestrator = AgentOrchestrator(backend)
|
|
195
206
|
session_mgr = SessionManager()
|
|
196
|
-
|
|
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
|
|
197
219
|
|
|
198
220
|
tool_actions: List[Dict[str, str]] = []
|
|
199
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
|
|
200
229
|
|
|
201
230
|
for event in orchestrator.run_task(session=session, task=full_task, cfg=cfg):
|
|
202
231
|
if event.type == "tool_call":
|
|
@@ -212,10 +241,22 @@ def alpie_magic(line: str, cell: Optional[str] = None):
|
|
|
212
241
|
summary = ""
|
|
213
242
|
if t_name == "bash":
|
|
214
243
|
cmd = t_args.get("command", "")
|
|
215
|
-
summary = f"$ {cmd[:
|
|
244
|
+
summary = f"$ {cmd[:50]}..." if len(cmd) > 50 else f"$ {cmd}"
|
|
216
245
|
elif t_name in ("write_file", "edit_file", "read_file"):
|
|
217
246
|
summary = str(t_args.get("path", ""))
|
|
218
|
-
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
|
|
219
260
|
|
|
220
261
|
elif event.type == "message":
|
|
221
262
|
final_content = event.data.get("content", "")
|
|
@@ -229,8 +270,12 @@ def alpie_magic(line: str, cell: Optional[str] = None):
|
|
|
229
270
|
if cleaned_content.upper().startswith("DONE:"):
|
|
230
271
|
cleaned_content = cleaned_content[5:].strip()
|
|
231
272
|
|
|
232
|
-
#
|
|
233
|
-
|
|
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
|
+
|
|
234
279
|
if cleaned_content:
|
|
235
280
|
_display_markdown(cleaned_content)
|
|
236
281
|
|
|
@@ -299,6 +344,20 @@ def alpie_explain_magic(line: str):
|
|
|
299
344
|
alpie_magic(explain_task)
|
|
300
345
|
|
|
301
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
|
+
|
|
302
361
|
def alpie_doctor_magic(line: str = ""):
|
|
303
362
|
"""%alpie_doctor — Run system health diagnostic checks."""
|
|
304
363
|
from .doctor import run_doctor
|
|
@@ -311,6 +370,7 @@ def load_ipython_extension(ipython):
|
|
|
311
370
|
ipython.register_magic_function(alpie_plan_magic, magic_kind="line_cell", magic_name="alpie_plan")
|
|
312
371
|
ipython.register_magic_function(alpie_explain_magic, magic_kind="line", magic_name="alpie_explain")
|
|
313
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")
|
|
314
374
|
if _is_notebook():
|
|
315
375
|
_display_html(
|
|
316
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,
|
|
@@ -59,7 +61,7 @@ class AgentOrchestrator:
|
|
|
59
61
|
|
|
60
62
|
# ── Phase 0: Discovery — pre-compute task intelligence ──
|
|
61
63
|
task_context = build_task_context(task, session.workdir)
|
|
62
|
-
complexity = task_context.complexity
|
|
64
|
+
complexity = complexity or task_context.complexity
|
|
63
65
|
|
|
64
66
|
yield AgentEvent("discovery", {
|
|
65
67
|
"intent": task_context.intent,
|
|
@@ -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,15 +153,19 @@ 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
|
|
|
140
160
|
|
|
141
161
|
# ── Adaptive thinking ──
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
if cfg.enable_thinking:
|
|
163
|
+
enable_thinking = True
|
|
164
|
+
else:
|
|
165
|
+
enable_thinking = task_context.enable_thinking
|
|
166
|
+
if enable_thinking and complexity in ("qa", "low"):
|
|
167
|
+
enable_thinking = False
|
|
168
|
+
yield AgentEvent("adaptive_mode", {"message": "Simple task detected, skipping deep reasoning."})
|
|
146
169
|
|
|
147
170
|
# ── Goal-driven turn loop (no fixed limit) ──
|
|
148
171
|
progress_monitor = ProgressMonitor()
|
|
@@ -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
|
|
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
|