alpiecode 10.5.4__tar.gz → 10.5.6__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-10.5.4 → alpiecode-10.5.6}/PKG-INFO +1 -2
- {alpiecode-10.5.4 → alpiecode-10.5.6}/pyproject.toml +1 -2
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/PKG-INFO +1 -2
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/SOURCES.txt +1 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/requires.txt +0 -1
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/assessment.py +27 -6
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/openai_backend.py +51 -4
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/cli.py +29 -2
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/config.py +21 -2
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/discovery.py +13 -3
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/orchestrator.py +35 -4
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/prompt.py +13 -2
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/rephraser.py +19 -4
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/tools.py +96 -44
- alpiecode-10.5.6/test/test_adaptive_resilience.py +264 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/README.md +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/setup.cfg +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/agent.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/cache.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/client.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/context.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/dream_rsi.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/executor.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/github.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/local_model.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/media.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/memory.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/metrics.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/progress.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/server.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/session.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/system1.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/updater.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/verifier.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_cli_ui.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_context_and_trajectory.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_dream_rsi.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_task_assessment.py +0 -0
- {alpiecode-10.5.4 → alpiecode-10.5.6}/test/test_verifier.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 10.5.
|
|
3
|
+
Version: 10.5.6
|
|
4
4
|
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
@@ -8,7 +8,6 @@ Requires-Dist: rich>=13.0
|
|
|
8
8
|
Requires-Dist: click>=8.0
|
|
9
9
|
Requires-Dist: openai>=1.0.0
|
|
10
10
|
Requires-Dist: httpx[http2]>=0.24.0
|
|
11
|
-
Requires-Dist: duckduckgo-search>=7.0.0
|
|
12
11
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
13
12
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
14
13
|
Requires-Dist: imageio-ffmpeg>=0.4.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpiecode"
|
|
7
|
-
version = "10.5.
|
|
7
|
+
version = "10.5.6"
|
|
8
8
|
description = "AlpieCode"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = [
|
|
@@ -13,7 +13,6 @@ dependencies = [
|
|
|
13
13
|
"click>=8.0",
|
|
14
14
|
"openai>=1.0.0",
|
|
15
15
|
"httpx[http2]>=0.24.0",
|
|
16
|
-
"duckduckgo-search>=7.0.0",
|
|
17
16
|
"beautifulsoup4>=4.12.0",
|
|
18
17
|
"yt-dlp>=2024.0.0",
|
|
19
18
|
"imageio-ffmpeg>=0.4.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 10.5.
|
|
3
|
+
Version: 10.5.6
|
|
4
4
|
Summary: AlpieCode
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
@@ -8,7 +8,6 @@ Requires-Dist: rich>=13.0
|
|
|
8
8
|
Requires-Dist: click>=8.0
|
|
9
9
|
Requires-Dist: openai>=1.0.0
|
|
10
10
|
Requires-Dist: httpx[http2]>=0.24.0
|
|
11
|
-
Requires-Dist: duckduckgo-search>=7.0.0
|
|
12
11
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
13
12
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
14
13
|
Requires-Dist: imageio-ffmpeg>=0.4.0
|
|
@@ -45,6 +45,7 @@ src/codeagent/backends/base.py
|
|
|
45
45
|
src/codeagent/backends/local_backend.py
|
|
46
46
|
src/codeagent/backends/openai_backend.py
|
|
47
47
|
src/codeagent/extension/alpiecode.vsix
|
|
48
|
+
test/test_adaptive_resilience.py
|
|
48
49
|
test/test_cli_ui.py
|
|
49
50
|
test/test_context_and_trajectory.py
|
|
50
51
|
test/test_dream_rsi.py
|
|
@@ -17,7 +17,7 @@ Supports:
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any, Dict
|
|
19
19
|
|
|
20
|
-
__version__ = "10.5.
|
|
20
|
+
__version__ = "10.5.6"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -85,6 +85,7 @@ class TaskAssessment:
|
|
|
85
85
|
estimated_tool_calls: int = 1
|
|
86
86
|
strategy: str = ExecutionStrategy.FAST_PATH.value
|
|
87
87
|
rationale: str = ""
|
|
88
|
+
requires_strict_format: bool = False
|
|
88
89
|
|
|
89
90
|
@property
|
|
90
91
|
def legacy_complexity(self) -> str:
|
|
@@ -115,6 +116,7 @@ class TaskAssessment:
|
|
|
115
116
|
"estimated_tool_calls": self.estimated_tool_calls,
|
|
116
117
|
"strategy": self.strategy,
|
|
117
118
|
"rationale": self.rationale,
|
|
119
|
+
"requires_strict_format": self.requires_strict_format,
|
|
118
120
|
"legacy_complexity": self.legacy_complexity,
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -122,12 +124,15 @@ class TaskAssessment:
|
|
|
122
124
|
# ── Pattern Dictionaries for Sub-15ms Deterministic Analysis ───────────────
|
|
123
125
|
|
|
124
126
|
_QA_INDICATORS = [
|
|
125
|
-
r"^\s*(what\s+is|what\s+are|what\'s|what\s+does|what\s+do)\b",
|
|
127
|
+
r"^\s*(what\s+is|what\s+are|what\'s|what\s+does|what\s+do|what\s+can\s+you|what\s+can\s+i|what\s+can\s+we)\b",
|
|
128
|
+
r"^\s*(what\s+are\s+your|what\s+tools|what\s+capabilities|what\s+can)\b",
|
|
129
|
+
r"^\s*(who\s+are\s+you|who\s+made\s+you|who\s+created\s+you)\b",
|
|
126
130
|
r"^\s*(explain|describe|define|compare|contrast)\b",
|
|
127
|
-
r"^\s*(how\s+does|how\s+do|why\s+does|why\s+do|why\s+is)\b",
|
|
128
|
-
r"^\s*(can\s+you\s+explain|could\s+you\s+explain|tell\s+me\s+about)\b",
|
|
131
|
+
r"^\s*(how\s+does|how\s+do|why\s+does|why\s+do|why\s+is|how\s+to|how\s+can\s+i|how\s+can\s+you)\b",
|
|
132
|
+
r"^\s*(can\s+you\s+explain|could\s+you\s+explain|tell\s+me\s+about|can\s+you\s+help|help\s+me\s+with)\b",
|
|
129
133
|
r"^\s*(who\s+invented|when\s+was|difference\s+between)\b",
|
|
130
134
|
r"^\s*(is\s+it\s+possible|is\s+there\s+a\s+way)\b",
|
|
135
|
+
r"^\s*(help|capabilities|hello|hi|hey)\b",
|
|
131
136
|
]
|
|
132
137
|
|
|
133
138
|
_EXPLAIN_INDICATORS = [
|
|
@@ -192,6 +197,15 @@ _EXTERNAL_DEP_INDICATORS = [
|
|
|
192
197
|
r"\b(pydantic|fastapi|flask|django|react|vue|angular|express|torch|pandas|numpy)\b",
|
|
193
198
|
]
|
|
194
199
|
|
|
200
|
+
_STRICT_FORMAT_INDICATORS = [
|
|
201
|
+
r"\b(?:only\s+(?:a\s+)?json|valid\s+json|return\s+(?:only\s+)?json|json\s+object|json\s+only|just\s+json|pure\s+json)\b",
|
|
202
|
+
r"\b(?:strict(?:ly)?\s+json|in\s+json\s+format|raw\s+json|respond\s+with\s+json)\b",
|
|
203
|
+
r"\b(?:no\s+explanation|without\s+(?:any\s+)?explanation|without\s+extra\s+text|no\s+surrounding\s+text|no\s+preamble)\b",
|
|
204
|
+
r"\b(?:single\s+line|one\s+line\s+only|single-line)\b",
|
|
205
|
+
r"\b(?:one\s+word|single\s+word|just\s+the\s+number|regex\s+only)\b",
|
|
206
|
+
r"\b(?:format:\s*json|schema:\s*\{)\b",
|
|
207
|
+
]
|
|
208
|
+
|
|
195
209
|
|
|
196
210
|
def assess_task(
|
|
197
211
|
task: str,
|
|
@@ -215,10 +229,12 @@ def assess_task(
|
|
|
215
229
|
estimated_tool_calls=1,
|
|
216
230
|
strategy=ExecutionStrategy.FAST_PATH.value,
|
|
217
231
|
rationale="Empty task string defaulted to fast local execution.",
|
|
232
|
+
requires_strict_format=False,
|
|
218
233
|
)
|
|
219
234
|
|
|
220
235
|
task_clean = task.strip()
|
|
221
236
|
task_lower = task_clean.lower()
|
|
237
|
+
requires_strict_format = any(re.search(pat, task_lower) for pat in _STRICT_FORMAT_INDICATORS)
|
|
222
238
|
repo = repo_info or {}
|
|
223
239
|
file_count = repo.get("file_count", 0)
|
|
224
240
|
has_tests = repo.get("has_tests", False)
|
|
@@ -228,8 +244,12 @@ def assess_task(
|
|
|
228
244
|
|
|
229
245
|
# ── 1. Intent Classification ──
|
|
230
246
|
intent = TaskIntent.CREATE.value
|
|
231
|
-
|
|
232
|
-
|
|
247
|
+
is_qa_candidate = (
|
|
248
|
+
any(re.search(pat, task_lower) for pat in _QA_INDICATORS)
|
|
249
|
+
or (task_clean.endswith("?") and not any(kw in task_lower for kw in ["write", "create", "implement", "build", "fix", "code", "script", "add", "modify", "refactor"]))
|
|
250
|
+
)
|
|
251
|
+
if is_qa_candidate and not any(
|
|
252
|
+
kw in task_lower for kw in ["write", "create", "implement", "build", "fix", "code", "script", "refactor", "modify"]
|
|
233
253
|
):
|
|
234
254
|
intent = TaskIntent.QA.value
|
|
235
255
|
elif any(re.search(pat, task_lower) for pat in _EXPLAIN_INDICATORS) and not any(
|
|
@@ -240,7 +260,7 @@ def assess_task(
|
|
|
240
260
|
intent = TaskIntent.DEBUG.value
|
|
241
261
|
elif any(re.search(pat, task_lower) for pat in _MODIFY_INDICATORS):
|
|
242
262
|
intent = TaskIntent.MODIFY.value
|
|
243
|
-
elif intent == TaskIntent.CREATE.value and any(re.search(pat, task_lower) for pat in _QA_INDICATORS):
|
|
263
|
+
elif intent == TaskIntent.CREATE.value and (any(re.search(pat, task_lower) for pat in _QA_INDICATORS) or task_clean.endswith("?")):
|
|
244
264
|
# Fallback question check
|
|
245
265
|
intent = TaskIntent.QA.value
|
|
246
266
|
|
|
@@ -397,4 +417,5 @@ def assess_task(
|
|
|
397
417
|
estimated_tool_calls=estimated_tool_calls,
|
|
398
418
|
strategy=strategy,
|
|
399
419
|
rationale=rationale,
|
|
420
|
+
requires_strict_format=requires_strict_format,
|
|
400
421
|
)
|
|
@@ -127,6 +127,7 @@ class OpenAIBackend:
|
|
|
127
127
|
self._cfg = cfg
|
|
128
128
|
self._client: Optional[OpenAI] = None
|
|
129
129
|
self._resolved_model: Optional[str] = None
|
|
130
|
+
self._max_model_len: Optional[int] = None
|
|
130
131
|
|
|
131
132
|
@property
|
|
132
133
|
def name(self) -> str:
|
|
@@ -138,6 +139,8 @@ class OpenAIBackend:
|
|
|
138
139
|
|
|
139
140
|
@property
|
|
140
141
|
def context_window(self) -> int:
|
|
142
|
+
if self._max_model_len:
|
|
143
|
+
return self._max_model_len
|
|
141
144
|
return 262_144
|
|
142
145
|
|
|
143
146
|
def _ensure_client(self) -> OpenAI:
|
|
@@ -173,10 +176,13 @@ class OpenAIBackend:
|
|
|
173
176
|
for m in models_list.data:
|
|
174
177
|
if m.id == self._cfg.model or getattr(m, "root", None) == self._cfg.model:
|
|
175
178
|
self._resolved_model = m.id
|
|
176
|
-
|
|
179
|
+
self._max_model_len = getattr(m, "max_model_len", None)
|
|
180
|
+
logger.info("Resolved model: %s (max_len=%s)", self._resolved_model, self._max_model_len)
|
|
177
181
|
return self._resolved_model
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
first_m = models_list.data[0]
|
|
183
|
+
self._resolved_model = first_m.id
|
|
184
|
+
self._max_model_len = getattr(first_m, "max_model_len", None)
|
|
185
|
+
logger.info("Using first available model: %s (max_len=%s)", self._resolved_model, self._max_model_len)
|
|
180
186
|
return self._resolved_model
|
|
181
187
|
except Exception as e:
|
|
182
188
|
logger.debug("Model discovery failed (%.1fs timeout), using configured model '%s': %s",
|
|
@@ -206,6 +212,26 @@ class OpenAIBackend:
|
|
|
206
212
|
client = self._ensure_client()
|
|
207
213
|
model_name = self._get_available_model(client)
|
|
208
214
|
|
|
215
|
+
if self._max_model_len:
|
|
216
|
+
input_chars = sum(len(str(m.get("content", ""))) for m in messages)
|
|
217
|
+
if tools:
|
|
218
|
+
input_chars += sum(len(str(t)) for t in tools)
|
|
219
|
+
input_est = max(1, input_chars // 3)
|
|
220
|
+
safe_headroom = 1024
|
|
221
|
+
max_allowed = self._max_model_len - input_est - safe_headroom
|
|
222
|
+
if m_tokens > max_allowed:
|
|
223
|
+
m_tokens = max(512, max_allowed)
|
|
224
|
+
if self._max_model_len <= 16384 and m_tokens > 4096:
|
|
225
|
+
m_tokens = 4096
|
|
226
|
+
|
|
227
|
+
import os
|
|
228
|
+
stream_timeout = 180.0 if enable_thinking else 90.0
|
|
229
|
+
if "ALPIECODE_TIMEOUT" in os.environ:
|
|
230
|
+
try:
|
|
231
|
+
stream_timeout = float(os.environ["ALPIECODE_TIMEOUT"])
|
|
232
|
+
except ValueError:
|
|
233
|
+
pass
|
|
234
|
+
|
|
209
235
|
params = {
|
|
210
236
|
"model": model_name,
|
|
211
237
|
"messages": messages,
|
|
@@ -213,7 +239,7 @@ class OpenAIBackend:
|
|
|
213
239
|
"max_tokens": m_tokens,
|
|
214
240
|
"extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
|
|
215
241
|
"stream": True,
|
|
216
|
-
"timeout":
|
|
242
|
+
"timeout": stream_timeout,
|
|
217
243
|
}
|
|
218
244
|
if tools:
|
|
219
245
|
params["tools"] = tools
|
|
@@ -385,12 +411,33 @@ class OpenAIBackend:
|
|
|
385
411
|
client = self._ensure_client()
|
|
386
412
|
model_name = self._get_available_model(client)
|
|
387
413
|
|
|
414
|
+
if self._max_model_len:
|
|
415
|
+
input_chars = sum(len(str(m.get("content", ""))) for m in messages)
|
|
416
|
+
if tools:
|
|
417
|
+
input_chars += sum(len(str(t)) for t in tools)
|
|
418
|
+
input_est = max(1, input_chars // 3)
|
|
419
|
+
safe_headroom = 1024
|
|
420
|
+
max_allowed = self._max_model_len - input_est - safe_headroom
|
|
421
|
+
if m_tokens > max_allowed:
|
|
422
|
+
m_tokens = max(512, max_allowed)
|
|
423
|
+
if self._max_model_len <= 16384 and m_tokens > 4096:
|
|
424
|
+
m_tokens = 4096
|
|
425
|
+
|
|
426
|
+
import os
|
|
427
|
+
call_timeout = 180.0 if enable_thinking else 90.0
|
|
428
|
+
if "ALPIECODE_TIMEOUT" in os.environ:
|
|
429
|
+
try:
|
|
430
|
+
call_timeout = float(os.environ["ALPIECODE_TIMEOUT"])
|
|
431
|
+
except ValueError:
|
|
432
|
+
pass
|
|
433
|
+
|
|
388
434
|
params = {
|
|
389
435
|
"model": model_name,
|
|
390
436
|
"messages": messages,
|
|
391
437
|
"temperature": temp,
|
|
392
438
|
"max_tokens": m_tokens,
|
|
393
439
|
"extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
|
|
440
|
+
"timeout": call_timeout,
|
|
394
441
|
}
|
|
395
442
|
if tools:
|
|
396
443
|
params["tools"] = tools
|
|
@@ -64,7 +64,11 @@ def _normalize_args():
|
|
|
64
64
|
|
|
65
65
|
flags = []
|
|
66
66
|
positionals = []
|
|
67
|
-
value_flags = {
|
|
67
|
+
value_flags = {
|
|
68
|
+
"--workdir", "--image", "--video", "--url", "--github", "--max-turns",
|
|
69
|
+
"--host", "--port", "--ctx-size", "--endpoint", "--base-url", "--model",
|
|
70
|
+
"--api-key", "--preset",
|
|
71
|
+
}
|
|
68
72
|
|
|
69
73
|
i = 0
|
|
70
74
|
while i < len(sub_args):
|
|
@@ -93,8 +97,13 @@ def main():
|
|
|
93
97
|
common.add_argument("--url", default=None, help="YouTube URL for video analysis")
|
|
94
98
|
common.add_argument("--github", default=None, help="GitHub repository (e.g. owner/repo or URL) for open-source analysis")
|
|
95
99
|
common.add_argument("--max-turns", type=int, default=None, help="Override max turns")
|
|
100
|
+
common.add_argument("--endpoint", "--base-url", dest="endpoint", default=None, help="VLM API endpoint base URL")
|
|
101
|
+
common.add_argument("--model", dest="model", default=None, help="Model name (e.g. alpie_9b)")
|
|
102
|
+
common.add_argument("--api-key", dest="api_key", default=None, help="API key for endpoint authentication")
|
|
103
|
+
common.add_argument("--preset", dest="preset", choices=["default", "alpie_9b"], default=None, help="Endpoint preset profile")
|
|
96
104
|
common.add_argument("--thinking", "--reasoning", dest="thinking", action="store_true", help="Enable VLM reasoning mode")
|
|
97
105
|
common.add_argument("--no-thinking", "--non-thinking", dest="no_thinking", action="store_true", help="Disable VLM reasoning mode")
|
|
106
|
+
common.add_argument("--force-thinking", dest="force_thinking", action="store_true", help="Force VLM reasoning on all turns, disabling dynamic gating")
|
|
98
107
|
common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
|
|
99
108
|
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
100
109
|
common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
|
|
@@ -237,6 +246,21 @@ def main():
|
|
|
237
246
|
|
|
238
247
|
cfg = load_config()
|
|
239
248
|
|
|
249
|
+
from .config import KNOWN_PRESETS
|
|
250
|
+
if getattr(args, "preset", None):
|
|
251
|
+
preset_key = args.preset.lower()
|
|
252
|
+
if preset_key in KNOWN_PRESETS:
|
|
253
|
+
cfg.base_url = KNOWN_PRESETS[preset_key]["base_url"]
|
|
254
|
+
cfg.model = KNOWN_PRESETS[preset_key]["model"]
|
|
255
|
+
cfg.api_key = KNOWN_PRESETS[preset_key]["api_key"]
|
|
256
|
+
|
|
257
|
+
if getattr(args, "endpoint", None):
|
|
258
|
+
cfg.base_url = args.endpoint
|
|
259
|
+
if getattr(args, "model", None):
|
|
260
|
+
cfg.model = args.model
|
|
261
|
+
if getattr(args, "api_key", None):
|
|
262
|
+
cfg.api_key = args.api_key
|
|
263
|
+
|
|
240
264
|
if getattr(args, "offline", False):
|
|
241
265
|
cfg.base_url = "offline"
|
|
242
266
|
# 8192 default fits 4.4GB model + KV cache 100% in 6GB VRAM (4.75GB / 6GB) with zero host-paging
|
|
@@ -247,7 +271,10 @@ def main():
|
|
|
247
271
|
if getattr(args, "ctx_size", None):
|
|
248
272
|
cfg.n_ctx = args.ctx_size
|
|
249
273
|
|
|
250
|
-
if getattr(args, "
|
|
274
|
+
if getattr(args, "force_thinking", False):
|
|
275
|
+
cfg.force_thinking = True
|
|
276
|
+
cfg.enable_thinking = True
|
|
277
|
+
elif getattr(args, "thinking", False):
|
|
251
278
|
cfg.enable_thinking = True
|
|
252
279
|
elif getattr(args, "no_thinking", False):
|
|
253
280
|
cfg.enable_thinking = False
|
|
@@ -37,6 +37,19 @@ DEFAULTS = {
|
|
|
37
37
|
"config_version": CONFIG_VERSION,
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
KNOWN_PRESETS = {
|
|
41
|
+
"default": {
|
|
42
|
+
"base_url": "https://test.169pi.ai/v1",
|
|
43
|
+
"model": "alpie_9b",
|
|
44
|
+
"api_key": "not-needed",
|
|
45
|
+
},
|
|
46
|
+
"alpie_9b": {
|
|
47
|
+
"base_url": "https://test.169pi.ai/v1",
|
|
48
|
+
"model": "alpie_9b",
|
|
49
|
+
"api_key": "not-needed",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
|
|
41
54
|
def is_server_reachable(base_url: Optional[str], timeout: float = 0.4) -> bool:
|
|
42
55
|
"""Fast network ping check (0.4s max) to see if server endpoint is online."""
|
|
@@ -77,7 +90,7 @@ def get_shared_http_client():
|
|
|
77
90
|
import httpx
|
|
78
91
|
_SHARED_HTTP_CLIENT = httpx.Client(
|
|
79
92
|
http2=True,
|
|
80
|
-
timeout=httpx.Timeout(
|
|
93
|
+
timeout=httpx.Timeout(180.0, connect=10.0, read=180.0, write=30.0),
|
|
81
94
|
limits=httpx.Limits(max_keepalive_connections=20, max_connections=50),
|
|
82
95
|
)
|
|
83
96
|
return _SHARED_HTTP_CLIENT
|
|
@@ -127,6 +140,12 @@ def load_config() -> Config:
|
|
|
127
140
|
|
|
128
141
|
# env vars override saved file
|
|
129
142
|
for prefix in ("ALPIECODE_", "CODEAGENT_"):
|
|
143
|
+
preset_val = os.environ.get(f"{prefix}PRESET")
|
|
144
|
+
if preset_val and preset_val.lower() in KNOWN_PRESETS:
|
|
145
|
+
p_data = KNOWN_PRESETS[preset_val.lower()]
|
|
146
|
+
data["base_url"] = p_data["base_url"]
|
|
147
|
+
data["model"] = p_data["model"]
|
|
148
|
+
data["api_key"] = p_data["api_key"]
|
|
130
149
|
if os.environ.get(f"{prefix}BASE_URL"):
|
|
131
150
|
data["base_url"] = os.environ[f"{prefix}BASE_URL"]
|
|
132
151
|
if os.environ.get(f"{prefix}MODEL"):
|
|
@@ -137,7 +156,7 @@ def load_config() -> Config:
|
|
|
137
156
|
data["api_key"] = os.environ[f"{prefix}API_KEY"]
|
|
138
157
|
if os.environ.get("HF_TOKEN"):
|
|
139
158
|
data["hf_token"] = os.environ["HF_TOKEN"]
|
|
140
|
-
|
|
159
|
+
|
|
141
160
|
# Context size overrides from environment
|
|
142
161
|
for ctx_var in ("ALPIECODE_N_CTX", "ALPIECODE_CTX_SIZE", "ALPIECODE_CONTEXT_SIZE", "CODEAGENT_N_CTX"):
|
|
143
162
|
if os.environ.get(ctx_var):
|
|
@@ -59,6 +59,7 @@ class TaskContext:
|
|
|
59
59
|
max_tokens: int = 8192
|
|
60
60
|
tool_set: str = "core" # none, core, full
|
|
61
61
|
enable_thinking: bool = False
|
|
62
|
+
requires_strict_format: bool = False
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
# -- Intent Detection ------------------------------------------------------
|
|
@@ -66,11 +67,15 @@ class TaskContext:
|
|
|
66
67
|
# Question patterns -- user wants an answer, not code
|
|
67
68
|
_QA_STARTERS = [
|
|
68
69
|
"what is", "what are", "what\'s", "what does", "what do",
|
|
70
|
+
"what can you", "what can i", "what can we", "what are your", "what tools", "what capabilities",
|
|
71
|
+
"who are you", "who made you", "who created you",
|
|
69
72
|
"explain", "how does", "how do", "why does", "why do", "why is",
|
|
73
|
+
"how to", "how can i", "how can you",
|
|
70
74
|
"describe", "define", "compare", "difference between",
|
|
71
|
-
"tell me about", "who invented", "
|
|
75
|
+
"tell me about", "who invented", "when was",
|
|
72
76
|
"can you explain", "could you explain", "please explain",
|
|
73
|
-
"what\'s the difference", "is it possible", "is there a way",
|
|
77
|
+
"can you help", "help me with", "what\'s the difference", "is it possible", "is there a way",
|
|
78
|
+
"help", "capabilities", "hello", "hi", "hey",
|
|
74
79
|
]
|
|
75
80
|
|
|
76
81
|
# Action verbs that indicate code generation
|
|
@@ -663,6 +668,10 @@ def build_task_context(task: str, workdir: Path) -> TaskContext:
|
|
|
663
668
|
|
|
664
669
|
# Phase 5: Budget from complexity
|
|
665
670
|
budget = COMPLEXITY_CONFIG.get(complexity, COMPLEXITY_CONFIG["low"])
|
|
671
|
+
enable_thinking = budget["enable_thinking"]
|
|
672
|
+
requires_strict_format = getattr(assessment, "requires_strict_format", False)
|
|
673
|
+
if requires_strict_format:
|
|
674
|
+
enable_thinking = False
|
|
666
675
|
|
|
667
676
|
symbol_index = {}
|
|
668
677
|
if repo.get("file_count", 0) > 0 and repo.get("project_type") != "empty":
|
|
@@ -696,5 +705,6 @@ def build_task_context(task: str, workdir: Path) -> TaskContext:
|
|
|
696
705
|
# Budget
|
|
697
706
|
max_tokens=budget["max_tokens"],
|
|
698
707
|
tool_set=budget["tool_set"],
|
|
699
|
-
enable_thinking=
|
|
708
|
+
enable_thinking=enable_thinking,
|
|
709
|
+
requires_strict_format=requires_strict_format,
|
|
700
710
|
)
|
|
@@ -325,7 +325,17 @@ class AgentOrchestrator:
|
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
# ── Adaptive thinking (base setting) ──
|
|
328
|
-
|
|
328
|
+
strict_format = getattr(getattr(task_context, "assessment", None), "requires_strict_format", False) or getattr(task_context, "requires_strict_format", False)
|
|
329
|
+
if getattr(cfg, "force_thinking", False):
|
|
330
|
+
base_enable_thinking = True
|
|
331
|
+
elif strict_format:
|
|
332
|
+
base_enable_thinking = False
|
|
333
|
+
yield AgentEvent("adaptive_mode", {
|
|
334
|
+
"message": "Strict formatting requested (e.g. JSON/single-line); dynamic reasoning gating turned off thinking for fast execution.",
|
|
335
|
+
"thinking": False,
|
|
336
|
+
"strict_format_gated": True,
|
|
337
|
+
})
|
|
338
|
+
elif cfg.enable_thinking:
|
|
329
339
|
base_enable_thinking = True
|
|
330
340
|
elif complexity in ("qa", "low", "no-thinking") or strategy == "fast_path":
|
|
331
341
|
base_enable_thinking = False
|
|
@@ -391,6 +401,17 @@ class AgentOrchestrator:
|
|
|
391
401
|
# ── Turn-Level Adaptive Reasoning Dynamics ──
|
|
392
402
|
if getattr(cfg, "force_thinking", False) or not getattr(cfg, "adaptive_thinking", True):
|
|
393
403
|
turn_enable_thinking = base_enable_thinking
|
|
404
|
+
elif strict_format:
|
|
405
|
+
if prior_turn_had_error:
|
|
406
|
+
turn_enable_thinking = True
|
|
407
|
+
yield AgentEvent("adaptive_mode", {
|
|
408
|
+
"turn": turn,
|
|
409
|
+
"thinking": True,
|
|
410
|
+
"escalated": True,
|
|
411
|
+
"message": "Prior turn encountered error; escalating deep reasoning trace.",
|
|
412
|
+
})
|
|
413
|
+
else:
|
|
414
|
+
turn_enable_thinking = False
|
|
394
415
|
elif turn == 1:
|
|
395
416
|
turn_enable_thinking = base_enable_thinking
|
|
396
417
|
else:
|
|
@@ -488,12 +509,22 @@ class AgentOrchestrator:
|
|
|
488
509
|
if not is_offline and isinstance(task_backend, OpenAIBackend):
|
|
489
510
|
err_str = str(e).lower()
|
|
490
511
|
is_timeout = "timeout" in err_str or "timed out" in err_str or "read operation timed out" in err_str
|
|
512
|
+
is_stream_err = (
|
|
513
|
+
"streaming" in err_str
|
|
514
|
+
or "connection" in err_str
|
|
515
|
+
or "disconnect" in err_str
|
|
516
|
+
or "remoteprotocolerror" in err_str
|
|
517
|
+
or "protocol" in err_str
|
|
518
|
+
or "peer closed" in err_str
|
|
519
|
+
or "broken pipe" in err_str
|
|
520
|
+
)
|
|
491
521
|
is_network_down = not is_internet_available(timeout=1.5)
|
|
492
522
|
|
|
493
|
-
if is_timeout and not is_network_down:
|
|
494
|
-
# Server is alive but
|
|
523
|
+
if (is_timeout or is_stream_err) and not is_network_down:
|
|
524
|
+
# Server is alive but encountered a stream drop or timeout — retry with resilient connection
|
|
525
|
+
warn_msg = f"Server connection issue ({e}). Retrying with resilient connection..." if is_stream_err else f"Server inference timed out: {e}. Retrying..."
|
|
495
526
|
yield AgentEvent("warning", {
|
|
496
|
-
"message":
|
|
527
|
+
"message": warn_msg,
|
|
497
528
|
})
|
|
498
529
|
try:
|
|
499
530
|
resp = task_backend.chat_completion(
|
|
@@ -100,6 +100,9 @@ SYSTEM_PROMPT = """\
|
|
|
100
100
|
You are AlpieCode, an autonomous software-engineering agent built by 169Pi. \
|
|
101
101
|
You write complete, working code in a single pass and verify it by running it.
|
|
102
102
|
|
|
103
|
+
## Technical Focus Scope
|
|
104
|
+
This session is strictly focused on software engineering and technical implementation. For purely technical tasks, bypass all non-applicable geopolitical, territorial, and sovereign policy deliberation in internal reasoning traces. Focus internal reasoning exclusively on software architecture, code correctness, execution efficiency, and explicit format constraints.
|
|
105
|
+
|
|
103
106
|
# CRITICAL RULE: WRITE CODE FIRST
|
|
104
107
|
|
|
105
108
|
# CRITICAL: FAST TESTING & EXECUTION (NO REAL SLEEP)
|
|
@@ -176,6 +179,9 @@ You are AlpieCode, an autonomous software-engineering agent built by 169Pi. \
|
|
|
176
179
|
You operate with the judgement of a staff engineer. You read and edit real \
|
|
177
180
|
codebases, implement features, fix bugs, write tests, and run builds.
|
|
178
181
|
|
|
182
|
+
## Technical Focus Scope
|
|
183
|
+
This session is strictly focused on software engineering and technical architecture. For purely technical tasks, bypass all non-applicable geopolitical, territorial, and sovereign policy deliberation in internal reasoning traces. Focus internal reasoning exclusively on software design, algorithmic correctness, execution speed, and explicit format constraints.
|
|
184
|
+
|
|
179
185
|
# Workflow
|
|
180
186
|
1. **UNDERSTAND**: Read relevant files to understand the codebase
|
|
181
187
|
2. **PLAN**: For complex multi-file projects, plan the architecture mentally
|
|
@@ -254,6 +260,9 @@ OFFLINE_SYSTEM_PROMPT = """\
|
|
|
254
260
|
You are AlpieCode, an autonomous software engineering AI agent built by 169Pi.
|
|
255
261
|
You are running in OFFLINE mode — there is NO internet access.
|
|
256
262
|
|
|
263
|
+
## Technical Focus Scope
|
|
264
|
+
This session is strictly focused on software engineering. For purely technical tasks, bypass all non-applicable geopolitical, territorial, and sovereign policy deliberation in internal reasoning traces. Focus purely on technical implementation, algorithmic correctness, and execution.
|
|
265
|
+
|
|
257
266
|
# CRITICAL RULE: WRITE CODE FIRST
|
|
258
267
|
|
|
259
268
|
# CRITICAL: FAST TESTING & EXECUTION (NO REAL SLEEP)
|
|
@@ -303,16 +312,18 @@ def classify_task(task: str) -> str:
|
|
|
303
312
|
|
|
304
313
|
# ── QA: questions that need no code ──
|
|
305
314
|
qa_patterns = [
|
|
306
|
-
"what is", "what are", "
|
|
315
|
+
"what is", "what are", "what can you", "what can i", "what do you", "what does",
|
|
316
|
+
"who are you", "who made you", "explain", "how does", "why does", "why is",
|
|
307
317
|
"describe", "define", "compare", "difference between",
|
|
308
318
|
"tell me about", "what's the", "who invented", "when was",
|
|
319
|
+
"can you help", "how to", "help", "capabilities", "hello", "hi", "hey",
|
|
309
320
|
]
|
|
310
321
|
# Must start with or contain a question pattern AND not contain action words
|
|
311
322
|
action_words = ["build", "create", "write", "make", "implement", "fix",
|
|
312
323
|
"add", "modify", "change", "update", "delete", "remove",
|
|
313
324
|
"generate", "develop", "code", "script", "program"]
|
|
314
325
|
|
|
315
|
-
if any(task_lower.startswith(pat) for pat in qa_patterns):
|
|
326
|
+
if any(task_lower.startswith(pat) for pat in qa_patterns) or (task_lower.endswith("?") and not any(aw in task_lower for aw in action_words)):
|
|
316
327
|
if not any(aw in task_lower for aw in action_words):
|
|
317
328
|
return "qa"
|
|
318
329
|
|
|
@@ -99,7 +99,19 @@ class PromptRephraser:
|
|
|
99
99
|
|
|
100
100
|
# If pure trivial greeting, skip model call for instant response
|
|
101
101
|
clean_task = task.strip().lower()
|
|
102
|
-
if clean_task in ("hi", "hello", "hey", "help", "ping", "exit", "quit"):
|
|
102
|
+
if clean_task in ("hi", "hello", "hey", "help", "ping", "exit", "quit", "status", "info"):
|
|
103
|
+
return task
|
|
104
|
+
|
|
105
|
+
# If classified as QA or explain, never rephrase into a software engineering specification
|
|
106
|
+
if task_context is not None:
|
|
107
|
+
ctx_intent = getattr(task_context, "intent", None)
|
|
108
|
+
ctx_comp = getattr(task_context, "complexity", None)
|
|
109
|
+
if ctx_intent in ("qa", "explain") or ctx_comp == "qa":
|
|
110
|
+
return task
|
|
111
|
+
|
|
112
|
+
# General conversational / question check: do not fabricate software projects for direct questions
|
|
113
|
+
action_kws = ["write", "create", "implement", "build", "fix", "code", "script", "refactor", "modify", "add", "delete"]
|
|
114
|
+
if clean_task.endswith("?") and not any(kw in clean_task for kw in action_kws):
|
|
103
115
|
return task
|
|
104
116
|
|
|
105
117
|
# Check if backend is available
|
|
@@ -107,15 +119,18 @@ class PromptRephraser:
|
|
|
107
119
|
return task
|
|
108
120
|
|
|
109
121
|
# In offline/local GGUF mode, skip redundant rephraser LLM pass to ensure instant turn-0 start
|
|
110
|
-
if type(backend).__name__ == "LocalBackend" or getattr(backend, "is_offline",
|
|
122
|
+
if type(backend).__name__ == "LocalBackend" or getattr(backend, "is_offline", None) is True:
|
|
111
123
|
return task
|
|
112
124
|
|
|
113
125
|
try:
|
|
114
|
-
# Inject project context if
|
|
126
|
+
# Inject project context only if modifying/debugging or if the task explicitly references the repo/files
|
|
115
127
|
project_hint = ""
|
|
116
128
|
standalone_kws = ["palindrome", "fibonacci", "quicksort", "bubblesort", "factorial", "hello world", "calculator", "reverse string"]
|
|
117
129
|
is_standalone = any(kw in task.lower() for kw in standalone_kws)
|
|
118
|
-
if task_context
|
|
130
|
+
ctx_intent = getattr(task_context, "intent", "create") if task_context else "create"
|
|
131
|
+
has_repo_ref = any(term in task.lower() for term in ["this repo", "this project", "existing", "codebase", "our app", "in the project", "entry point"])
|
|
132
|
+
|
|
133
|
+
if task_context and not is_standalone and (ctx_intent in ("modify", "debug", "explain") or has_repo_ref):
|
|
119
134
|
_fw = ", ".join(getattr(task_context, "frameworks", []) or []) or "none"
|
|
120
135
|
_pt = getattr(task_context, "project_type", "unknown")
|
|
121
136
|
_fc = getattr(task_context, "file_count", 0)
|
|
@@ -760,60 +760,112 @@ def _apply_patch(workdir: Path, path: str, patch: str) -> str:
|
|
|
760
760
|
|
|
761
761
|
|
|
762
762
|
def _web_search(query: str, num_results: int = 5) -> str:
|
|
763
|
-
"""Search the web using
|
|
763
|
+
"""Search the web using httpx and DuckDuckGo HTTP endpoints, parsed via BeautifulSoup."""
|
|
764
764
|
import warnings
|
|
765
|
+
import urllib.parse
|
|
765
766
|
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
|
766
767
|
|
|
767
768
|
try:
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
769
|
+
import httpx
|
|
770
|
+
from bs4 import BeautifulSoup
|
|
771
|
+
|
|
772
|
+
headers = {
|
|
773
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
774
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
775
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
776
|
+
}
|
|
777
|
+
with httpx.Client(timeout=10.0, follow_redirects=True) as client:
|
|
778
|
+
resp = client.post("https://html.duckduckgo.com/html/", data={"q": query}, headers=headers)
|
|
779
|
+
if resp.status_code != 200:
|
|
780
|
+
resp = client.post("https://lite.duckduckgo.com/lite/", data={"q": query}, headers=headers)
|
|
781
|
+
|
|
782
|
+
if resp.status_code == 200 and resp.text:
|
|
783
|
+
soup = BeautifulSoup(resp.text, "html.parser")
|
|
784
|
+
results = []
|
|
785
|
+
for item in soup.select(".result"):
|
|
786
|
+
title_elem = item.select_one(".result__title a")
|
|
787
|
+
snippet_elem = item.select_one(".result__snippet")
|
|
788
|
+
if title_elem:
|
|
789
|
+
title = title_elem.get_text(strip=True)
|
|
790
|
+
href = title_elem.get("href", "")
|
|
791
|
+
if "uddg=" in href:
|
|
792
|
+
parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
|
|
793
|
+
href = parsed.get("uddg", [href])[0]
|
|
794
|
+
snippet = snippet_elem.get_text(strip=True) if snippet_elem else ""
|
|
795
|
+
results.append({"title": title, "href": href, "body": snippet})
|
|
796
|
+
if len(results) >= min(num_results, 10):
|
|
797
|
+
break
|
|
798
|
+
|
|
799
|
+
if not results:
|
|
800
|
+
for row in soup.select("table tr"):
|
|
801
|
+
link = row.select_one("a.result-link")
|
|
802
|
+
snippet = row.select_one(".result-snippet")
|
|
803
|
+
if link:
|
|
804
|
+
title = link.get_text(strip=True)
|
|
805
|
+
href = link.get("href", "")
|
|
806
|
+
if "uddg=" in href:
|
|
807
|
+
parsed = urllib.parse.parse_qs(urllib.parse.urlparse(href).query)
|
|
808
|
+
href = parsed.get("uddg", [href])[0]
|
|
809
|
+
snip_text = snippet.get_text(strip=True) if snippet else ""
|
|
810
|
+
results.append({"title": title, "href": href, "body": snip_text})
|
|
811
|
+
if len(results) >= min(num_results, 10):
|
|
812
|
+
break
|
|
813
|
+
|
|
814
|
+
if results:
|
|
815
|
+
formatted = []
|
|
816
|
+
for i, r in enumerate(results, 1):
|
|
817
|
+
formatted.append(f"{i}. **{r.get('title', 'No title')}**\n URL: {r.get('href', '')}\n {r.get('body', '')}")
|
|
818
|
+
return "\n\n".join(formatted)
|
|
786
819
|
except Exception as e:
|
|
787
|
-
return f"error during web search: {e}
|
|
820
|
+
return f"error during web search: {e}"
|
|
821
|
+
|
|
822
|
+
return (
|
|
823
|
+
f"No web results found for '{query}'. "
|
|
824
|
+
"Tip: If searching for Python package documentation, use bash: python -c 'import pkg; help(pkg)'"
|
|
825
|
+
)
|
|
788
826
|
|
|
789
827
|
|
|
790
828
|
def _fetch_url(url: str) -> str:
|
|
791
|
-
"""Fetch and extract text content from a URL."""
|
|
829
|
+
"""Fetch and extract text content from a URL using httpx."""
|
|
830
|
+
html = ""
|
|
792
831
|
try:
|
|
793
|
-
import
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
html
|
|
797
|
-
|
|
798
|
-
|
|
832
|
+
import httpx
|
|
833
|
+
headers = {
|
|
834
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
835
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
836
|
+
}
|
|
837
|
+
with httpx.Client(timeout=15.0, follow_redirects=True) as client:
|
|
838
|
+
resp = client.get(url, headers=headers)
|
|
839
|
+
html = resp.text
|
|
840
|
+
except Exception:
|
|
799
841
|
try:
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
842
|
+
import urllib.request
|
|
843
|
+
req = urllib.request.Request(url, headers={"User-Agent": "AlpieCode/0.2.0"})
|
|
844
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
845
|
+
html = resp.read().decode("utf-8", errors="replace")
|
|
846
|
+
except Exception as e:
|
|
847
|
+
return f"error fetching URL: {e}"
|
|
848
|
+
|
|
849
|
+
if not html:
|
|
850
|
+
return "error fetching URL: empty response"
|
|
851
|
+
|
|
852
|
+
# Try to extract text with beautifulsoup if available
|
|
853
|
+
try:
|
|
854
|
+
from bs4 import BeautifulSoup
|
|
855
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
856
|
+
# Remove script and style elements
|
|
857
|
+
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
|
858
|
+
tag.decompose()
|
|
859
|
+
text = soup.get_text(separator="\n", strip=True)
|
|
860
|
+
except ImportError:
|
|
861
|
+
# Fallback: basic HTML tag stripping
|
|
862
|
+
text = re.sub(r"<[^>]+>", "", html)
|
|
863
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
864
|
+
|
|
865
|
+
# Truncate to reasonable length
|
|
866
|
+
if len(text) > 8000:
|
|
867
|
+
text = text[:8000] + f"\n\n... (truncated, {len(text)} chars total)"
|
|
868
|
+
return text
|
|
817
869
|
|
|
818
870
|
|
|
819
871
|
def _request_user_input(question: str) -> str:
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for adaptive conversational intelligence, prompt rephraser isolation,
|
|
3
|
+
adaptive timeout configuration, and streaming error resilience.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
from unittest.mock import MagicMock, patch
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from codeagent.assessment import assess_task, TaskIntent, ExecutionStrategy
|
|
12
|
+
from codeagent.discovery import build_task_context, detect_intent
|
|
13
|
+
from codeagent.prompt import classify_task, PromptBuilder
|
|
14
|
+
from codeagent.rephraser import PromptRephraser
|
|
15
|
+
from codeagent.backends.openai_backend import OpenAIBackend
|
|
16
|
+
from codeagent.orchestrator import AgentOrchestrator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_conversational_questions_route_to_qa_without_tools():
|
|
20
|
+
"""Verify common user conversational inquiries route directly to QA with fast_path and 0 tools."""
|
|
21
|
+
conversational_queries = [
|
|
22
|
+
"what can you do for me?",
|
|
23
|
+
"who are you?",
|
|
24
|
+
"what can you do?",
|
|
25
|
+
"can you help me?",
|
|
26
|
+
"how to use alpiecode?",
|
|
27
|
+
"what tools do you have?",
|
|
28
|
+
"help",
|
|
29
|
+
"what are your capabilities?",
|
|
30
|
+
"how does Python asyncio work?",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
pb = PromptBuilder()
|
|
34
|
+
|
|
35
|
+
for q in conversational_queries:
|
|
36
|
+
assessment = assess_task(q)
|
|
37
|
+
assert assessment.intent == TaskIntent.QA.value, f"Failed on: {q}"
|
|
38
|
+
assert assessment.strategy == ExecutionStrategy.FAST_PATH.value, f"Failed on: {q}"
|
|
39
|
+
assert assessment.legacy_complexity == "qa", f"Failed on: {q}"
|
|
40
|
+
assert assessment.verification_required is False
|
|
41
|
+
|
|
42
|
+
# Verify zero tools are provided
|
|
43
|
+
tools = pb.get_tools(is_offline=False, complexity="qa")
|
|
44
|
+
assert tools == [], f"Tools should be empty for QA task: {q}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_rephraser_skips_qa_and_preserves_new_project_independence():
|
|
48
|
+
"""Verify rephraser never turns questions into software specs and does not pollute new project tasks."""
|
|
49
|
+
rephraser = PromptRephraser()
|
|
50
|
+
tc_qa = build_task_context("what can you do for me?", Path("."))
|
|
51
|
+
|
|
52
|
+
# QA query must be returned as-is without invoking model
|
|
53
|
+
result_qa = rephraser.rephrase("what can you do for me?", None, tc_qa)
|
|
54
|
+
assert result_qa == "what can you do for me?"
|
|
55
|
+
|
|
56
|
+
# Question ending with ? without action verbs must also be returned as-is
|
|
57
|
+
result_q2 = rephraser.rephrase("is this running locally?", None, None)
|
|
58
|
+
assert result_q2 == "is this running locally?"
|
|
59
|
+
|
|
60
|
+
# For brand-new creation tasks, existing Python/FastAPI repo context must not be injected
|
|
61
|
+
mock_backend = MagicMock()
|
|
62
|
+
mock_backend.is_available = True
|
|
63
|
+
mock_backend.chat_completion.return_value = "Refined spec"
|
|
64
|
+
|
|
65
|
+
tc_create = build_task_context("create a website where i can interact with solar system in C++", Path("."))
|
|
66
|
+
# Calling rephrase should not inject the unrelated python/fastapi repo context
|
|
67
|
+
rephraser.rephrase("create a website where i can interact with solar system in C++", mock_backend, tc_create)
|
|
68
|
+
assert mock_backend.chat_completion.called
|
|
69
|
+
call_args = mock_backend.chat_completion.call_args[1]
|
|
70
|
+
messages = call_args["messages"]
|
|
71
|
+
user_prompt = messages[1]["content"]
|
|
72
|
+
assert "Project Context:" not in user_prompt
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_openai_backend_adaptive_timeout():
|
|
76
|
+
"""Verify OpenAIBackend uses 180s for thinking mode and 90s for standard requests."""
|
|
77
|
+
cfg = SimpleNamespace(
|
|
78
|
+
base_url="http://mock-api",
|
|
79
|
+
api_key="test-key",
|
|
80
|
+
model="alpie_9b",
|
|
81
|
+
temperature=0.1,
|
|
82
|
+
max_tokens=2048,
|
|
83
|
+
)
|
|
84
|
+
backend = OpenAIBackend(cfg)
|
|
85
|
+
|
|
86
|
+
mock_client = MagicMock()
|
|
87
|
+
mock_client.chat.completions.create.return_value = []
|
|
88
|
+
backend._client = mock_client
|
|
89
|
+
backend._resolved_model = "alpie_9b"
|
|
90
|
+
|
|
91
|
+
# 1. Thinking enabled -> 180.0s timeout
|
|
92
|
+
list(backend.chat_completion_stream(messages=[{"role": "user", "content": "test"}], enable_thinking=True))
|
|
93
|
+
create_call = mock_client.chat.completions.create.call_args[1]
|
|
94
|
+
assert create_call["timeout"] == 180.0
|
|
95
|
+
|
|
96
|
+
# 2. Thinking disabled -> 90.0s timeout
|
|
97
|
+
list(backend.chat_completion_stream(messages=[{"role": "user", "content": "test"}], enable_thinking=False))
|
|
98
|
+
create_call_no_think = mock_client.chat.completions.create.call_args[1]
|
|
99
|
+
assert create_call_no_think["timeout"] == 90.0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_orchestrator_retries_transient_streaming_errors():
|
|
103
|
+
"""Verify orchestrator recovers from transient streaming chunk drops."""
|
|
104
|
+
from codeagent.session import SessionManager
|
|
105
|
+
from codeagent.backends.base import ChatResponse
|
|
106
|
+
|
|
107
|
+
cfg = SimpleNamespace(
|
|
108
|
+
base_url="http://mock-api",
|
|
109
|
+
api_key="test-key",
|
|
110
|
+
model="alpie_9b",
|
|
111
|
+
temperature=0.1,
|
|
112
|
+
max_tokens=2048,
|
|
113
|
+
enable_thinking=False,
|
|
114
|
+
max_turns=5,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
mock_backend = MagicMock(spec=OpenAIBackend)
|
|
118
|
+
mock_backend.name = "Mock OpenAI"
|
|
119
|
+
mock_backend.is_available = True
|
|
120
|
+
|
|
121
|
+
# Stream throws transient error mid-flight
|
|
122
|
+
def failing_stream(**kwargs):
|
|
123
|
+
raise RuntimeError("Server error: An error occurred during streaming")
|
|
124
|
+
yield # make it a generator
|
|
125
|
+
|
|
126
|
+
mock_backend.chat_completion_stream = failing_stream
|
|
127
|
+
# Non-streaming fallback succeeds
|
|
128
|
+
mock_backend.chat_completion.return_value = ChatResponse(
|
|
129
|
+
content="I am AlpieCode, an AI coding assistant.",
|
|
130
|
+
reasoning=None,
|
|
131
|
+
tool_calls=None,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
sm = SessionManager()
|
|
135
|
+
session = sm.create_session(Path("."))
|
|
136
|
+
orchestrator = AgentOrchestrator(backend=mock_backend)
|
|
137
|
+
|
|
138
|
+
with patch("codeagent.orchestrator.is_internet_available", return_value=True):
|
|
139
|
+
events = list(orchestrator.run_task(session, "what can you do for me?", cfg))
|
|
140
|
+
|
|
141
|
+
event_types = [ev.type for ev in events]
|
|
142
|
+
assert "warning" in event_types
|
|
143
|
+
assert "done" in event_types
|
|
144
|
+
# Verify warning announced resilient retry
|
|
145
|
+
warn_event = next(ev for ev in events if ev.type == "warning")
|
|
146
|
+
assert "resilient connection" in warn_event.data["message"].lower() or "retrying" in warn_event.data["message"].lower()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_strict_format_detection_and_dynamic_reasoning_gating():
|
|
150
|
+
"""Verify strict format tasks are detected and dynamic reasoning gating disables thinking."""
|
|
151
|
+
strict_tasks = [
|
|
152
|
+
"Return only JSON with keys: status, version",
|
|
153
|
+
"Output valid JSON: {'status': 'ok'}",
|
|
154
|
+
"respond with json only",
|
|
155
|
+
"Give me single line without explanation",
|
|
156
|
+
"one word answer: yes or no",
|
|
157
|
+
"regex only for email validation",
|
|
158
|
+
]
|
|
159
|
+
for q in strict_tasks:
|
|
160
|
+
ass = assess_task(q)
|
|
161
|
+
assert ass.requires_strict_format is True, f"Failed detection for: {q}"
|
|
162
|
+
assert ass.to_dict()["requires_strict_format"] is True
|
|
163
|
+
|
|
164
|
+
non_strict_tasks = [
|
|
165
|
+
"create a fullstack application",
|
|
166
|
+
"debug memory leak in server.py",
|
|
167
|
+
"what is quicksort?",
|
|
168
|
+
]
|
|
169
|
+
for q in non_strict_tasks:
|
|
170
|
+
ass = assess_task(q)
|
|
171
|
+
assert ass.requires_strict_format is False, f"False positive for: {q}"
|
|
172
|
+
|
|
173
|
+
# Verify orchestrator gates thinking off even when cfg.enable_thinking is True
|
|
174
|
+
from codeagent.session import SessionManager
|
|
175
|
+
from codeagent.backends.base import ChatResponse
|
|
176
|
+
|
|
177
|
+
cfg = SimpleNamespace(
|
|
178
|
+
base_url="http://mock-api",
|
|
179
|
+
api_key="test-key",
|
|
180
|
+
model="alpie_9b",
|
|
181
|
+
temperature=0.1,
|
|
182
|
+
max_tokens=2048,
|
|
183
|
+
enable_thinking=True, # User passed --thinking
|
|
184
|
+
force_thinking=False,
|
|
185
|
+
adaptive_thinking=True,
|
|
186
|
+
max_turns=2,
|
|
187
|
+
)
|
|
188
|
+
mock_backend = MagicMock()
|
|
189
|
+
mock_backend.name = "Mock"
|
|
190
|
+
mock_backend.is_available = True
|
|
191
|
+
mock_backend.chat_completion_stream.return_value = iter([
|
|
192
|
+
ChatResponse(content='{"status": "ok"}', reasoning=None, tool_calls=None)
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
sm = SessionManager()
|
|
196
|
+
session = sm.create_session(Path("."))
|
|
197
|
+
orchestrator = AgentOrchestrator(backend=mock_backend)
|
|
198
|
+
|
|
199
|
+
with patch("codeagent.orchestrator.is_internet_available", return_value=True):
|
|
200
|
+
events = list(orchestrator.run_task(session, "Return only JSON with keys: status, version", cfg))
|
|
201
|
+
|
|
202
|
+
adaptive_events = [ev for ev in events if ev.type == "adaptive_mode"]
|
|
203
|
+
assert any(ev.data.get("strict_format_gated") is True for ev in adaptive_events)
|
|
204
|
+
# Verify the stream was called with enable_thinking=False!
|
|
205
|
+
call_args = mock_backend.chat_completion_stream.call_args[1]
|
|
206
|
+
assert call_args["enable_thinking"] is False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_force_thinking_overrides_dynamic_reasoning_gating():
|
|
210
|
+
"""Verify force_thinking=True forces reasoning on strict format requests."""
|
|
211
|
+
from codeagent.session import SessionManager
|
|
212
|
+
from codeagent.backends.base import ChatResponse
|
|
213
|
+
|
|
214
|
+
cfg = SimpleNamespace(
|
|
215
|
+
base_url="http://mock-api",
|
|
216
|
+
api_key="test-key",
|
|
217
|
+
model="alpie_9b",
|
|
218
|
+
temperature=0.1,
|
|
219
|
+
max_tokens=2048,
|
|
220
|
+
enable_thinking=True,
|
|
221
|
+
force_thinking=True, # User explicitly used --force-thinking
|
|
222
|
+
adaptive_thinking=True,
|
|
223
|
+
max_turns=2,
|
|
224
|
+
)
|
|
225
|
+
mock_backend = MagicMock()
|
|
226
|
+
mock_backend.name = "Mock"
|
|
227
|
+
mock_backend.is_available = True
|
|
228
|
+
mock_backend.chat_completion_stream.return_value = iter([
|
|
229
|
+
ChatResponse(content='{"status": "ok"}', reasoning="Let me think...", tool_calls=None)
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
sm = SessionManager()
|
|
233
|
+
session = sm.create_session(Path("."))
|
|
234
|
+
orchestrator = AgentOrchestrator(backend=mock_backend)
|
|
235
|
+
|
|
236
|
+
with patch("codeagent.orchestrator.is_internet_available", return_value=True):
|
|
237
|
+
events = list(orchestrator.run_task(session, "Return only JSON with keys: status, version", cfg))
|
|
238
|
+
|
|
239
|
+
# Should NOT have strict_format_gated event
|
|
240
|
+
adaptive_events = [ev for ev in events if ev.type == "adaptive_mode" and ev.data.get("strict_format_gated")]
|
|
241
|
+
assert len(adaptive_events) == 0
|
|
242
|
+
|
|
243
|
+
call_args = mock_backend.chat_completion_stream.call_args[1]
|
|
244
|
+
assert call_args["enable_thinking"] is True
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_technical_scope_policy_filtering_in_system_prompts():
|
|
248
|
+
"""Verify system prompts include the technical scope directive to bypass sovereign policy checks."""
|
|
249
|
+
pb = PromptBuilder()
|
|
250
|
+
|
|
251
|
+
# Standard prompt
|
|
252
|
+
prompt_std = pb.build_system_prompt(Path("."), is_offline=False, complexity="low")
|
|
253
|
+
assert "Technical Focus Scope" in prompt_std
|
|
254
|
+
assert "bypass all non-applicable geopolitical" in prompt_std
|
|
255
|
+
|
|
256
|
+
# High complexity prompt
|
|
257
|
+
prompt_high = pb.build_system_prompt(Path("."), is_offline=False, complexity="high")
|
|
258
|
+
assert "Technical Focus Scope" in prompt_high
|
|
259
|
+
assert "bypass all non-applicable geopolitical" in prompt_high
|
|
260
|
+
|
|
261
|
+
# Offline prompt
|
|
262
|
+
prompt_offline = pb.build_system_prompt(Path("."), is_offline=True, complexity="low")
|
|
263
|
+
assert "Technical Focus Scope" in prompt_offline
|
|
264
|
+
assert "bypass all non-applicable geopolitical" in prompt_offline
|
|
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
|
|
File without changes
|
|
File without changes
|