alpiecode 7.0.7__tar.gz → 8.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alpiecode-7.0.7 → alpiecode-8.0.1}/PKG-INFO +1 -1
- {alpiecode-7.0.7 → alpiecode-8.0.1}/pyproject.toml +1 -1
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/PKG-INFO +1 -1
- alpiecode-8.0.1/src/codeagent/backends/openai_backend.py +319 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/orchestrator.py +107 -8
- alpiecode-7.0.7/src/codeagent/backends/openai_backend.py +0 -152
- {alpiecode-7.0.7 → alpiecode-8.0.1}/README.md +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/setup.cfg +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode/__init__.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/__init__.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/agent.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/backends/base.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/cache.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/cli.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/client.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/compaction.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/config.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/context.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/discovery.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/doctor.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/executor.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/git_ops.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/github.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/guardian.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/local_model.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/media.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/memory.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/progress.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/prompt.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/rephraser.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/server.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/session.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/tools.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/updater.py +0 -0
- {alpiecode-7.0.7 → alpiecode-8.0.1}/src/codeagent/vscode_installer.py +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI-compatible API inference backend with smart model resolution.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
from openai import OpenAI, NotFoundError
|
|
7
|
+
|
|
8
|
+
from ..config import Config, get_shared_http_client, is_server_reachable
|
|
9
|
+
from .base import ChatResponse, ToolCall
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenAIBackend:
|
|
13
|
+
"""Backend for remote OpenAI-compatible servers (vLLM, Ollama, etc.)."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cfg: Config):
|
|
16
|
+
self._cfg = cfg
|
|
17
|
+
self._client: Optional[OpenAI] = None
|
|
18
|
+
self._resolved_model: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
return f"Online API ({self._cfg.base_url})"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_available(self) -> bool:
|
|
26
|
+
return is_server_reachable(self._cfg.base_url)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def context_window(self) -> int:
|
|
30
|
+
return 262_144
|
|
31
|
+
|
|
32
|
+
def _ensure_client(self) -> OpenAI:
|
|
33
|
+
if self._client is None:
|
|
34
|
+
self._client = OpenAI(
|
|
35
|
+
base_url=self._cfg.base_url,
|
|
36
|
+
api_key=self._cfg.api_key or "not-needed",
|
|
37
|
+
http_client=get_shared_http_client(),
|
|
38
|
+
)
|
|
39
|
+
return self._client
|
|
40
|
+
|
|
41
|
+
def _get_available_model(self, client: OpenAI) -> str:
|
|
42
|
+
"""Auto-discover the served model name from /v1/models."""
|
|
43
|
+
if self._resolved_model:
|
|
44
|
+
return self._resolved_model
|
|
45
|
+
try:
|
|
46
|
+
models_list = client.models.list()
|
|
47
|
+
if models_list.data:
|
|
48
|
+
for m in models_list.data:
|
|
49
|
+
if m.id == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
|
|
50
|
+
self._resolved_model = m.id
|
|
51
|
+
return self._resolved_model
|
|
52
|
+
self._resolved_model = models_list.data[0].id
|
|
53
|
+
return self._resolved_model
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
return self._cfg.model
|
|
57
|
+
|
|
58
|
+
def chat_completion_stream(
|
|
59
|
+
self,
|
|
60
|
+
messages: List[dict],
|
|
61
|
+
tools: Optional[List[dict]] = None,
|
|
62
|
+
temperature: Optional[float] = None,
|
|
63
|
+
max_tokens: Optional[int] = None,
|
|
64
|
+
enable_thinking: bool = True,
|
|
65
|
+
):
|
|
66
|
+
"""
|
|
67
|
+
True real-time SSE streaming completion yielding progressive thinking deltas,
|
|
68
|
+
duration, content tokens, and final ChatResponse.
|
|
69
|
+
"""
|
|
70
|
+
import time
|
|
71
|
+
import json
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.1)
|
|
74
|
+
m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 8192)
|
|
75
|
+
|
|
76
|
+
client = self._ensure_client()
|
|
77
|
+
model_name = self._get_available_model(client)
|
|
78
|
+
|
|
79
|
+
params = {
|
|
80
|
+
"model": model_name,
|
|
81
|
+
"messages": messages,
|
|
82
|
+
"temperature": temp,
|
|
83
|
+
"max_tokens": m_tokens,
|
|
84
|
+
"extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
|
|
85
|
+
"stream": True,
|
|
86
|
+
}
|
|
87
|
+
if tools:
|
|
88
|
+
params["tools"] = tools
|
|
89
|
+
params["tool_choice"] = "auto"
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
response = client.chat.completions.create(**params)
|
|
93
|
+
except NotFoundError:
|
|
94
|
+
self._resolved_model = None
|
|
95
|
+
model_name = self._get_available_model(client)
|
|
96
|
+
params["model"] = model_name
|
|
97
|
+
response = client.chat.completions.create(**params)
|
|
98
|
+
|
|
99
|
+
in_think = enable_thinking
|
|
100
|
+
think_started = False
|
|
101
|
+
full_content = []
|
|
102
|
+
full_reasoning = []
|
|
103
|
+
tool_calls_dict = {}
|
|
104
|
+
|
|
105
|
+
for chunk in response:
|
|
106
|
+
if not chunk.choices:
|
|
107
|
+
continue
|
|
108
|
+
delta = chunk.choices[0].delta
|
|
109
|
+
if not delta:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Tool call deltas
|
|
113
|
+
if delta.tool_calls:
|
|
114
|
+
for tc in delta.tool_calls:
|
|
115
|
+
idx = tc.index
|
|
116
|
+
if idx not in tool_calls_dict:
|
|
117
|
+
tool_calls_dict[idx] = {
|
|
118
|
+
"id": tc.id or f"call_{idx}",
|
|
119
|
+
"name": tc.function.name if tc.function and tc.function.name else "",
|
|
120
|
+
"arguments": ""
|
|
121
|
+
}
|
|
122
|
+
else:
|
|
123
|
+
if tc.id:
|
|
124
|
+
tool_calls_dict[idx]["id"] = tc.id
|
|
125
|
+
if tc.function and tc.function.name:
|
|
126
|
+
tool_calls_dict[idx]["name"] += tc.function.name
|
|
127
|
+
if tc.function and tc.function.arguments:
|
|
128
|
+
tool_calls_dict[idx]["arguments"] += tc.function.arguments
|
|
129
|
+
|
|
130
|
+
# Explicit reasoning field from some models
|
|
131
|
+
explicit_reasoning = getattr(delta, "reasoning", None) or getattr(delta, "reasoning_content", None)
|
|
132
|
+
if explicit_reasoning:
|
|
133
|
+
if not think_started:
|
|
134
|
+
think_started = True
|
|
135
|
+
yield ("thinking_start", {"time": start_time})
|
|
136
|
+
full_reasoning.append(explicit_reasoning)
|
|
137
|
+
yield ("thinking_delta", {"delta": explicit_reasoning})
|
|
138
|
+
|
|
139
|
+
# Content deltas (contains <think>...</think> on vLLM/Qwen models)
|
|
140
|
+
if delta.content:
|
|
141
|
+
text = delta.content
|
|
142
|
+
if in_think:
|
|
143
|
+
if not think_started:
|
|
144
|
+
think_started = True
|
|
145
|
+
yield ("thinking_start", {"time": start_time})
|
|
146
|
+
|
|
147
|
+
if "</think>" in text:
|
|
148
|
+
before, after = text.split("</think>", 1)
|
|
149
|
+
if before:
|
|
150
|
+
full_reasoning.append(before)
|
|
151
|
+
yield ("thinking_delta", {"delta": before})
|
|
152
|
+
in_think = False
|
|
153
|
+
elapsed = time.time() - start_time
|
|
154
|
+
yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
|
|
155
|
+
if after.strip():
|
|
156
|
+
full_content.append(after)
|
|
157
|
+
yield ("token", {"delta": after})
|
|
158
|
+
elif "DONE:" in text:
|
|
159
|
+
# Model output DONE: directly inside thinking without </think> tag
|
|
160
|
+
before, after = text.split("DONE:", 1)
|
|
161
|
+
if before:
|
|
162
|
+
full_reasoning.append(before)
|
|
163
|
+
yield ("thinking_delta", {"delta": before})
|
|
164
|
+
in_think = False
|
|
165
|
+
elapsed = time.time() - start_time
|
|
166
|
+
yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
|
|
167
|
+
done_chunk = "DONE:" + after
|
|
168
|
+
full_content.append(done_chunk)
|
|
169
|
+
yield ("token", {"delta": done_chunk})
|
|
170
|
+
else:
|
|
171
|
+
clean_text = text.replace("<think>", "")
|
|
172
|
+
full_reasoning.append(clean_text)
|
|
173
|
+
yield ("thinking_delta", {"delta": clean_text})
|
|
174
|
+
else:
|
|
175
|
+
full_content.append(text)
|
|
176
|
+
yield ("token", {"delta": text})
|
|
177
|
+
|
|
178
|
+
# Close thinking if stream ended while still in think block
|
|
179
|
+
if in_think and think_started:
|
|
180
|
+
elapsed = time.time() - start_time
|
|
181
|
+
yield ("thinking_end", {"duration": round(elapsed, 1), "content": "".join(full_reasoning).strip()})
|
|
182
|
+
|
|
183
|
+
tool_calls = None
|
|
184
|
+
if tool_calls_dict:
|
|
185
|
+
tool_calls = []
|
|
186
|
+
for idx in sorted(tool_calls_dict.keys()):
|
|
187
|
+
item = tool_calls_dict[idx]
|
|
188
|
+
try:
|
|
189
|
+
args = json.loads(item["arguments"])
|
|
190
|
+
except Exception:
|
|
191
|
+
args = {}
|
|
192
|
+
tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
|
|
193
|
+
|
|
194
|
+
final_content_str = "".join(full_content).strip()
|
|
195
|
+
final_reasoning_str = "".join(full_reasoning).strip()
|
|
196
|
+
|
|
197
|
+
# CRITICAL RECOVERY: If content is empty but reasoning has text,
|
|
198
|
+
# extract any DONE: or answer that was mistakenly trapped in reasoning!
|
|
199
|
+
if not final_content_str and final_reasoning_str:
|
|
200
|
+
if "DONE:" in final_reasoning_str:
|
|
201
|
+
idx = final_reasoning_str.find("DONE:")
|
|
202
|
+
final_content_str = final_reasoning_str[idx:].strip()
|
|
203
|
+
final_reasoning_str = final_reasoning_str[:idx].strip()
|
|
204
|
+
yield ("token", {"delta": final_content_str})
|
|
205
|
+
elif "</think>" in final_reasoning_str:
|
|
206
|
+
parts = final_reasoning_str.split("</think>", 1)
|
|
207
|
+
final_reasoning_str = parts[0].strip()
|
|
208
|
+
final_content_str = parts[1].strip()
|
|
209
|
+
yield ("token", {"delta": final_content_str})
|
|
210
|
+
elif any(m in final_reasoning_str for m in ["The codebase is complete", "All JavaScript files", "I have implemented", "Verified the", "All files"]):
|
|
211
|
+
final_content_str = final_reasoning_str
|
|
212
|
+
final_reasoning_str = ""
|
|
213
|
+
yield ("token", {"delta": final_content_str})
|
|
214
|
+
|
|
215
|
+
final_content = final_content_str
|
|
216
|
+
final_reasoning = final_reasoning_str if final_reasoning_str else None
|
|
217
|
+
|
|
218
|
+
yield ("done", ChatResponse(
|
|
219
|
+
content=final_content,
|
|
220
|
+
reasoning=final_reasoning,
|
|
221
|
+
tool_calls=tool_calls,
|
|
222
|
+
raw=None
|
|
223
|
+
))
|
|
224
|
+
|
|
225
|
+
def chat_completion(
|
|
226
|
+
self,
|
|
227
|
+
messages: List[dict],
|
|
228
|
+
tools: Optional[List[dict]] = None,
|
|
229
|
+
temperature: Optional[float] = None,
|
|
230
|
+
max_tokens: Optional[int] = None,
|
|
231
|
+
enable_thinking: bool = True,
|
|
232
|
+
) -> ChatResponse:
|
|
233
|
+
temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.1)
|
|
234
|
+
m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 8192)
|
|
235
|
+
|
|
236
|
+
client = self._ensure_client()
|
|
237
|
+
model_name = self._get_available_model(client)
|
|
238
|
+
|
|
239
|
+
params = {
|
|
240
|
+
"model": model_name,
|
|
241
|
+
"messages": messages,
|
|
242
|
+
"temperature": temp,
|
|
243
|
+
"max_tokens": m_tokens,
|
|
244
|
+
"extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
|
|
245
|
+
}
|
|
246
|
+
if tools:
|
|
247
|
+
params["tools"] = tools
|
|
248
|
+
params["tool_choice"] = "auto"
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
resp = client.chat.completions.create(**params)
|
|
252
|
+
except NotFoundError:
|
|
253
|
+
# Model name mismatch -> re-discover model
|
|
254
|
+
self._resolved_model = None
|
|
255
|
+
model_name = self._get_available_model(client)
|
|
256
|
+
params["model"] = model_name
|
|
257
|
+
resp = client.chat.completions.create(**params)
|
|
258
|
+
|
|
259
|
+
msg = resp.choices[0].message
|
|
260
|
+
|
|
261
|
+
tool_calls = None
|
|
262
|
+
if msg.tool_calls:
|
|
263
|
+
import json
|
|
264
|
+
tool_calls = []
|
|
265
|
+
for tc in msg.tool_calls:
|
|
266
|
+
try:
|
|
267
|
+
args = json.loads(tc.function.arguments or "{}")
|
|
268
|
+
except (json.JSONDecodeError, TypeError):
|
|
269
|
+
args = {}
|
|
270
|
+
if not isinstance(args, dict):
|
|
271
|
+
args = {}
|
|
272
|
+
tool_calls.append(
|
|
273
|
+
ToolCall(
|
|
274
|
+
id=tc.id,
|
|
275
|
+
name=tc.function.name,
|
|
276
|
+
arguments=args,
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
|
|
281
|
+
content = msg.content or ""
|
|
282
|
+
|
|
283
|
+
# Extract embedded thinking/reasoning if not provided in separate fields
|
|
284
|
+
if not reasoning and content:
|
|
285
|
+
import re
|
|
286
|
+
if "</think>" in content:
|
|
287
|
+
# Handled when model starts thinking implicitly or with <think>
|
|
288
|
+
parts = content.split("</think>", 1)
|
|
289
|
+
reasoning = parts[0].replace("<think>", "").strip()
|
|
290
|
+
content = parts[1].strip()
|
|
291
|
+
elif "<think>" in content:
|
|
292
|
+
think_match = re.search(r"<think>(.*?)</think>", content, flags=re.DOTALL)
|
|
293
|
+
if think_match:
|
|
294
|
+
reasoning = think_match.group(1).strip()
|
|
295
|
+
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
|
|
296
|
+
elif content.strip().lower().startswith("thinking process:") or "\nthinking process:" in content.lower():
|
|
297
|
+
if tool_calls:
|
|
298
|
+
reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
|
|
299
|
+
content = ""
|
|
300
|
+
else:
|
|
301
|
+
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)
|
|
302
|
+
if code_match:
|
|
303
|
+
think_part = content[:code_match.start()]
|
|
304
|
+
content_part = content[code_match.start():]
|
|
305
|
+
reasoning = re.sub(r"(?i)^thinking process:\s*", "", think_part).strip()
|
|
306
|
+
content = content_part.strip()
|
|
307
|
+
else:
|
|
308
|
+
reasoning = re.sub(r"(?i)^thinking process:\s*", "", content).strip()
|
|
309
|
+
content = ""
|
|
310
|
+
|
|
311
|
+
return ChatResponse(
|
|
312
|
+
content=content,
|
|
313
|
+
reasoning=reasoning,
|
|
314
|
+
tool_calls=tool_calls,
|
|
315
|
+
raw=resp,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def shutdown(self) -> None:
|
|
319
|
+
self._client = None
|
|
Binary file
|
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
def generate_walkthrough_markdown(task: str, summary: str, files: list, commands: list) -> str:
|
|
2
|
+
"""Generate Antigravity-style Walkthrough markdown document."""
|
|
3
|
+
lines = [
|
|
4
|
+
f"# Walkthrough: {task}",
|
|
5
|
+
"",
|
|
6
|
+
"## Overview",
|
|
7
|
+
summary.strip() if summary else "All requested changes and verification steps have been completed.",
|
|
8
|
+
"",
|
|
9
|
+
"## Changes Made",
|
|
10
|
+
]
|
|
11
|
+
if files:
|
|
12
|
+
lines.append("### Modified / Created Files")
|
|
13
|
+
for f in files:
|
|
14
|
+
lines.append(f"- `{f}`")
|
|
15
|
+
lines.append("")
|
|
16
|
+
else:
|
|
17
|
+
lines.append("No files were modified during this session.")
|
|
18
|
+
lines.append("")
|
|
19
|
+
|
|
20
|
+
if commands:
|
|
21
|
+
lines.append("## Verification Results")
|
|
22
|
+
for c in commands:
|
|
23
|
+
cmd = c.get("command") or c.get("cmd") or ""
|
|
24
|
+
code = c.get("exit_code", 0)
|
|
25
|
+
status = "✓ Passed" if code == 0 else f"✗ Failed (exit code {code})"
|
|
26
|
+
lines.append(f"- `{cmd}` — **{status}**")
|
|
27
|
+
lines.append("")
|
|
28
|
+
|
|
29
|
+
lines.append("## How to Run / Verify")
|
|
30
|
+
lines.append("Review the files above or run your test/build commands to verify project execution.")
|
|
31
|
+
lines.append("")
|
|
32
|
+
return "\n".join(lines)
|
|
33
|
+
|
|
34
|
+
|
|
1
35
|
"""
|
|
2
36
|
Agent orchestrator for AlpieCode.
|
|
3
37
|
|
|
@@ -81,6 +115,10 @@ class AgentOrchestrator:
|
|
|
81
115
|
# Safety ceiling: hard emergency brake (should never be hit naturally)
|
|
82
116
|
safety_ceiling = cfg.max_turns if cfg.max_turns != 200 else 200
|
|
83
117
|
|
|
118
|
+
# Track touched files and executed commands for Antigravity Walkthrough
|
|
119
|
+
session_touched_files = set()
|
|
120
|
+
session_executed_commands = []
|
|
121
|
+
|
|
84
122
|
# ── Response cache check ──
|
|
85
123
|
is_cacheable = not any([image_path, video_path, url, github_repo])
|
|
86
124
|
if is_cacheable:
|
|
@@ -211,13 +249,27 @@ class AgentOrchestrator:
|
|
|
211
249
|
else:
|
|
212
250
|
max_tokens = 2048 if is_offline else effective_max_tokens
|
|
213
251
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
252
|
+
if hasattr(self.backend, "chat_completion_stream") and not is_offline:
|
|
253
|
+
resp = None
|
|
254
|
+
for event_type, data in self.backend.chat_completion_stream(
|
|
255
|
+
messages=session.context.messages,
|
|
256
|
+
tools=active_tools if active_tools else None,
|
|
257
|
+
temperature=cfg.temperature,
|
|
258
|
+
max_tokens=max_tokens,
|
|
259
|
+
enable_thinking=enable_thinking,
|
|
260
|
+
):
|
|
261
|
+
if event_type == "done":
|
|
262
|
+
resp = data
|
|
263
|
+
else:
|
|
264
|
+
yield AgentEvent(event_type, data)
|
|
265
|
+
else:
|
|
266
|
+
resp = self.backend.chat_completion(
|
|
267
|
+
messages=session.context.messages,
|
|
268
|
+
tools=active_tools if active_tools else None,
|
|
269
|
+
temperature=cfg.temperature,
|
|
270
|
+
max_tokens=max_tokens,
|
|
271
|
+
enable_thinking=enable_thinking,
|
|
272
|
+
)
|
|
221
273
|
except Exception as e:
|
|
222
274
|
# Online error -> fallback to local
|
|
223
275
|
if not is_offline and isinstance(self.backend, OpenAIBackend):
|
|
@@ -241,11 +293,25 @@ class AgentOrchestrator:
|
|
|
241
293
|
yield AgentEvent("error", {"error": str(e)})
|
|
242
294
|
return
|
|
243
295
|
|
|
244
|
-
if resp.reasoning:
|
|
296
|
+
if resp.reasoning and not (hasattr(self.backend, "chat_completion_stream") and not is_offline):
|
|
245
297
|
yield AgentEvent("thinking", {"content": resp.reasoning})
|
|
246
298
|
|
|
247
299
|
session.context.add_assistant_response(resp)
|
|
248
300
|
|
|
301
|
+
# Normalize content and reasoning: rescue any DONE: or answer trapped in reasoning
|
|
302
|
+
if (not resp.content or not resp.content.strip()) and resp.reasoning:
|
|
303
|
+
if "DONE:" in resp.reasoning.upper():
|
|
304
|
+
idx = resp.reasoning.upper().find("DONE:")
|
|
305
|
+
resp.content = resp.reasoning[idx:].strip()
|
|
306
|
+
resp.reasoning = resp.reasoning[:idx].strip() or None
|
|
307
|
+
elif "</think>" in resp.reasoning:
|
|
308
|
+
parts = resp.reasoning.split("</think>", 1)
|
|
309
|
+
resp.reasoning = parts[0].strip() or None
|
|
310
|
+
resp.content = parts[1].strip()
|
|
311
|
+
elif any(m in resp.reasoning for m in ["The codebase is complete", "All JavaScript files", "I have implemented", "Verified the"]):
|
|
312
|
+
resp.content = resp.reasoning.strip()
|
|
313
|
+
resp.reasoning = None
|
|
314
|
+
|
|
249
315
|
# ── DONE detection in assistant content ──
|
|
250
316
|
if resp.content and "DONE:" in resp.content.upper():
|
|
251
317
|
# Model said DONE — finish even if there are tool calls
|
|
@@ -266,6 +332,28 @@ class AgentOrchestrator:
|
|
|
266
332
|
yield AgentEvent("message", {"content": resp.content})
|
|
267
333
|
extract_and_save_memories(session.workdir, session.context.messages)
|
|
268
334
|
|
|
335
|
+
# Generate and write real walkthrough.md file to the project workspace
|
|
336
|
+
from pathlib import Path as _Path
|
|
337
|
+
try:
|
|
338
|
+
w_path = _Path(session.workdir) / "walkthrough.md"
|
|
339
|
+
w_content = generate_walkthrough_markdown(
|
|
340
|
+
task=task,
|
|
341
|
+
summary=resp.content,
|
|
342
|
+
files=sorted(list(session_touched_files)),
|
|
343
|
+
commands=session_executed_commands,
|
|
344
|
+
)
|
|
345
|
+
w_path.write_text(w_content, encoding="utf-8")
|
|
346
|
+
session_touched_files.add("walkthrough.md")
|
|
347
|
+
except Exception:
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
yield AgentEvent("walkthrough", {
|
|
351
|
+
"path": "walkthrough.md",
|
|
352
|
+
"summary": resp.content,
|
|
353
|
+
"files": sorted(list(session_touched_files)),
|
|
354
|
+
"commands": session_executed_commands,
|
|
355
|
+
})
|
|
356
|
+
|
|
269
357
|
# Cache if single-turn
|
|
270
358
|
if is_cacheable and turn == 0:
|
|
271
359
|
try:
|
|
@@ -301,6 +389,17 @@ class AgentOrchestrator:
|
|
|
301
389
|
})
|
|
302
390
|
session.context.add_tool_result(res.tool_call_id, res.content)
|
|
303
391
|
|
|
392
|
+
# Track touched files and commands for walkthrough
|
|
393
|
+
for tc in tool_calls:
|
|
394
|
+
if tc.name in ("write_file", "edit_file", "apply_patch"):
|
|
395
|
+
p = tc.arguments.get("path") or tc.arguments.get("filename")
|
|
396
|
+
if p:
|
|
397
|
+
session_touched_files.add(p)
|
|
398
|
+
elif tc.name == "bash":
|
|
399
|
+
c = tc.arguments.get("command", "")
|
|
400
|
+
if c:
|
|
401
|
+
session_executed_commands.append({"command": c, "exit_code": 0})
|
|
402
|
+
|
|
304
403
|
# ── Record progress for stall detection ──
|
|
305
404
|
tc_dicts = [{"name": tc.name, "arguments": tc.arguments} for tc in tool_calls]
|
|
306
405
|
res_dicts = [{"content": res.content, "name": res.name} for res in results]
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
OpenAI-compatible API inference backend with smart model resolution.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Any, List, Optional
|
|
6
|
-
from openai import OpenAI, NotFoundError
|
|
7
|
-
|
|
8
|
-
from ..config import Config, get_shared_http_client, is_server_reachable
|
|
9
|
-
from .base import ChatResponse, ToolCall
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class OpenAIBackend:
|
|
13
|
-
"""Backend for remote OpenAI-compatible servers (vLLM, Ollama, etc.)."""
|
|
14
|
-
|
|
15
|
-
def __init__(self, cfg: Config):
|
|
16
|
-
self._cfg = cfg
|
|
17
|
-
self._client: Optional[OpenAI] = None
|
|
18
|
-
self._resolved_model: Optional[str] = None
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def name(self) -> str:
|
|
22
|
-
return f"Online API ({self._cfg.base_url})"
|
|
23
|
-
|
|
24
|
-
@property
|
|
25
|
-
def is_available(self) -> bool:
|
|
26
|
-
return is_server_reachable(self._cfg.base_url)
|
|
27
|
-
|
|
28
|
-
@property
|
|
29
|
-
def context_window(self) -> int:
|
|
30
|
-
return 262_144
|
|
31
|
-
|
|
32
|
-
def _ensure_client(self) -> OpenAI:
|
|
33
|
-
if self._client is None:
|
|
34
|
-
self._client = OpenAI(
|
|
35
|
-
base_url=self._cfg.base_url,
|
|
36
|
-
api_key=self._cfg.api_key or "not-needed",
|
|
37
|
-
http_client=get_shared_http_client(),
|
|
38
|
-
)
|
|
39
|
-
return self._client
|
|
40
|
-
|
|
41
|
-
def _get_available_model(self, client: OpenAI) -> str:
|
|
42
|
-
"""Auto-discover the served model name from /v1/models."""
|
|
43
|
-
if self._resolved_model:
|
|
44
|
-
return self._resolved_model
|
|
45
|
-
try:
|
|
46
|
-
models_list = client.models.list()
|
|
47
|
-
if models_list.data:
|
|
48
|
-
for m in models_list.data:
|
|
49
|
-
if m.id == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
|
|
50
|
-
self._resolved_model = m.id
|
|
51
|
-
return self._resolved_model
|
|
52
|
-
self._resolved_model = models_list.data[0].id
|
|
53
|
-
return self._resolved_model
|
|
54
|
-
except Exception:
|
|
55
|
-
pass
|
|
56
|
-
return self._cfg.model
|
|
57
|
-
|
|
58
|
-
def chat_completion(
|
|
59
|
-
self,
|
|
60
|
-
messages: List[dict],
|
|
61
|
-
tools: Optional[List[dict]] = None,
|
|
62
|
-
temperature: Optional[float] = None,
|
|
63
|
-
max_tokens: Optional[int] = None,
|
|
64
|
-
enable_thinking: bool = True,
|
|
65
|
-
) -> ChatResponse:
|
|
66
|
-
temp = temperature if temperature is not None else getattr(self._cfg, "temperature", 0.1)
|
|
67
|
-
m_tokens = max_tokens if max_tokens is not None else getattr(self._cfg, "max_tokens", 8192)
|
|
68
|
-
|
|
69
|
-
client = self._ensure_client()
|
|
70
|
-
model_name = self._get_available_model(client)
|
|
71
|
-
|
|
72
|
-
params = {
|
|
73
|
-
"model": model_name,
|
|
74
|
-
"messages": messages,
|
|
75
|
-
"temperature": temp,
|
|
76
|
-
"max_tokens": m_tokens,
|
|
77
|
-
"extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
|
|
78
|
-
}
|
|
79
|
-
if tools:
|
|
80
|
-
params["tools"] = tools
|
|
81
|
-
params["tool_choice"] = "auto"
|
|
82
|
-
|
|
83
|
-
try:
|
|
84
|
-
resp = client.chat.completions.create(**params)
|
|
85
|
-
except NotFoundError:
|
|
86
|
-
# Model name mismatch -> re-discover model
|
|
87
|
-
self._resolved_model = None
|
|
88
|
-
model_name = self._get_available_model(client)
|
|
89
|
-
params["model"] = model_name
|
|
90
|
-
resp = client.chat.completions.create(**params)
|
|
91
|
-
|
|
92
|
-
msg = resp.choices[0].message
|
|
93
|
-
|
|
94
|
-
tool_calls = None
|
|
95
|
-
if msg.tool_calls:
|
|
96
|
-
import json
|
|
97
|
-
tool_calls = []
|
|
98
|
-
for tc in msg.tool_calls:
|
|
99
|
-
try:
|
|
100
|
-
args = json.loads(tc.function.arguments or "{}")
|
|
101
|
-
except (json.JSONDecodeError, TypeError):
|
|
102
|
-
args = {}
|
|
103
|
-
if not isinstance(args, dict):
|
|
104
|
-
args = {}
|
|
105
|
-
tool_calls.append(
|
|
106
|
-
ToolCall(
|
|
107
|
-
id=tc.id,
|
|
108
|
-
name=tc.function.name,
|
|
109
|
-
arguments=args,
|
|
110
|
-
)
|
|
111
|
-
)
|
|
112
|
-
|
|
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 = ""
|
|
143
|
-
|
|
144
|
-
return ChatResponse(
|
|
145
|
-
content=content,
|
|
146
|
-
reasoning=reasoning,
|
|
147
|
-
tool_calls=tool_calls,
|
|
148
|
-
raw=resp,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
def shutdown(self) -> None:
|
|
152
|
-
self._client = None
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|