alpiecode 10.0.2__tar.gz → 10.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-10.0.2 → alpiecode-10.0.3}/PKG-INFO +1 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/README.md +86 -12
- {alpiecode-10.0.2 → alpiecode-10.0.3}/pyproject.toml +1 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/SOURCES.txt +3 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/local_backend.py +25 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/cache.py +39 -8
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/cli.py +6 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/local_model.py +41 -9
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/memory.py +23 -4
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/metrics.py +19 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/orchestrator.py +9 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/prompt.py +5 -1
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/tools.py +16 -2
- alpiecode-10.0.3/test/test_math_tools.py +214 -0
- alpiecode-10.0.3/test/test_offline_opt.py +59 -0
- alpiecode-10.0.3/test/test_palindrome_check.py +50 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/setup.cfg +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/agent.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/client.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/config.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/context.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/executor.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/github.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/media.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/progress.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/rephraser.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/server.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/session.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/system1.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/updater.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_audit_fixes.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_benchmark_system1.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_cache_reads.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_chat_mode.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_claude_codex_capabilities.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_context_filter.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_metrics.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_offline_mode.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_phase1.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_phase2.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_phase3.py +0 -0
- {alpiecode-10.0.2 → alpiecode-10.0.3}/test/test_system1.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AlpieCode
|
|
2
2
|
|
|
3
3
|
[](https://www.python.org/)
|
|
4
|
-
[](https://github.com/169Pi/codeagent-poc)
|
|
5
5
|
[](https://marketplace.visualstudio.com/)
|
|
6
6
|
[](https://fastapi.tiangolo.com/)
|
|
7
7
|
[](https://colab.research.google.com/)
|
|
@@ -16,16 +16,18 @@
|
|
|
16
16
|
## 📑 Table of Contents
|
|
17
17
|
1. [🌟 Overview & Capabilities](#-overview--capabilities)
|
|
18
18
|
2. [🏗️ Core Architecture & Hybrid Dual-Engine](#️-core-architecture--hybrid-dual-engine)
|
|
19
|
-
3. [
|
|
20
|
-
4. [
|
|
21
|
-
5. [
|
|
22
|
-
6. [
|
|
23
|
-
7. [
|
|
24
|
-
8. [
|
|
25
|
-
9. [
|
|
26
|
-
10. [
|
|
27
|
-
11. [
|
|
28
|
-
12. [
|
|
19
|
+
3. [⚡ System 1 (Jev Architecture) & Performance Engine](#-system-1-jev-architecture--performance-engine)
|
|
20
|
+
4. [📊 Performance & Optimization Benchmarks](#-performance--optimization-benchmarks)
|
|
21
|
+
5. [📦 Complete Installation Guide](#-complete-installation-guide)
|
|
22
|
+
6. [🚀 Backend API Server (`alpiecode serve`)](#-backend-api-server-alpiecode-serve)
|
|
23
|
+
7. [⌨️ Command-Line Interface (CLI) Full Reference](#️-command-line-interface-cli-full-reference)
|
|
24
|
+
8. [💻 VS Code Extension Deep-Dive](#-vs-code-extension-deep-dive)
|
|
25
|
+
9. [🐍 Python SDK Reference (Programmatic Usage)](#-python-sdk-reference-programmatic-usage)
|
|
26
|
+
10. [📓 Jupyter Notebooks, JupyterLab & Google Colab](#-jupyter-notebooks-jupyterlab--google-colab)
|
|
27
|
+
11. [🛠️ 15 Built-In Autonomous Agent Tools](#️-15-built-in-autonomous-agent-tools)
|
|
28
|
+
12. [🛡️ Safety Guardian, Git Checkpoints & Memory System](#️-safety-guardian-git-checkpoints--memory-system)
|
|
29
|
+
13. [⚙️ Configuration & Environment Variables](#️-configuration--environment-variables)
|
|
30
|
+
14. [❓ FAQ & Troubleshooting](#-faq--troubleshooting)
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
@@ -122,6 +124,78 @@ AlpieCode employs an intelligent **Hybrid Dual-Engine** routing system that pair
|
|
|
122
124
|
|
|
123
125
|
---
|
|
124
126
|
|
|
127
|
+
## ⚡ System 1 (Jev Architecture) & Performance Engine
|
|
128
|
+
|
|
129
|
+
AlpieCode introduces a **Dual-Process Cognitive Architecture** inspired by TypeSafe AI's **Jev (System 1)** framework:
|
|
130
|
+
- **System 1 (Fast Reflex & Decision Plane)**: A sub-5ms non-autoregressive decision engine that handles routine routing, parameter validation, test parsing, loop prevention, and error triage.
|
|
131
|
+
- **System 2 (Deep Thought & Generative Engine)**: The heavy reasoning LLM (169Pi Cloud VLA / Claude / GPT-4o online, or local Qwen GGUF offline) dedicated purely to code synthesis and architectural reasoning.
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
┌──────────────────────────────────────────────┐
|
|
135
|
+
│ User Coding Task │
|
|
136
|
+
└──────────────────────┬───────────────────────┘
|
|
137
|
+
│
|
|
138
|
+
┌────────────────────────────────────────▼──────────────────────────────────────────────────┐
|
|
139
|
+
│ SYSTEM 1: Jev Decision Plane (<5ms) │
|
|
140
|
+
│ • Tool Call Validation • Repeated Loop Detection • Test Result Triage │
|
|
141
|
+
│ • Surgical Diff Enforcement • Context Noise Filtering │
|
|
142
|
+
└────────────────────────────────────────┬──────────────────────────────────────────────────┘
|
|
143
|
+
│
|
|
144
|
+
┌───────────────────┴───────────────────┐
|
|
145
|
+
▼ ▼
|
|
146
|
+
┌───────────────────────────┐ ┌───────────────────────────┐
|
|
147
|
+
│ Online Cloud Backend │ │ Offline Local Engine │
|
|
148
|
+
│ (169Pi / Claude / GPT) │ │ (llama.cpp Qwen GGUF) │
|
|
149
|
+
│ • Sub-12s Task Completion│ │ • Zero Context Overflow │
|
|
150
|
+
│ • >75% Token Savings │ │ • Zero Syntax Crash Loops│
|
|
151
|
+
└───────────────────────────┘ └───────────────────────────┘
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Key Capabilities
|
|
155
|
+
|
|
156
|
+
1. **⚡ TypeSafe System 1 Decision Loop (`src/codeagent/system1.py`)**:
|
|
157
|
+
- **Loop & Oscillation Detection**: Tracks tool call signatures. If the agent fails an operation twice (e.g. repeated syntax errors), System 1 intercepts the turn and forces a targeted `edit_file` directive rather than an infinite full-file rewrite.
|
|
158
|
+
- **Automated Test Health Parser**: Fast regex engine parsing pytest, unittest, and npm test outputs in <1ms to determine test success without burning LLM inference.
|
|
159
|
+
- **Tool Parameter Validator**: Pre-execution validation preventing malformed or missing parameters.
|
|
160
|
+
|
|
161
|
+
2. **🚀 Multi-Tier Caching System (`src/codeagent/cache.py`)**:
|
|
162
|
+
- **`FileReadCache`**: In-memory cache for repeated file reads keyed by `(path, mtime, size, line_range)`. Cuts read latency from 1.2ms to **<0.05ms** (24x speedup).
|
|
163
|
+
- **`TestResultCache`**: Disk-persisted cache in `~/.alpiecode/cache/test_cache.json` keyed by a workspace file fingerprint (`hash(mtimes)`). Unchanged test suites re-run in **<1ms** with zero redundant execution.
|
|
164
|
+
- **`ResponseCache`**: Disk-backed LRU cache for pure text explanations and Q&A.
|
|
165
|
+
|
|
166
|
+
3. **🗜️ Context Noise Filtering (`src/codeagent/context.py`)**:
|
|
167
|
+
- Compresses 50+ lines of clean passing test noise down to a 2-line summary (`[Clean Test Output]`).
|
|
168
|
+
- On test failures, preserves critical tracebacks and failure frames while stripping passing test dots, achieving **>75% token reduction**.
|
|
169
|
+
|
|
170
|
+
4. **📊 Real-Time Performance Observability (`src/codeagent/metrics.py`)**:
|
|
171
|
+
- At the conclusion of every task, AlpieCode renders an end-of-task summary:
|
|
172
|
+
```text
|
|
173
|
+
╭──────────────────────── 📊 Task Performance Summary ────────────────────────╮
|
|
174
|
+
│ ⏱️ Latency: 12.22s | 🧠 LLM Calls: 2 (5425ms) │
|
|
175
|
+
│ 🛠️ Tools Time: 153.5ms (1 calls) | 📦 Context Prep: 6253ms | ⚡ Cache: 1│
|
|
176
|
+
╰─────────────────────────────────────────────────────────────────────────────╯
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
5. **📜 Agent Operational Guidelines (`agents.md`)**:
|
|
180
|
+
- Operational rules defining the **Investigate → Plan → Surgical Edit → Verify → Complete** cycle, enforcing surgical diffs for files >100 lines and zero stub implementations.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 📊 Performance & Optimization Benchmarks
|
|
185
|
+
|
|
186
|
+
Comprehensive benchmarks comparing AlpieCode before vs. after the System 1 Jev engine and caching upgrade:
|
|
187
|
+
|
|
188
|
+
| Benchmark Metric | Baseline (v9.0) | With System 1 & Caching (v10.0.2) | Improvement |
|
|
189
|
+
| :--- | :---: | :---: | :---: |
|
|
190
|
+
| **Simple Test & Verify Task Latency** | 45.57s | **12.22s** | **3.7x faster (73% drop)** |
|
|
191
|
+
| **Repeated File Read Latency** | 1.20 ms | **<0.05 ms** | **24x faster** (In-Memory Cache) |
|
|
192
|
+
| **Unchanged Test Suite Re-run** | 3,500 ms | **<1.0 ms** | **Instant (Zero-Latency Cache)** |
|
|
193
|
+
| **Test Output Context Token Footprint**| ~3,500 tokens (500 lines) | **~80 tokens (4 lines)** | **>75% Context Compression** |
|
|
194
|
+
| **Syntax Error Retry Recovery** | Infinite full-file loop | **Loop intercepted on Turn 2** | Self-healing surgical diff |
|
|
195
|
+
| **Full Regression Test Suite** | 52 passed | **96 passed (100% green)** | Complete reliability |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
125
199
|
## 📦 Complete Installation Guide
|
|
126
200
|
|
|
127
201
|
### 1. Prerequisites
|
|
@@ -177,7 +251,7 @@ pip install dist/alpiecode-9.0.1-py3-none-any.whl
|
|
|
177
251
|
#### Verify CLI Installation:
|
|
178
252
|
```bash
|
|
179
253
|
alpiecode version
|
|
180
|
-
# Output: alpiecode
|
|
254
|
+
# Output: alpiecode 10.0.2
|
|
181
255
|
|
|
182
256
|
alpiecode doctor
|
|
183
257
|
```
|
|
@@ -48,8 +48,11 @@ test/test_cache_reads.py
|
|
|
48
48
|
test/test_chat_mode.py
|
|
49
49
|
test/test_claude_codex_capabilities.py
|
|
50
50
|
test/test_context_filter.py
|
|
51
|
+
test/test_math_tools.py
|
|
51
52
|
test/test_metrics.py
|
|
52
53
|
test/test_offline_mode.py
|
|
54
|
+
test/test_offline_opt.py
|
|
55
|
+
test/test_palindrome_check.py
|
|
53
56
|
test/test_phase1.py
|
|
54
57
|
test/test_phase2.py
|
|
55
58
|
test/test_phase3.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.0.
|
|
20
|
+
__version__ = "10.0.3"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -202,6 +202,9 @@ class LocalBackend:
|
|
|
202
202
|
full_content = []
|
|
203
203
|
full_reasoning = []
|
|
204
204
|
tool_calls_dict = {}
|
|
205
|
+
# In non-thinking mode, swallow any accidental <think>...</think> generated by model
|
|
206
|
+
suppress_think = not enable_thinking
|
|
207
|
+
inside_suppressed_think = False
|
|
205
208
|
|
|
206
209
|
for chunk in chunks:
|
|
207
210
|
if not chunk or not chunk.get("choices"):
|
|
@@ -210,7 +213,7 @@ class LocalBackend:
|
|
|
210
213
|
delta = choice.get("delta") or {}
|
|
211
214
|
|
|
212
215
|
explicit_reasoning = delta.get("reasoning") or delta.get("reasoning_content")
|
|
213
|
-
if explicit_reasoning:
|
|
216
|
+
if explicit_reasoning and enable_thinking:
|
|
214
217
|
if not think_started:
|
|
215
218
|
think_started = True
|
|
216
219
|
yield ("thinking_start", {"time": start_time})
|
|
@@ -238,6 +241,23 @@ class LocalBackend:
|
|
|
238
241
|
|
|
239
242
|
text = delta.get("content")
|
|
240
243
|
if text:
|
|
244
|
+
if suppress_think:
|
|
245
|
+
# Silently swallow any think tags/blocks emitted in fast mode
|
|
246
|
+
if "<think>" in text:
|
|
247
|
+
inside_suppressed_think = True
|
|
248
|
+
text = text.replace("<think>", "")
|
|
249
|
+
if inside_suppressed_think:
|
|
250
|
+
if "</think>" in text:
|
|
251
|
+
_, after = text.split("</think>", 1)
|
|
252
|
+
inside_suppressed_think = False
|
|
253
|
+
text = after
|
|
254
|
+
else:
|
|
255
|
+
text = ""
|
|
256
|
+
else:
|
|
257
|
+
text = text.replace("</think>", "")
|
|
258
|
+
if not text:
|
|
259
|
+
continue
|
|
260
|
+
|
|
241
261
|
if in_think:
|
|
242
262
|
if not think_started:
|
|
243
263
|
think_started = True
|
|
@@ -286,6 +306,10 @@ class LocalBackend:
|
|
|
286
306
|
tool_calls.append(ToolCall(id=item["id"], name=item["name"], arguments=args))
|
|
287
307
|
|
|
288
308
|
final_content_str = "".join(full_content).strip()
|
|
309
|
+
if not enable_thinking and final_content_str:
|
|
310
|
+
import re
|
|
311
|
+
final_content_str = re.sub(r"^\s*<think>.*?</think>\s*", "", final_content_str, flags=re.DOTALL).strip()
|
|
312
|
+
final_content_str = re.sub(r"^\s*(?:</think>|<think>)*\s*", "", final_content_str).strip()
|
|
289
313
|
final_reasoning_str = "".join(full_reasoning).strip()
|
|
290
314
|
|
|
291
315
|
if not final_content_str and final_reasoning_str:
|
|
@@ -246,15 +246,32 @@ class FileReadCache:
|
|
|
246
246
|
|
|
247
247
|
|
|
248
248
|
class TestResultCache:
|
|
249
|
-
"""Caches test command execution results when workspace files are unchanged.
|
|
249
|
+
"""Caches test command execution results when workspace files are unchanged.
|
|
250
|
+
Persists across CLI runs in ~/.alpiecode/cache/test_cache.json
|
|
251
|
+
"""
|
|
250
252
|
__test__ = False
|
|
251
253
|
|
|
252
254
|
def __init__(self, max_entries: int = 100):
|
|
253
255
|
self.max_entries = max_entries
|
|
256
|
+
self.disk_file = CACHE_DIR / "test_cache.json"
|
|
254
257
|
self._cache: Dict[str, Dict[str, Any]] = {}
|
|
255
|
-
self._workspace_versions: Dict[str, float] = {}
|
|
256
258
|
self.hits = 0
|
|
257
259
|
self.misses = 0
|
|
260
|
+
self._load_disk()
|
|
261
|
+
|
|
262
|
+
def _load_disk(self):
|
|
263
|
+
if self.disk_file.exists():
|
|
264
|
+
try:
|
|
265
|
+
self._cache = json.loads(self.disk_file.read_text(encoding="utf-8"))
|
|
266
|
+
except Exception:
|
|
267
|
+
self._cache = {}
|
|
268
|
+
|
|
269
|
+
def _save_disk(self):
|
|
270
|
+
try:
|
|
271
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
272
|
+
self.disk_file.write_text(json.dumps(self._cache, indent=2), encoding="utf-8")
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
258
275
|
|
|
259
276
|
def _is_test_cmd(self, cmd: str) -> bool:
|
|
260
277
|
c = cmd.strip().lower()
|
|
@@ -266,10 +283,21 @@ class TestResultCache:
|
|
|
266
283
|
or "go test" in c
|
|
267
284
|
)
|
|
268
285
|
|
|
286
|
+
def _get_workspace_fingerprint(self, workdir: Path) -> str:
|
|
287
|
+
try:
|
|
288
|
+
mtimes = []
|
|
289
|
+
for ext in ("*.py", "*.js", "*.ts", "*.json"):
|
|
290
|
+
for p in workdir.glob(ext):
|
|
291
|
+
if p.is_file() and not p.name.startswith("."):
|
|
292
|
+
mtimes.append(f"{p.name}:{p.stat().st_mtime_ns}")
|
|
293
|
+
return hashlib.md5("".join(sorted(mtimes)).encode()).hexdigest()[:12]
|
|
294
|
+
except Exception:
|
|
295
|
+
return "0"
|
|
296
|
+
|
|
269
297
|
def get(self, workdir: Path, cmd: str) -> Optional[str]:
|
|
270
298
|
if not self._is_test_cmd(cmd):
|
|
271
299
|
return None
|
|
272
|
-
version = self.
|
|
300
|
+
version = self._get_workspace_fingerprint(workdir)
|
|
273
301
|
key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
|
|
274
302
|
entry = self._cache.get(key)
|
|
275
303
|
if entry is not None:
|
|
@@ -281,17 +309,20 @@ class TestResultCache:
|
|
|
281
309
|
def put(self, workdir: Path, cmd: str, result: str) -> None:
|
|
282
310
|
if not self._is_test_cmd(cmd):
|
|
283
311
|
return
|
|
284
|
-
version = self.
|
|
312
|
+
version = self._get_workspace_fingerprint(workdir)
|
|
285
313
|
key = f"{workdir.resolve()}:{cmd.strip()}:{version}"
|
|
286
314
|
self._cache[key] = {"content": result, "cached_at": time.time()}
|
|
315
|
+
self._save_disk()
|
|
287
316
|
|
|
288
317
|
def invalidate(self, workdir: Optional[Path] = None) -> None:
|
|
289
318
|
if workdir is None:
|
|
290
319
|
self._cache.clear()
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
320
|
+
else:
|
|
321
|
+
prefix = f"{workdir.resolve()}:"
|
|
322
|
+
keys_to_del = [k for k in self._cache if k.startswith(prefix)]
|
|
323
|
+
for k in keys_to_del:
|
|
324
|
+
self._cache.pop(k, None)
|
|
325
|
+
self._save_disk()
|
|
295
326
|
|
|
296
327
|
|
|
297
328
|
_FILE_CACHE: Optional[FileReadCache] = None
|
|
@@ -87,6 +87,7 @@ def main():
|
|
|
87
87
|
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
88
88
|
common.add_argument("--debug", action="store_true", help="Show autonomous discovery and debug diagnostics")
|
|
89
89
|
common.add_argument("--offline", "--local", dest="offline", action="store_true", help="Run using local offline GGUF model on GPU without internet")
|
|
90
|
+
common.add_argument("--ctx-size", type=int, default=None, help="Context size in tokens (default: 4096 offline, 262144 online)")
|
|
90
91
|
|
|
91
92
|
from . import __version__
|
|
92
93
|
parser = argparse.ArgumentParser(
|
|
@@ -174,10 +175,14 @@ def main():
|
|
|
174
175
|
|
|
175
176
|
if getattr(args, "offline", False):
|
|
176
177
|
cfg.base_url = "offline"
|
|
177
|
-
|
|
178
|
+
# 4096 default fits 4.4GB model + KV cache 100% in 6GB VRAM with zero host-paging
|
|
178
179
|
import os
|
|
180
|
+
cfg.n_ctx = int(os.environ.get("ALPIECODE_CTX_SIZE", 4096))
|
|
179
181
|
os.environ["ALPIECODE_BASE_URL"] = "offline"
|
|
180
182
|
|
|
183
|
+
if getattr(args, "ctx_size", None):
|
|
184
|
+
cfg.n_ctx = args.ctx_size
|
|
185
|
+
|
|
181
186
|
if getattr(args, "thinking", False):
|
|
182
187
|
cfg.enable_thinking = True
|
|
183
188
|
elif getattr(args, "no_thinking", False):
|
|
@@ -237,20 +237,41 @@ def _ensure_llama_cpp():
|
|
|
237
237
|
|
|
238
238
|
is_win = sys.platform == "win32"
|
|
239
239
|
is_mac = sys.platform == "darwin"
|
|
240
|
+
# Check if NVIDIA GPU is present on Linux
|
|
241
|
+
has_nvidia = False
|
|
242
|
+
if not is_win and not is_mac:
|
|
243
|
+
for smi in ["nvidia-smi", "/usr/lib/wsl/lib/nvidia-smi"]:
|
|
244
|
+
try:
|
|
245
|
+
if subprocess.run([smi], capture_output=True, timeout=2).returncode == 0:
|
|
246
|
+
has_nvidia = True
|
|
247
|
+
break
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
|
|
240
251
|
if is_win:
|
|
241
252
|
wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-vulkan/llama_cpp_python-0.3.34-py3-none-win_amd64.whl"
|
|
253
|
+
install_targets = [wheel_url, "llama-cpp-python"]
|
|
242
254
|
elif is_mac:
|
|
243
255
|
wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-metal/llama_cpp_python-0.3.34-py3-none-macosx_11_0_arm64.whl"
|
|
256
|
+
install_targets = [wheel_url, "llama-cpp-python"]
|
|
257
|
+
elif has_nvidia:
|
|
258
|
+
# Linux with NVIDIA: use pre-compiled CUDA 12.4 / 12.1 wheel index
|
|
259
|
+
wheel_url = "https://abetlen.github.io/llama-cpp-python/whl/cu124"
|
|
260
|
+
install_targets = [
|
|
261
|
+
["--extra-index-url", wheel_url, "llama-cpp-python"],
|
|
262
|
+
["--extra-index-url", "https://abetlen.github.io/llama-cpp-python/whl/cu121", "llama-cpp-python"],
|
|
263
|
+
"llama-cpp-python",
|
|
264
|
+
]
|
|
244
265
|
else:
|
|
245
266
|
wheel_url = "https://github.com/abetlen/llama-cpp-python/releases/download/v0.3.34-vulkan/llama_cpp_python-0.3.34-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl"
|
|
246
|
-
|
|
247
|
-
install_targets = [wheel_url, "llama-cpp-python"]
|
|
267
|
+
install_targets = [wheel_url, "llama-cpp-python"]
|
|
248
268
|
installed = False
|
|
249
269
|
last_error = ""
|
|
250
270
|
for target in install_targets:
|
|
271
|
+
target_args = target if isinstance(target, list) else [target]
|
|
251
272
|
for cmd_base in [
|
|
252
|
-
["uv", "pip", "install"
|
|
253
|
-
[sys.executable, "-m", "pip", "install"
|
|
273
|
+
["uv", "pip", "install"] + target_args,
|
|
274
|
+
[sys.executable, "-m", "pip", "install"] + target_args,
|
|
254
275
|
]:
|
|
255
276
|
try:
|
|
256
277
|
res = subprocess.run(cmd_base, capture_output=True, text=True, timeout=180)
|
|
@@ -421,7 +442,18 @@ class LocalModel:
|
|
|
421
442
|
enable_thinking = kwargs.get("enable_thinking", True)
|
|
422
443
|
|
|
423
444
|
if not enable_thinking:
|
|
424
|
-
|
|
445
|
+
# Instruct model cleanly in prompt rather than injecting closed assistant turn
|
|
446
|
+
msgs = []
|
|
447
|
+
directive_tag = "[Directive: Respond directly with code or tool calls. Do not output <think> tags.]"
|
|
448
|
+
for i, m in enumerate(messages):
|
|
449
|
+
if i == len(messages) - 1 and m.get("role") == "user":
|
|
450
|
+
content = m.get("content", "")
|
|
451
|
+
if isinstance(content, str) and directive_tag not in content:
|
|
452
|
+
msgs.append({**m, "content": f"{content}\n\n{directive_tag}"})
|
|
453
|
+
else:
|
|
454
|
+
msgs.append(m)
|
|
455
|
+
else:
|
|
456
|
+
msgs.append(m)
|
|
425
457
|
else:
|
|
426
458
|
msgs = messages
|
|
427
459
|
|
|
@@ -469,13 +501,13 @@ class LocalModel:
|
|
|
469
501
|
else:
|
|
470
502
|
raise
|
|
471
503
|
|
|
472
|
-
#
|
|
504
|
+
# Clean any accidental think tags from non-thinking responses
|
|
473
505
|
if not enable_thinking and result_dict.get("choices"):
|
|
474
506
|
msg_dict = result_dict["choices"][0].get("message", {})
|
|
475
507
|
raw_content = msg_dict.get("content") or ""
|
|
476
|
-
|
|
477
|
-
cleaned = re.sub(r"^\s*(?:</think>|<think>)*\s*", "",
|
|
478
|
-
msg_dict["content"] =
|
|
508
|
+
cleaned = re.sub(r"^\s*<think>.*?</think>\s*", "", raw_content, flags=re.DOTALL)
|
|
509
|
+
cleaned = re.sub(r"^\s*(?:</think>|<think>)*\s*", "", cleaned)
|
|
510
|
+
msg_dict["content"] = cleaned
|
|
479
511
|
|
|
480
512
|
return _DictWrapper(result_dict)
|
|
481
513
|
|
|
@@ -105,7 +105,11 @@ def save_memory(workdir: Path, content: str, memory_type: str = "learning") -> N
|
|
|
105
105
|
save_memories_batch(workdir, [{"content": content, "type": memory_type}])
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def format_memories_for_prompt(
|
|
108
|
+
def format_memories_for_prompt(
|
|
109
|
+
workdir: Path,
|
|
110
|
+
max_entries: int = 5,
|
|
111
|
+
max_chars: int = 600,
|
|
112
|
+
) -> Optional[str]:
|
|
109
113
|
"""
|
|
110
114
|
Format memories into a string suitable for injection into the system prompt.
|
|
111
115
|
|
|
@@ -116,9 +120,24 @@ def format_memories_for_prompt(workdir: Path) -> Optional[str]:
|
|
|
116
120
|
if not memories:
|
|
117
121
|
return None
|
|
118
122
|
|
|
119
|
-
lines = ["## Recalled memories from previous sessions on this project
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
lines = ["## Recalled memories from previous sessions on this project:"]
|
|
124
|
+
total_chars = 0
|
|
125
|
+
# Use most recent memories first, single-line summarized to keep prompt lean
|
|
126
|
+
for mem in reversed(memories):
|
|
127
|
+
c = mem.get("content", "").strip()
|
|
128
|
+
if not c:
|
|
129
|
+
continue
|
|
130
|
+
first_line = c.split("\n")[0][:120]
|
|
131
|
+
entry = f"- [{mem.get('type', 'note')}] {first_line}"
|
|
132
|
+
if total_chars + len(entry) > max_chars:
|
|
133
|
+
break
|
|
134
|
+
lines.append(entry)
|
|
135
|
+
total_chars += len(entry)
|
|
136
|
+
if len(lines) - 1 >= max_entries:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if len(lines) <= 1:
|
|
140
|
+
return None
|
|
122
141
|
|
|
123
142
|
lines.append(
|
|
124
143
|
"\nNote: These memories are from previous sessions. "
|
|
@@ -27,6 +27,16 @@ class TaskMetrics:
|
|
|
27
27
|
tool_call_count: int = 0
|
|
28
28
|
cache_hits: int = 0
|
|
29
29
|
errors_encountered: int = 0
|
|
30
|
+
_initial_file_hits: int = 0
|
|
31
|
+
_initial_test_hits: int = 0
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
try:
|
|
35
|
+
from .cache import get_file_cache, get_test_cache
|
|
36
|
+
self._initial_file_hits = get_file_cache().hits
|
|
37
|
+
self._initial_test_hits = get_test_cache().hits
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
30
40
|
|
|
31
41
|
def start(self) -> "TaskMetrics":
|
|
32
42
|
self.start_time = time.time()
|
|
@@ -59,11 +69,19 @@ class TaskMetrics:
|
|
|
59
69
|
if self.end_time is None:
|
|
60
70
|
latency_ms = (time.time() - self.start_time) * 1000.0
|
|
61
71
|
|
|
72
|
+
total_cache_hits = self.cache_hits
|
|
73
|
+
try:
|
|
74
|
+
from .cache import get_file_cache, get_test_cache
|
|
75
|
+
delta = (get_file_cache().hits - self._initial_file_hits) + (get_test_cache().hits - self._initial_test_hits)
|
|
76
|
+
total_cache_hits += max(0, delta)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
|
|
62
80
|
return {
|
|
63
81
|
"task_id": self.task_id,
|
|
64
82
|
"llm_call_count": self.llm_call_count,
|
|
65
83
|
"tool_call_count": self.tool_call_count,
|
|
66
|
-
"cache_hits":
|
|
84
|
+
"cache_hits": total_cache_hits,
|
|
67
85
|
"total_task_latency_ms": round(latency_ms, 2),
|
|
68
86
|
"total_task_latency_s": round(latency_ms / 1000.0, 2),
|
|
69
87
|
"total_tool_execution_ms": round(self.total_tool_execution_ms, 2),
|
|
@@ -211,6 +211,7 @@ class AgentOrchestrator:
|
|
|
211
211
|
is_offline = not task_backend.is_available or isinstance(task_backend, LocalBackend)
|
|
212
212
|
session.is_offline = is_offline
|
|
213
213
|
|
|
214
|
+
ctx_prep_start = time.time()
|
|
214
215
|
# ── Configure tools & system prompt based on complexity ──
|
|
215
216
|
active_tools = self.prompt_builder.get_tools(is_offline=is_offline, complexity=complexity, task_context=task_context)
|
|
216
217
|
system_prompt = self.prompt_builder.build_system_prompt(
|
|
@@ -279,6 +280,7 @@ class AgentOrchestrator:
|
|
|
279
280
|
else:
|
|
280
281
|
base_enable_thinking = task_context.enable_thinking
|
|
281
282
|
|
|
283
|
+
task_metrics.record_context_prep((time.time() - ctx_prep_start) * 1000.0)
|
|
282
284
|
# ── Goal-driven turn loop (no fixed limit) ──
|
|
283
285
|
progress_monitor = ProgressMonitor()
|
|
284
286
|
turn = 0
|
|
@@ -427,6 +429,8 @@ class AgentOrchestrator:
|
|
|
427
429
|
yield AgentEvent("error", {"error": "Backend returned no response"})
|
|
428
430
|
return
|
|
429
431
|
|
|
432
|
+
task_metrics.record_llm_call((time.time() - llm_call_start) * 1000.0)
|
|
433
|
+
|
|
430
434
|
if resp.reasoning and not hasattr(task_backend, "chat_completion_stream"):
|
|
431
435
|
yield AgentEvent("thinking", {"content": resp.reasoning})
|
|
432
436
|
|
|
@@ -446,6 +450,11 @@ class AgentOrchestrator:
|
|
|
446
450
|
resp.content = resp.reasoning.strip()
|
|
447
451
|
resp.reasoning = None
|
|
448
452
|
|
|
453
|
+
# Clean any stray </think> tags from content
|
|
454
|
+
if resp.content and "</think>" in resp.content:
|
|
455
|
+
parts = resp.content.split("</think>", 1)
|
|
456
|
+
resp.content = parts[1].strip()
|
|
457
|
+
|
|
449
458
|
# ── DONE detection in assistant content ──
|
|
450
459
|
if resp.content and "DONE:" in resp.content.upper():
|
|
451
460
|
# Model said DONE — finish even if there are tool calls
|
|
@@ -549,7 +549,11 @@ class PromptBuilder:
|
|
|
549
549
|
prompt += "\n\n" + GOAL_DRIVEN_RULES
|
|
550
550
|
# qa intent: no extra prompt needed (model answers directly)
|
|
551
551
|
|
|
552
|
-
|
|
552
|
+
# For offline mode, keep system prompt lean to ensure sub-10s prefill on CPU/GPU
|
|
553
|
+
if is_offline:
|
|
554
|
+
memories = format_memories_for_prompt(workdir, max_entries=2, max_chars=250)
|
|
555
|
+
else:
|
|
556
|
+
memories = format_memories_for_prompt(workdir, max_entries=5, max_chars=800)
|
|
553
557
|
if memories:
|
|
554
558
|
prompt += f"\n\n{memories}"
|
|
555
559
|
return prompt
|
|
@@ -551,13 +551,27 @@ def _read_file(workdir: Path, path: str, start_line: int = None, end_line: int =
|
|
|
551
551
|
return result
|
|
552
552
|
|
|
553
553
|
|
|
554
|
+
def _resolve_safe_path(workdir: Path, path: str) -> Path:
|
|
555
|
+
"""Resolve a path safely within workdir, stripping synthetic root prefixes."""
|
|
556
|
+
raw_p = Path(path)
|
|
557
|
+
if raw_p.is_absolute():
|
|
558
|
+
try:
|
|
559
|
+
raw_p.resolve().relative_to(workdir.resolve())
|
|
560
|
+
return raw_p
|
|
561
|
+
except (ValueError, RuntimeError):
|
|
562
|
+
parts = [part for part in raw_p.parts if part not in ("/", "\\", "home", "user", "project", "workspace", "app", "root")]
|
|
563
|
+
if parts:
|
|
564
|
+
return workdir / Path(*parts)
|
|
565
|
+
return workdir / raw_p.name
|
|
566
|
+
return workdir / raw_p
|
|
567
|
+
|
|
554
568
|
def _write_file(workdir: Path, path: str, content: str) -> str:
|
|
555
569
|
# Pre-commit AST syntax check to prevent writing corrupt code
|
|
556
570
|
is_valid, err = validate_code_syntax(path, content)
|
|
557
571
|
if not is_valid:
|
|
558
572
|
return f"error: Pre-commit syntax check failed for '{path}':\n{err}\nPlease fix the syntax error before writing."
|
|
559
573
|
|
|
560
|
-
p = workdir
|
|
574
|
+
p = _resolve_safe_path(workdir, path)
|
|
561
575
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
562
576
|
p.write_text(content, encoding="utf-8")
|
|
563
577
|
_RECENTLY_WRITTEN_FILES.add(Path(path).name.lower())
|
|
@@ -567,7 +581,7 @@ def _write_file(workdir: Path, path: str, content: str) -> str:
|
|
|
567
581
|
|
|
568
582
|
|
|
569
583
|
def _edit_file(workdir: Path, path: str, old_str: str, new_str: str) -> str:
|
|
570
|
-
p = workdir
|
|
584
|
+
p = _resolve_safe_path(workdir, path)
|
|
571
585
|
if not p.exists():
|
|
572
586
|
return f"error: file not found: {path}"
|
|
573
587
|
text = p.read_text(encoding="utf-8", errors="replace")
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for math_tools module.
|
|
3
|
+
|
|
4
|
+
Run tests using: python3 -m unittest test_math_tools
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
from math_tools import add, multiply
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestAddFunction(unittest.TestCase):
|
|
12
|
+
"""Test cases for the add function."""
|
|
13
|
+
|
|
14
|
+
def test_positive_integers(self):
|
|
15
|
+
"""Test addition with positive integers."""
|
|
16
|
+
self.assertEqual(add(2, 3), 5)
|
|
17
|
+
self.assertEqual(add(10, 20), 30)
|
|
18
|
+
self.assertEqual(add(100, 200), 300)
|
|
19
|
+
|
|
20
|
+
def test_negative_integers(self):
|
|
21
|
+
"""Test addition with negative integers."""
|
|
22
|
+
self.assertEqual(add(-2, -3), -5)
|
|
23
|
+
self.assertEqual(add(-10, -20), -30)
|
|
24
|
+
self.assertEqual(add(-100, -200), -300)
|
|
25
|
+
|
|
26
|
+
def test_mixed_positive_negative(self):
|
|
27
|
+
"""Test addition with mixed positive and negative integers."""
|
|
28
|
+
self.assertEqual(add(-2, 3), 1)
|
|
29
|
+
self.assertEqual(add(2, -3), -1)
|
|
30
|
+
self.assertEqual(add(-10, 20), 10)
|
|
31
|
+
self.assertEqual(add(10, -20), -10)
|
|
32
|
+
|
|
33
|
+
def test_zero_inputs(self):
|
|
34
|
+
"""Test addition with zero."""
|
|
35
|
+
self.assertEqual(add(0, 0), 0)
|
|
36
|
+
self.assertEqual(add(5, 0), 5)
|
|
37
|
+
self.assertEqual(add(0, 5), 5)
|
|
38
|
+
self.assertEqual(add(-5, 0), -5)
|
|
39
|
+
self.assertEqual(add(0, -5), -5)
|
|
40
|
+
|
|
41
|
+
def test_float_inputs(self):
|
|
42
|
+
"""Test addition with floating point numbers."""
|
|
43
|
+
self.assertAlmostEqual(add(1.5, 2.5), 4.0)
|
|
44
|
+
self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=10)
|
|
45
|
+
self.assertAlmostEqual(add(-1.5, 2.5), 1.0)
|
|
46
|
+
self.assertAlmostEqual(add(1.234, 5.678), 6.912, places=10)
|
|
47
|
+
|
|
48
|
+
def test_large_numbers(self):
|
|
49
|
+
"""Test addition with large numbers."""
|
|
50
|
+
self.assertEqual(add(10**100, 10**100), 2 * 10**100)
|
|
51
|
+
self.assertEqual(add(-10**100, 10**100), 0)
|
|
52
|
+
self.assertEqual(add(10**100, -10**100 + 1), 1)
|
|
53
|
+
|
|
54
|
+
def test_small_numbers(self):
|
|
55
|
+
"""Test addition with very small numbers."""
|
|
56
|
+
self.assertAlmostEqual(add(1e-10, 2e-10), 3e-10, places=10)
|
|
57
|
+
self.assertAlmostEqual(add(-1e-10, 1e-10), 0.0, places=10)
|
|
58
|
+
|
|
59
|
+
def test_invalid_string_input(self):
|
|
60
|
+
"""Test that string inputs raise TypeError."""
|
|
61
|
+
with self.assertRaises(TypeError):
|
|
62
|
+
add("5", 3)
|
|
63
|
+
with self.assertRaises(TypeError):
|
|
64
|
+
add(5, "3")
|
|
65
|
+
with self.assertRaises(TypeError):
|
|
66
|
+
add("hello", "world")
|
|
67
|
+
|
|
68
|
+
def test_invalid_list_input(self):
|
|
69
|
+
"""Test that list inputs raise TypeError."""
|
|
70
|
+
with self.assertRaises(TypeError):
|
|
71
|
+
add([1, 2], 3)
|
|
72
|
+
with self.assertRaises(TypeError):
|
|
73
|
+
add(5, [1, 2])
|
|
74
|
+
|
|
75
|
+
def test_invalid_dict_input(self):
|
|
76
|
+
"""Test that dict inputs raise TypeError."""
|
|
77
|
+
with self.assertRaises(TypeError):
|
|
78
|
+
add({"a": 1}, 3)
|
|
79
|
+
with self.assertRaises(TypeError):
|
|
80
|
+
add(5, {"b": 2})
|
|
81
|
+
|
|
82
|
+
def test_invalid_none_input(self):
|
|
83
|
+
"""Test that None inputs raise TypeError."""
|
|
84
|
+
with self.assertRaises(TypeError):
|
|
85
|
+
add(None, 3)
|
|
86
|
+
with self.assertRaises(TypeError):
|
|
87
|
+
add(5, None)
|
|
88
|
+
|
|
89
|
+
def test_invalid_bool_input(self):
|
|
90
|
+
"""Test that bool inputs raise TypeError (bool is subclass of int)."""
|
|
91
|
+
with self.assertRaises(TypeError):
|
|
92
|
+
add(True, 3)
|
|
93
|
+
with self.assertRaises(TypeError):
|
|
94
|
+
add(5, False)
|
|
95
|
+
with self.assertRaises(TypeError):
|
|
96
|
+
add(True, False)
|
|
97
|
+
|
|
98
|
+
def test_complex_numbers(self):
|
|
99
|
+
"""Test that complex numbers raise TypeError."""
|
|
100
|
+
with self.assertRaises(TypeError):
|
|
101
|
+
add(1 + 2j, 3)
|
|
102
|
+
with self.assertRaises(TypeError):
|
|
103
|
+
add(5, 1 + 2j)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestMultiplyFunction(unittest.TestCase):
|
|
107
|
+
"""Test cases for the multiply function."""
|
|
108
|
+
|
|
109
|
+
def test_positive_integers(self):
|
|
110
|
+
"""Test multiplication with positive integers."""
|
|
111
|
+
self.assertEqual(multiply(2, 3), 6)
|
|
112
|
+
self.assertEqual(multiply(10, 20), 200)
|
|
113
|
+
self.assertEqual(multiply(7, 8), 56)
|
|
114
|
+
|
|
115
|
+
def test_negative_integers(self):
|
|
116
|
+
"""Test multiplication with negative integers."""
|
|
117
|
+
self.assertEqual(multiply(-2, -3), 6)
|
|
118
|
+
self.assertEqual(multiply(-10, -20), 200)
|
|
119
|
+
self.assertEqual(multiply(-7, 8), -56)
|
|
120
|
+
self.assertEqual(multiply(7, -8), -56)
|
|
121
|
+
|
|
122
|
+
def test_zero_inputs(self):
|
|
123
|
+
"""Test multiplication with zero."""
|
|
124
|
+
self.assertEqual(multiply(0, 0), 0)
|
|
125
|
+
self.assertEqual(multiply(5, 0), 0)
|
|
126
|
+
self.assertEqual(multiply(0, 5), 0)
|
|
127
|
+
self.assertEqual(multiply(-5, 0), 0)
|
|
128
|
+
self.assertEqual(multiply(0, -5), 0)
|
|
129
|
+
|
|
130
|
+
def test_float_inputs(self):
|
|
131
|
+
"""Test multiplication with floating point numbers."""
|
|
132
|
+
self.assertAlmostEqual(multiply(1.5, 2.5), 3.75)
|
|
133
|
+
self.assertAlmostEqual(multiply(0.1, 0.2), 0.02, places=10)
|
|
134
|
+
self.assertAlmostEqual(multiply(-1.5, 2.5), -3.75)
|
|
135
|
+
self.assertAlmostEqual(multiply(2.5, 0.4), 1.0)
|
|
136
|
+
|
|
137
|
+
def test_large_numbers(self):
|
|
138
|
+
"""Test multiplication with large numbers."""
|
|
139
|
+
self.assertEqual(multiply(10**50, 10**50), 10**100)
|
|
140
|
+
self.assertEqual(multiply(-10**50, 10**50), -10**100)
|
|
141
|
+
self.assertEqual(multiply(10**100, 10**100), 10**200)
|
|
142
|
+
|
|
143
|
+
def test_small_numbers(self):
|
|
144
|
+
"""Test multiplication with very small numbers."""
|
|
145
|
+
self.assertAlmostEqual(multiply(1e-10, 2e-10), 2e-20, places=10)
|
|
146
|
+
self.assertAlmostEqual(multiply(-1e-10, 1e-10), -1e-20, places=10)
|
|
147
|
+
|
|
148
|
+
def test_invalid_string_input(self):
|
|
149
|
+
"""Test that string inputs raise TypeError."""
|
|
150
|
+
with self.assertRaises(TypeError):
|
|
151
|
+
multiply("5", 3)
|
|
152
|
+
with self.assertRaises(TypeError):
|
|
153
|
+
multiply(5, "3")
|
|
154
|
+
with self.assertRaises(TypeError):
|
|
155
|
+
multiply("hello", "world")
|
|
156
|
+
|
|
157
|
+
def test_invalid_list_input(self):
|
|
158
|
+
"""Test that list inputs raise TypeError."""
|
|
159
|
+
with self.assertRaises(TypeError):
|
|
160
|
+
multiply([1, 2], 3)
|
|
161
|
+
with self.assertRaises(TypeError):
|
|
162
|
+
multiply(5, [1, 2])
|
|
163
|
+
|
|
164
|
+
def test_invalid_dict_input(self):
|
|
165
|
+
"""Test that dict inputs raise TypeError."""
|
|
166
|
+
with self.assertRaises(TypeError):
|
|
167
|
+
multiply({"a": 1}, 3)
|
|
168
|
+
with self.assertRaises(TypeError):
|
|
169
|
+
multiply(5, {"b": 2})
|
|
170
|
+
|
|
171
|
+
def test_invalid_none_input(self):
|
|
172
|
+
"""Test that None inputs raise TypeError."""
|
|
173
|
+
with self.assertRaises(TypeError):
|
|
174
|
+
multiply(None, 3)
|
|
175
|
+
with self.assertRaises(TypeError):
|
|
176
|
+
multiply(5, None)
|
|
177
|
+
|
|
178
|
+
def test_invalid_bool_input(self):
|
|
179
|
+
"""Test that bool inputs raise TypeError (bool is subclass of int)."""
|
|
180
|
+
with self.assertRaises(TypeError):
|
|
181
|
+
multiply(True, 3)
|
|
182
|
+
with self.assertRaises(TypeError):
|
|
183
|
+
multiply(5, False)
|
|
184
|
+
with self.assertRaises(TypeError):
|
|
185
|
+
multiply(True, False)
|
|
186
|
+
|
|
187
|
+
def test_complex_numbers(self):
|
|
188
|
+
"""Test that complex numbers raise TypeError."""
|
|
189
|
+
with self.assertRaises(TypeError):
|
|
190
|
+
multiply(1 + 2j, 3)
|
|
191
|
+
with self.assertRaises(TypeError):
|
|
192
|
+
multiply(5, 1 + 2j)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestModuleImport(unittest.TestCase):
|
|
196
|
+
"""Test that the module can be imported correctly."""
|
|
197
|
+
|
|
198
|
+
def test_module_import(self):
|
|
199
|
+
"""Test that both functions can be imported from the module."""
|
|
200
|
+
from math_tools import add, multiply
|
|
201
|
+
self.assertTrue(callable(add))
|
|
202
|
+
self.assertTrue(callable(multiply))
|
|
203
|
+
|
|
204
|
+
def test_function_names(self):
|
|
205
|
+
"""Test that function names are correct."""
|
|
206
|
+
import math_tools
|
|
207
|
+
self.assertTrue(hasattr(math_tools, 'add'))
|
|
208
|
+
self.assertTrue(hasattr(math_tools, 'multiply'))
|
|
209
|
+
self.assertTrue(callable(getattr(math_tools, 'add')))
|
|
210
|
+
self.assertTrue(callable(getattr(math_tools, 'multiply')))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == '__main__':
|
|
214
|
+
unittest.main()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
from codeagent.memory import format_memories_for_prompt
|
|
6
|
+
from codeagent.prompt import PromptBuilder
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestOfflineOptimization(unittest.TestCase):
|
|
10
|
+
def test_offline_prompt_is_lean(self):
|
|
11
|
+
pb = PromptBuilder()
|
|
12
|
+
prompt = pb.build_system_prompt(Path("."), is_offline=True, complexity="low")
|
|
13
|
+
# Ensure offline prompt is lean and under 2500 characters
|
|
14
|
+
self.assertLess(len(prompt), 2500)
|
|
15
|
+
self.assertIn("OFFLINE mode", prompt)
|
|
16
|
+
|
|
17
|
+
def test_memory_character_and_entry_capping(self):
|
|
18
|
+
fake_memories = [
|
|
19
|
+
{"content": "line 1\nline 2\nline 3" + "x" * 500, "type": "command"},
|
|
20
|
+
{"content": "def very_long_function():\n pass\n" + "y" * 500, "type": "structure"},
|
|
21
|
+
{"content": "third memory\n" + "z" * 500, "type": "learning"},
|
|
22
|
+
]
|
|
23
|
+
with patch("codeagent.memory.load_memories", return_value=fake_memories):
|
|
24
|
+
formatted = format_memories_for_prompt(Path("."), max_entries=2, max_chars=200)
|
|
25
|
+
self.assertIsNotNone(formatted)
|
|
26
|
+
self.assertLess(len(formatted), 400)
|
|
27
|
+
# Ensure multi-line scripts are condensed to single-line summaries
|
|
28
|
+
self.assertNotIn("def very_long_function():\n pass", formatted)
|
|
29
|
+
|
|
30
|
+
def test_stream_think_suppression_logic(self):
|
|
31
|
+
from codeagent.backends.local_backend import LocalBackend
|
|
32
|
+
from codeagent.config import Config
|
|
33
|
+
|
|
34
|
+
cfg = Config(base_url="offline", n_ctx=4096)
|
|
35
|
+
backend = LocalBackend(cfg)
|
|
36
|
+
|
|
37
|
+
# Mock chunk generator from model emitting think tags in non-thinking mode
|
|
38
|
+
mock_chunks = [
|
|
39
|
+
{"choices": [{"delta": {"content": "<think>\nThinking step 1...\n</think>"}}]},
|
|
40
|
+
{"choices": [{"delta": {"content": "Here is the code."}}]},
|
|
41
|
+
]
|
|
42
|
+
mock_model = MagicMock()
|
|
43
|
+
mock_model.create_chat_completion.return_value = mock_chunks
|
|
44
|
+
backend._model = mock_model
|
|
45
|
+
|
|
46
|
+
events = list(backend.chat_completion_stream(
|
|
47
|
+
messages=[{"role": "user", "content": "test"}],
|
|
48
|
+
enable_thinking=False,
|
|
49
|
+
))
|
|
50
|
+
|
|
51
|
+
tokens = [data["delta"] for event_type, data in events if event_type == "token"]
|
|
52
|
+
combined_tokens = "".join(tokens)
|
|
53
|
+
self.assertNotIn("<think>", combined_tokens)
|
|
54
|
+
self.assertNotIn("Thinking step 1", combined_tokens)
|
|
55
|
+
self.assertIn("Here is the code.", combined_tokens)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
unittest.main()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from palindrome_check import is_palindrome, is_palindrome_strict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestPalindromeCheck(unittest.TestCase):
|
|
6
|
+
|
|
7
|
+
def test_palindrome_basic(self):
|
|
8
|
+
self.assertTrue(is_palindrome("racecar"))
|
|
9
|
+
self.assertTrue(is_palindrome("Racecar"))
|
|
10
|
+
self.assertTrue(is_palindrome("a"))
|
|
11
|
+
self.assertTrue(is_palindrome(""))
|
|
12
|
+
|
|
13
|
+
def test_palindrome_case_insensitive(self):
|
|
14
|
+
self.assertTrue(is_palindrome("A man a plan a canal Panama"))
|
|
15
|
+
self.assertTrue(is_palindrome("Was it a car or a cat I saw"))
|
|
16
|
+
|
|
17
|
+
def test_palindrome_numbers(self):
|
|
18
|
+
self.assertTrue(is_palindrome("12321"))
|
|
19
|
+
self.assertTrue(is_palindrome("121"))
|
|
20
|
+
|
|
21
|
+
def test_not_palindrome(self):
|
|
22
|
+
self.assertFalse(is_palindrome("hello"))
|
|
23
|
+
self.assertFalse(is_palindrome("hello, world"))
|
|
24
|
+
self.assertFalse(is_palindrome("12345"))
|
|
25
|
+
|
|
26
|
+
def test_palindrome_strict_case_sensitive(self):
|
|
27
|
+
self.assertTrue(is_palindrome_strict("noon"))
|
|
28
|
+
self.assertTrue(is_palindrome_strict("level"))
|
|
29
|
+
self.assertTrue(is_palindrome_strict("racecar"))
|
|
30
|
+
self.assertFalse(is_palindrome_strict("Racecar")) # Case-sensitive: "Racecar" != "racecar"
|
|
31
|
+
self.assertFalse(is_palindrome_strict("hello"))
|
|
32
|
+
|
|
33
|
+
def test_palindrome_strict_exact(self):
|
|
34
|
+
self.assertTrue(is_palindrome_strict("noon"))
|
|
35
|
+
self.assertTrue(is_palindrome_strict("level"))
|
|
36
|
+
self.assertFalse(is_palindrome_strict("Hello"))
|
|
37
|
+
|
|
38
|
+
def test_palindrome_with_special_chars(self):
|
|
39
|
+
self.assertTrue(is_palindrome("..."))
|
|
40
|
+
self.assertTrue(is_palindrome("a-b-a"))
|
|
41
|
+
|
|
42
|
+
def test_palindrome_invalid_input(self):
|
|
43
|
+
with self.assertRaises(TypeError):
|
|
44
|
+
is_palindrome(123)
|
|
45
|
+
with self.assertRaises(TypeError):
|
|
46
|
+
is_palindrome(None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
unittest.main()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|