alpiecode 10.6.7__tar.gz → 10.7.0__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.6.7 → alpiecode-10.7.0}/PKG-INFO +1 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/pyproject.toml +1 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/__init__.py +1 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/agent.py +6 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/backends/local_backend.py +304 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/backends/openai_backend.py +13 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/config.py +20 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/local_model.py +4 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/orchestrator.py +7 -2
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/prompt.py +14 -1
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/rephraser.py +7 -1
- alpiecode-10.7.0/test/test_non_thinking_and_local_stream.py +136 -0
- alpiecode-10.6.7/test/test_non_thinking_and_local_stream.py +0 -70
- {alpiecode-10.6.7 → alpiecode-10.7.0}/README.md +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/setup.cfg +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode/__init__.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode.egg-info/SOURCES.txt +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/assessment.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/backends/base.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/cache.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/cli.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/client.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/compaction.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/context.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/discovery.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/doctor.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/dream_rsi.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/executor.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/git_ops.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/github.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/guardian.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/guardrails.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/media.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/memory.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/metrics.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/paths.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/progress.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/server.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/session.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/system1.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/tools.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/updater.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/verifier.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/src/codeagent/vscode_installer.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_adaptive_resilience.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_cli_ui.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_context_and_trajectory.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_dream_rsi.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_local_model_download.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_task_assessment.py +0 -0
- {alpiecode-10.6.7 → alpiecode-10.7.0}/test/test_verifier.py +0 -0
|
@@ -17,7 +17,7 @@ Supports:
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Optional, Any, Dict
|
|
19
19
|
|
|
20
|
-
__version__ = "10.6.
|
|
20
|
+
__version__ = "10.6.9"
|
|
21
21
|
|
|
22
22
|
def run(task: str, workdir: str = ".", reasoning_level: str = "thinking", **kwargs):
|
|
23
23
|
"""Run an autonomous coding task programmatically."""
|
|
@@ -734,7 +734,12 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True, debug: bool = Fal
|
|
|
734
734
|
session_mgr = SessionManager()
|
|
735
735
|
session = session_mgr.create_session(workdir, max_tokens=backend.context_window if hasattr(backend, 'context_window') and backend.context_window else (cfg.n_ctx if cfg.base_url == 'offline' else 262_144))
|
|
736
736
|
|
|
737
|
-
is_offline =
|
|
737
|
+
is_offline = (
|
|
738
|
+
getattr(backend, "is_offline", False) is True
|
|
739
|
+
or isinstance(backend, LocalBackend)
|
|
740
|
+
or not backend.is_available
|
|
741
|
+
or any(h in str(getattr(cfg, "base_url", "")).lower() for h in ("127.0.0.1", "localhost", "0.0.0.0", "offline"))
|
|
742
|
+
)
|
|
738
743
|
active_tools = PromptBuilder().get_tools(is_offline=is_offline)
|
|
739
744
|
|
|
740
745
|
if HAS_RICH:
|
|
@@ -39,6 +39,10 @@ class LocalBackend:
|
|
|
39
39
|
def is_available(self) -> bool:
|
|
40
40
|
return True
|
|
41
41
|
|
|
42
|
+
@property
|
|
43
|
+
def is_offline(self) -> bool:
|
|
44
|
+
return True
|
|
45
|
+
|
|
42
46
|
|
|
43
47
|
@property
|
|
44
48
|
def context_window(self) -> int:
|
|
@@ -401,6 +405,166 @@ class LocalBackend:
|
|
|
401
405
|
)
|
|
402
406
|
yield ("done", chat_resp)
|
|
403
407
|
|
|
408
|
+
def _get_bridge_url(self) -> str:
|
|
409
|
+
return os.environ.get("LOCAL_LLAMA_BRIDGE", "http://127.0.0.1:8080").rstrip("/")
|
|
410
|
+
|
|
411
|
+
def _is_bridge_online(self) -> bool:
|
|
412
|
+
try:
|
|
413
|
+
import httpx
|
|
414
|
+
r = httpx.get(f"{self._get_bridge_url()}/v1/models", timeout=0.3)
|
|
415
|
+
return r.status_code == 200
|
|
416
|
+
except Exception:
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
def _bridge_chat_completion(
|
|
420
|
+
self,
|
|
421
|
+
messages: List[dict],
|
|
422
|
+
tools: Optional[List[dict]] = None,
|
|
423
|
+
temperature: float = 0.2,
|
|
424
|
+
max_tokens: int = 2048,
|
|
425
|
+
enable_thinking: bool = True,
|
|
426
|
+
) -> ChatResponse:
|
|
427
|
+
import httpx
|
|
428
|
+
import json
|
|
429
|
+
bridge_url = self._get_bridge_url()
|
|
430
|
+
sanitized = self._sanitize_messages_for_gguf(messages)
|
|
431
|
+
if not enable_thinking and sanitized and sanitized[-1].get("role") == "user":
|
|
432
|
+
u_text = str(sanitized[-1].get("content", ""))
|
|
433
|
+
if "[Direct Mode:" not in u_text:
|
|
434
|
+
sanitized[-1]["content"] = u_text + "\n\n[Direct Mode: Output code and tool calls directly without <think> tags. Do not deliberate.]"
|
|
435
|
+
payload = {
|
|
436
|
+
"model": "local",
|
|
437
|
+
"messages": sanitized,
|
|
438
|
+
"temperature": temperature,
|
|
439
|
+
"max_tokens": max_tokens,
|
|
440
|
+
"stream": False,
|
|
441
|
+
}
|
|
442
|
+
if tools:
|
|
443
|
+
payload["tools"] = tools
|
|
444
|
+
payload["tool_choice"] = "auto"
|
|
445
|
+
client = httpx.Client(http2=False, timeout=120.0)
|
|
446
|
+
try:
|
|
447
|
+
resp = client.post(f"{bridge_url}/v1/chat/completions", json=payload)
|
|
448
|
+
finally:
|
|
449
|
+
client.close()
|
|
450
|
+
if resp.status_code != 200:
|
|
451
|
+
raise RuntimeError(f"Local bridge HTTP {resp.status_code}: {resp.text}")
|
|
452
|
+
data = json.loads(resp.text)
|
|
453
|
+
choice = data["choices"][0]["message"]
|
|
454
|
+
content = choice.get("content") or ""
|
|
455
|
+
reasoning = choice.get("reasoning") or choice.get("reasoning_content")
|
|
456
|
+
if not enable_thinking:
|
|
457
|
+
reasoning = None
|
|
458
|
+
if "<think>" in content:
|
|
459
|
+
import re
|
|
460
|
+
content = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()
|
|
461
|
+
raw_tc = choice.get("tool_calls")
|
|
462
|
+
tool_calls = None
|
|
463
|
+
if raw_tc:
|
|
464
|
+
tool_calls = []
|
|
465
|
+
for tc in raw_tc:
|
|
466
|
+
fn = tc.get("function", {})
|
|
467
|
+
args = fn.get("arguments", "{}")
|
|
468
|
+
if isinstance(args, str):
|
|
469
|
+
try:
|
|
470
|
+
args = json.loads(args)
|
|
471
|
+
except Exception:
|
|
472
|
+
args = {}
|
|
473
|
+
tool_calls.append(ToolCall(id=tc.get("id", "call_1"), name=fn.get("name", ""), arguments=args))
|
|
474
|
+
return ChatResponse(content=content, reasoning=reasoning, tool_calls=tool_calls, raw=data)
|
|
475
|
+
|
|
476
|
+
def _bridge_chat_completion_stream(
|
|
477
|
+
self,
|
|
478
|
+
messages: List[dict],
|
|
479
|
+
tools: Optional[List[dict]] = None,
|
|
480
|
+
temperature: Optional[float] = None,
|
|
481
|
+
max_tokens: Optional[int] = None,
|
|
482
|
+
enable_thinking: bool = True,
|
|
483
|
+
):
|
|
484
|
+
import httpx
|
|
485
|
+
import json
|
|
486
|
+
bridge_url = self._get_bridge_url()
|
|
487
|
+
sanitized = self._sanitize_messages_for_gguf(messages)
|
|
488
|
+
if not enable_thinking and sanitized and sanitized[-1].get("role") == "user":
|
|
489
|
+
u_text = str(sanitized[-1].get("content", ""))
|
|
490
|
+
if "[Direct Mode:" not in u_text:
|
|
491
|
+
sanitized[-1]["content"] = u_text + "\n\n[Direct Mode: Output code and tool calls directly without <think> tags. Do not deliberate.]"
|
|
492
|
+
payload = {
|
|
493
|
+
"model": "local",
|
|
494
|
+
"messages": sanitized,
|
|
495
|
+
"temperature": temperature or 0.2,
|
|
496
|
+
"max_tokens": max_tokens or 2048,
|
|
497
|
+
"stream": True,
|
|
498
|
+
}
|
|
499
|
+
if tools:
|
|
500
|
+
payload["tools"] = tools
|
|
501
|
+
payload["tool_choice"] = "auto"
|
|
502
|
+
client = httpx.Client(http2=False, timeout=120.0)
|
|
503
|
+
req = client.build_request("POST", f"{bridge_url}/v1/chat/completions", json=payload)
|
|
504
|
+
resp = client.send(req, stream=True)
|
|
505
|
+
if resp.status_code != 200:
|
|
506
|
+
err = resp.read().decode("utf-8", errors="replace")
|
|
507
|
+
resp.close()
|
|
508
|
+
client.close()
|
|
509
|
+
raise RuntimeError(f"Local bridge HTTP {resp.status_code}: {err}")
|
|
510
|
+
full_content = []
|
|
511
|
+
full_reasoning = []
|
|
512
|
+
tool_calls = []
|
|
513
|
+
suppress_think = not enable_thinking
|
|
514
|
+
inside_suppressed_think = False
|
|
515
|
+
try:
|
|
516
|
+
for line in resp.iter_lines():
|
|
517
|
+
line = line.strip()
|
|
518
|
+
if not line or not line.startswith("data: "):
|
|
519
|
+
continue
|
|
520
|
+
data_str = line[6:].strip()
|
|
521
|
+
if data_str == "[DONE]":
|
|
522
|
+
break
|
|
523
|
+
try:
|
|
524
|
+
chunk = json.loads(data_str)
|
|
525
|
+
except Exception:
|
|
526
|
+
continue
|
|
527
|
+
choice = chunk["choices"][0]
|
|
528
|
+
delta = choice.get("delta") or {}
|
|
529
|
+
text = delta.get("content")
|
|
530
|
+
if text:
|
|
531
|
+
if suppress_think:
|
|
532
|
+
if "<think>" in text:
|
|
533
|
+
inside_suppressed_think = True
|
|
534
|
+
text = text.replace("<think>", "")
|
|
535
|
+
if inside_suppressed_think:
|
|
536
|
+
if "<tool_call>" in text or "DONE:" in text or "<function=" in text:
|
|
537
|
+
inside_suppressed_think = False
|
|
538
|
+
elif "</think>" in text:
|
|
539
|
+
_, after = text.split("</think>", 1)
|
|
540
|
+
inside_suppressed_think = False
|
|
541
|
+
text = after
|
|
542
|
+
else:
|
|
543
|
+
text = ""
|
|
544
|
+
else:
|
|
545
|
+
text = text.replace("</think>", "")
|
|
546
|
+
if not text:
|
|
547
|
+
continue
|
|
548
|
+
full_content.append(text)
|
|
549
|
+
yield ("token", {"delta": text})
|
|
550
|
+
continue
|
|
551
|
+
|
|
552
|
+
full_content.append(text)
|
|
553
|
+
yield ("token", {"delta": text})
|
|
554
|
+
|
|
555
|
+
if (delta.get("reasoning") or delta.get("reasoning_content")) and enable_thinking:
|
|
556
|
+
r = delta.get("reasoning") or delta.get("reasoning_content")
|
|
557
|
+
full_reasoning.append(r)
|
|
558
|
+
yield ("thinking_delta", {"delta": r, "count": len(full_reasoning)})
|
|
559
|
+
finally:
|
|
560
|
+
resp.close()
|
|
561
|
+
client.close()
|
|
562
|
+
yield ("done", ChatResponse(
|
|
563
|
+
content="".join(full_content).strip(),
|
|
564
|
+
reasoning="".join(full_reasoning).strip() or None,
|
|
565
|
+
tool_calls=tool_calls or None,
|
|
566
|
+
))
|
|
567
|
+
|
|
404
568
|
def shutdown(self) -> None:
|
|
405
569
|
if self._model:
|
|
406
570
|
self._model._llm = None
|
|
@@ -840,13 +1004,152 @@ class AndroidBackend(BasePlatformBackend):
|
|
|
840
1004
|
def platform_name(self) -> str:
|
|
841
1005
|
return "Android"
|
|
842
1006
|
|
|
1007
|
+
def _get_bridge_url(self) -> str:
|
|
1008
|
+
return os.environ.get("ANDROID_LLAMA_BRIDGE", "http://127.0.0.1:8080").rstrip("/")
|
|
1009
|
+
|
|
1010
|
+
def _find_llama_server_bin(self) -> Optional[Path]:
|
|
1011
|
+
custom = os.environ.get("LLAMA_SERVER_PATH") or os.environ.get("LLAMA_BIN")
|
|
1012
|
+
if custom and Path(custom).is_file():
|
|
1013
|
+
return Path(custom)
|
|
1014
|
+
which_bin = shutil.which("llama-server")
|
|
1015
|
+
if which_bin:
|
|
1016
|
+
return Path(which_bin)
|
|
1017
|
+
candidates = [
|
|
1018
|
+
Path("/data/data/com.termux/files/usr/bin/llama-server"),
|
|
1019
|
+
Path.home() / "llama.cpp" / "build" / "bin" / "llama-server",
|
|
1020
|
+
Path.home() / "llama.cpp" / "build" / "bin" / "Release" / "llama-server",
|
|
1021
|
+
Path.home() / ".alpiecode" / "bin" / "llama-server",
|
|
1022
|
+
Path.cwd() / "llama.cpp" / "build" / "bin" / "llama-server",
|
|
1023
|
+
]
|
|
1024
|
+
for c in candidates:
|
|
1025
|
+
if c.is_file() and os.access(c, os.X_OK):
|
|
1026
|
+
return c
|
|
1027
|
+
return None
|
|
1028
|
+
|
|
1029
|
+
def _find_gguf_model(self) -> Optional[Path]:
|
|
1030
|
+
from ..local_model import get_model_cache_dir
|
|
1031
|
+
candidate_dirs = [
|
|
1032
|
+
get_model_cache_dir(),
|
|
1033
|
+
Path.home() / ".alpiecode" / "models",
|
|
1034
|
+
Path.home() / "models",
|
|
1035
|
+
Path.cwd() / "models",
|
|
1036
|
+
Path.cwd() / ".alpiecode" / "models",
|
|
1037
|
+
]
|
|
1038
|
+
for d in candidate_dirs:
|
|
1039
|
+
if d.exists():
|
|
1040
|
+
ggufs = list(d.glob("*.gguf"))
|
|
1041
|
+
mains = [g for g in ggufs if "mmproj" not in g.name.lower()]
|
|
1042
|
+
if mains:
|
|
1043
|
+
mains.sort(key=lambda p: p.stat().st_size)
|
|
1044
|
+
return mains[0]
|
|
1045
|
+
return None
|
|
1046
|
+
|
|
1047
|
+
def _ensure_local_server(self) -> bool:
|
|
1048
|
+
"""Auto-detect or auto-start local llama-server in background if needed."""
|
|
1049
|
+
if self._is_bridge_online():
|
|
1050
|
+
return True
|
|
1051
|
+
server_bin = self._find_llama_server_bin()
|
|
1052
|
+
if not server_bin:
|
|
1053
|
+
self._last_server_error = "llama-server binary was not found in PATH or ~/llama.cpp/build/bin/llama-server."
|
|
1054
|
+
return False
|
|
1055
|
+
model_path = self._find_gguf_model()
|
|
1056
|
+
if not model_path:
|
|
1057
|
+
self._last_server_error = "No GGUF model found in ~/.alpiecode/models or ~/models."
|
|
1058
|
+
return False
|
|
1059
|
+
mmprojs = list(model_path.parent.glob("*mmproj*.gguf"))
|
|
1060
|
+
cmd = [
|
|
1061
|
+
str(server_bin),
|
|
1062
|
+
"-m", str(model_path),
|
|
1063
|
+
"-c", "2048",
|
|
1064
|
+
"-t", "4",
|
|
1065
|
+
"-np", "1",
|
|
1066
|
+
"-b", "512",
|
|
1067
|
+
"-ub", "256",
|
|
1068
|
+
"--cache-type-k", "q8_0",
|
|
1069
|
+
"--cache-type-v", "q8_0",
|
|
1070
|
+
"--port", "8080",
|
|
1071
|
+
]
|
|
1072
|
+
if mmprojs:
|
|
1073
|
+
cmd += ["--mmproj", str(mmprojs[0])]
|
|
1074
|
+
try:
|
|
1075
|
+
print(f"⏳ Starting local llama-server in background ({model_path.name})...")
|
|
1076
|
+
subprocess.Popen(
|
|
1077
|
+
cmd,
|
|
1078
|
+
stdout=subprocess.DEVNULL,
|
|
1079
|
+
stderr=subprocess.DEVNULL,
|
|
1080
|
+
start_new_session=True,
|
|
1081
|
+
)
|
|
1082
|
+
import time
|
|
1083
|
+
for _ in range(35):
|
|
1084
|
+
if self._is_bridge_online():
|
|
1085
|
+
return True
|
|
1086
|
+
time.sleep(0.5)
|
|
1087
|
+
except Exception as e:
|
|
1088
|
+
self._last_server_error = f"Failed to launch llama-server: {e}"
|
|
1089
|
+
return False
|
|
1090
|
+
return self._is_bridge_online()
|
|
1091
|
+
|
|
843
1092
|
@property
|
|
844
1093
|
def is_available(self) -> bool:
|
|
845
1094
|
try:
|
|
846
1095
|
import llama_cpp
|
|
847
1096
|
return True
|
|
848
1097
|
except ImportError:
|
|
849
|
-
return bool(
|
|
1098
|
+
return self._is_bridge_online() or bool(self._find_llama_server_bin() or shutil.which("llama-cli"))
|
|
1099
|
+
|
|
1100
|
+
def _raise_missing_engine_error(self) -> None:
|
|
1101
|
+
err_detail = getattr(self, "_last_server_error", None)
|
|
1102
|
+
detail_msg = f"\n│ Reason: {err_detail}" if err_detail else ""
|
|
1103
|
+
raise RuntimeError(
|
|
1104
|
+
"\n╭────────────────────────────────────────────────────────────╮\n"
|
|
1105
|
+
"│ Android / Termux Local Inference: │\n"
|
|
1106
|
+
"│ Local inference server (http://127.0.0.1:8080) is offline.│"
|
|
1107
|
+
f"{detail_msg}\n"
|
|
1108
|
+
"│ │\n"
|
|
1109
|
+
"│ To start it manually in Termux: │\n"
|
|
1110
|
+
"│ llama-server -m ~/.alpiecode/models/*.gguf -c 2048 -t 4 │\n"
|
|
1111
|
+
"╰────────────────────────────────────────────────────────────╯"
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
def chat_completion(
|
|
1115
|
+
self,
|
|
1116
|
+
messages: List[dict],
|
|
1117
|
+
tools: Optional[List[dict]] = None,
|
|
1118
|
+
temperature: float = 0.2,
|
|
1119
|
+
max_tokens: int = 2048,
|
|
1120
|
+
enable_thinking: bool = True,
|
|
1121
|
+
) -> ChatResponse:
|
|
1122
|
+
try:
|
|
1123
|
+
import llama_cpp
|
|
1124
|
+
return super().chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
|
|
1125
|
+
except ImportError:
|
|
1126
|
+
pass
|
|
1127
|
+
|
|
1128
|
+
if self._ensure_local_server():
|
|
1129
|
+
return self._bridge_chat_completion(messages, tools, temperature, max_tokens, enable_thinking)
|
|
1130
|
+
|
|
1131
|
+
self._raise_missing_engine_error()
|
|
1132
|
+
|
|
1133
|
+
def chat_completion_stream(
|
|
1134
|
+
self,
|
|
1135
|
+
messages: List[dict],
|
|
1136
|
+
tools: Optional[List[dict]] = None,
|
|
1137
|
+
temperature: Optional[float] = None,
|
|
1138
|
+
max_tokens: Optional[int] = None,
|
|
1139
|
+
enable_thinking: bool = True,
|
|
1140
|
+
):
|
|
1141
|
+
try:
|
|
1142
|
+
import llama_cpp
|
|
1143
|
+
yield from super().chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
|
|
1144
|
+
return
|
|
1145
|
+
except ImportError:
|
|
1146
|
+
pass
|
|
1147
|
+
|
|
1148
|
+
if self._ensure_local_server():
|
|
1149
|
+
yield from self._bridge_chat_completion_stream(messages, tools, temperature, max_tokens, enable_thinking)
|
|
1150
|
+
return
|
|
1151
|
+
|
|
1152
|
+
self._raise_missing_engine_error()
|
|
850
1153
|
|
|
851
1154
|
|
|
852
1155
|
class MacOSBackend(BasePlatformBackend):
|
|
@@ -293,7 +293,7 @@ def _repair_and_parse_json_arguments(raw_args: str) -> dict:
|
|
|
293
293
|
"_parse_error": "Failed to parse streaming JSON arguments",
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
from ..config import Config, get_shared_http_client, is_server_reachable
|
|
296
|
+
from ..config import Config, get_shared_http_client, is_server_reachable, is_local_endpoint
|
|
297
297
|
from .base import ChatResponse, ToolCall
|
|
298
298
|
|
|
299
299
|
|
|
@@ -306,6 +306,18 @@ class OpenAIBackend:
|
|
|
306
306
|
self._resolved_model: Optional[str] = None
|
|
307
307
|
self._max_model_len: Optional[int] = None
|
|
308
308
|
|
|
309
|
+
@property
|
|
310
|
+
def base_url(self) -> str:
|
|
311
|
+
return getattr(self._cfg, "base_url", "")
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def is_local(self) -> bool:
|
|
315
|
+
return is_local_endpoint(self.base_url)
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def is_offline(self) -> bool:
|
|
319
|
+
return self.is_local
|
|
320
|
+
|
|
309
321
|
@property
|
|
310
322
|
def name(self) -> str:
|
|
311
323
|
return f"Online API ({self._cfg.base_url})"
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass, asdict, fields
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Optional
|
|
16
16
|
|
|
17
|
-
from .paths import ensure_dir, get_alpiecode_dir, is_ios, load_dotenv_if_present
|
|
17
|
+
from .paths import ensure_dir, get_alpiecode_dir, is_android, is_ios, load_dotenv_if_present
|
|
18
18
|
|
|
19
19
|
# Auto-load .env if present (zero-dependency)
|
|
20
20
|
load_dotenv_if_present()
|
|
@@ -120,6 +120,14 @@ def is_internet_available(timeout: float = 1.0) -> bool:
|
|
|
120
120
|
return False
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
def is_local_endpoint(base_url: Optional[str]) -> bool:
|
|
124
|
+
"""Check if base_url points to a local machine / loopback interface or offline mode."""
|
|
125
|
+
if not base_url:
|
|
126
|
+
return False
|
|
127
|
+
u = str(base_url).lower().strip()
|
|
128
|
+
return any(h in u for h in ("127.0.0.1", "localhost", "0.0.0.0", "::1", "offline"))
|
|
129
|
+
|
|
130
|
+
|
|
123
131
|
# ── Shared HTTP/2 connection pool ─────────────────────────────────────
|
|
124
132
|
_SHARED_HTTP_CLIENT = None
|
|
125
133
|
|
|
@@ -299,6 +307,17 @@ def interactive_init() -> Config:
|
|
|
299
307
|
else:
|
|
300
308
|
print("ℹ️ iOS Inference: Online model endpoint (https://test.169pi.ai) is configured.")
|
|
301
309
|
print(" For offline inference on iOS, launch an in-app bridge (http://127.0.0.1:8080).")
|
|
310
|
+
elif is_android():
|
|
311
|
+
print("\n⚙️ Configuring inference engine for Android...")
|
|
312
|
+
from .backends.local_backend import AndroidBackend
|
|
313
|
+
b = AndroidBackend(cfg)
|
|
314
|
+
if b.is_available:
|
|
315
|
+
print("✅ Android local engine ready (native llama-cpp or local server active).")
|
|
316
|
+
print("✅ Offline mode ready — works without internet from now on!")
|
|
317
|
+
else:
|
|
318
|
+
print("ℹ️ To enable local offline inference on Termux, please install:")
|
|
319
|
+
print(" pkg install llama-cpp")
|
|
320
|
+
print(" Then run: alpiecode init")
|
|
302
321
|
else:
|
|
303
322
|
try:
|
|
304
323
|
from .local_model import _ensure_llama_cpp
|
|
@@ -258,12 +258,15 @@ def _ensure_llama_cpp():
|
|
|
258
258
|
)
|
|
259
259
|
|
|
260
260
|
if is_android():
|
|
261
|
+
import shutil
|
|
262
|
+
if shutil.which("llama-server") or shutil.which("llama-cli"):
|
|
263
|
+
return None
|
|
261
264
|
raise RuntimeError(
|
|
262
265
|
"\n╭────────────────────────────────────────────────────────────╮\n"
|
|
263
266
|
"│ Android / Termux Native Inference: │\n"
|
|
264
267
|
"│ Dynamic pip compilation is disabled on Android. │\n"
|
|
265
268
|
"│ To enable local inference on Termux, please install: │\n"
|
|
266
|
-
"│ pkg install llama
|
|
269
|
+
"│ pkg install llama-cpp │\n"
|
|
267
270
|
"╰────────────────────────────────────────────────────────────╯"
|
|
268
271
|
)
|
|
269
272
|
|
|
@@ -47,7 +47,7 @@ from .backends.base import InferenceBackend, ChatResponse
|
|
|
47
47
|
from .backends.local_backend import LocalBackend
|
|
48
48
|
from .backends.openai_backend import OpenAIBackend
|
|
49
49
|
from .cache import get_cache
|
|
50
|
-
from .config import Config, is_server_reachable, is_internet_available
|
|
50
|
+
from .config import Config, is_server_reachable, is_internet_available, is_local_endpoint
|
|
51
51
|
from .memory import extract_and_save_memories
|
|
52
52
|
from .discovery import build_task_context, gather_relevant_context, COMPLEXITY_CONFIG
|
|
53
53
|
from .progress import ProgressMonitor
|
|
@@ -251,7 +251,12 @@ class AgentOrchestrator:
|
|
|
251
251
|
if isinstance(task_backend, LocalBackend) and is_server_reachable(cfg.base_url, timeout=1.5):
|
|
252
252
|
task_backend = OpenAIBackend(cfg)
|
|
253
253
|
|
|
254
|
-
is_offline =
|
|
254
|
+
is_offline = (
|
|
255
|
+
not task_backend.is_available
|
|
256
|
+
or getattr(task_backend, "is_offline", False) is True
|
|
257
|
+
or isinstance(task_backend, LocalBackend)
|
|
258
|
+
or is_local_endpoint(getattr(cfg, "base_url", ""))
|
|
259
|
+
)
|
|
255
260
|
session.is_offline = is_offline
|
|
256
261
|
# ?? Dream-RSI Trajectory Recorder ??
|
|
257
262
|
trajectory_store = TrajectoryStore()
|
|
@@ -530,7 +530,20 @@ class PromptBuilder:
|
|
|
530
530
|
"""Build system prompt, optionally enhanced with TaskContext discovery."""
|
|
531
531
|
if is_offline:
|
|
532
532
|
prompt = OFFLINE_SYSTEM_PROMPT
|
|
533
|
-
|
|
533
|
+
if task_context is not None:
|
|
534
|
+
shell = getattr(task_context, "shell", "bash")
|
|
535
|
+
if shell == "powershell":
|
|
536
|
+
prompt += "\n\n" + POWERSHELL_RULES
|
|
537
|
+
elif shell == "wsl":
|
|
538
|
+
prompt += "\n\n" + WSL_RULES
|
|
539
|
+
elif shell == "bash":
|
|
540
|
+
prompt += "\n\n" + BASH_RULES
|
|
541
|
+
memories = format_memories_for_prompt(workdir, max_entries=2, max_chars=250)
|
|
542
|
+
if memories:
|
|
543
|
+
prompt += f"\n\n{memories}"
|
|
544
|
+
return prompt
|
|
545
|
+
|
|
546
|
+
if complexity == "high":
|
|
534
547
|
prompt = SYSTEM_PROMPT_HIGH
|
|
535
548
|
else:
|
|
536
549
|
prompt = SYSTEM_PROMPT
|
|
@@ -119,7 +119,13 @@ class PromptRephraser:
|
|
|
119
119
|
return task
|
|
120
120
|
|
|
121
121
|
# In offline/local GGUF mode, skip redundant rephraser LLM pass to ensure instant turn-0 start
|
|
122
|
-
|
|
122
|
+
is_local = (
|
|
123
|
+
type(backend).__name__ == "LocalBackend"
|
|
124
|
+
or getattr(backend, "is_offline", None) is True
|
|
125
|
+
or getattr(backend, "is_local", None) is True
|
|
126
|
+
or any(h in str(getattr(backend, "base_url", "")).lower() for h in ("127.0.0.1", "localhost", "0.0.0.0", "offline"))
|
|
127
|
+
)
|
|
128
|
+
if is_local:
|
|
123
129
|
return task
|
|
124
130
|
|
|
125
131
|
try:
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
from codeagent.config import Config
|
|
4
|
+
from codeagent.backends.openai_backend import _HTTPXClient, OpenAIBackend
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_httpx_client_uses_http1_for_local_bridge():
|
|
8
|
+
"""Verify that localhost/127.0.0.1 uses an unbuffered HTTP/1.1 client for llama-server."""
|
|
9
|
+
client_local = _HTTPXClient("http://127.0.0.1:8080/v1", "not-needed")
|
|
10
|
+
# http2 is disabled for local server
|
|
11
|
+
assert client_local.http_client._transport._pool._http2 is False or not getattr(client_local.http_client, "_http2", True)
|
|
12
|
+
|
|
13
|
+
client_remote = _HTTPXClient("https://test.169pi.ai/v1", "not-needed")
|
|
14
|
+
assert client_remote.http_client is not None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_force_no_thinking_flag_on_config():
|
|
18
|
+
"""Verify that force_no_thinking flag defaults to False and can be set to True."""
|
|
19
|
+
cfg = Config()
|
|
20
|
+
assert cfg.force_no_thinking is False
|
|
21
|
+
|
|
22
|
+
cfg_no_think = Config(force_no_thinking=True)
|
|
23
|
+
assert cfg_no_think.force_no_thinking is True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_openai_backend_suppresses_think_in_non_thinking_mode():
|
|
27
|
+
"""Verify that OpenAIBackend suppresses think tokens when enable_thinking=False."""
|
|
28
|
+
cfg = Config(base_url="http://127.0.0.1:8080/v1")
|
|
29
|
+
backend = OpenAIBackend(cfg)
|
|
30
|
+
|
|
31
|
+
# Mock response chunks containing a <think> block
|
|
32
|
+
class MockDelta:
|
|
33
|
+
def __init__(self, content=None, tool_calls=None):
|
|
34
|
+
self.content = content
|
|
35
|
+
self.tool_calls = tool_calls
|
|
36
|
+
|
|
37
|
+
class MockChoice:
|
|
38
|
+
def __init__(self, delta):
|
|
39
|
+
self.delta = delta
|
|
40
|
+
|
|
41
|
+
class MockChunk:
|
|
42
|
+
def __init__(self, content):
|
|
43
|
+
self.choices = [MockChoice(MockDelta(content=content))]
|
|
44
|
+
|
|
45
|
+
mock_stream = [
|
|
46
|
+
MockChunk("<think>"),
|
|
47
|
+
MockChunk("Reasoning about prime numbers..."),
|
|
48
|
+
MockChunk("</think>"),
|
|
49
|
+
MockChunk("def is_prime(n):"),
|
|
50
|
+
MockChunk(" return n > 1"),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
mock_client = MagicMock()
|
|
54
|
+
mock_client.chat.completions.create.return_value = mock_stream
|
|
55
|
+
backend._client = mock_client
|
|
56
|
+
backend._resolved_model = "alpie_9b"
|
|
57
|
+
|
|
58
|
+
events = list(backend.chat_completion_stream(
|
|
59
|
+
messages=[{"role": "user", "content": "Write a prime checker"}],
|
|
60
|
+
enable_thinking=False,
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
# In non-thinking mode, thinking_start and thinking_delta must NOT be emitted
|
|
64
|
+
event_types = [e[0] for e in events]
|
|
65
|
+
assert "thinking_start" not in event_types
|
|
66
|
+
assert "thinking_delta" not in event_types
|
|
67
|
+
|
|
68
|
+
# Only real content tokens must be emitted
|
|
69
|
+
token_events = [e[1]["delta"] for e in events if e[0] == "token"]
|
|
70
|
+
assert "".join(token_events).strip() == "def is_prime(n): return n > 1"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_android_backend_offline_bridge_routing():
|
|
74
|
+
"""Verify that AndroidBackend routes to bridge when available."""
|
|
75
|
+
from codeagent.backends.local_backend import AndroidBackend
|
|
76
|
+
cfg = Config(base_url="offline")
|
|
77
|
+
backend = AndroidBackend(cfg)
|
|
78
|
+
|
|
79
|
+
# When bridge is online and llama_cpp is absent (Android), is_available is True
|
|
80
|
+
with patch.dict("sys.modules", {"llama_cpp": None}):
|
|
81
|
+
with patch.object(backend, "_is_bridge_online", return_value=True):
|
|
82
|
+
assert backend.is_available is True
|
|
83
|
+
with patch.object(backend, "_bridge_chat_completion_stream", return_value=iter([("token", {"delta": "hello"})])):
|
|
84
|
+
res = list(backend.chat_completion_stream([{"role": "user", "content": "hi"}]))
|
|
85
|
+
assert len(res) == 1
|
|
86
|
+
assert res[0] == ("token", {"delta": "hello"})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_is_local_endpoint():
|
|
90
|
+
from codeagent.config import is_local_endpoint
|
|
91
|
+
assert is_local_endpoint("http://127.0.0.1:8080/v1") is True
|
|
92
|
+
assert is_local_endpoint("http://localhost:8080/v1") is True
|
|
93
|
+
assert is_local_endpoint("http://0.0.0.0:8000") is True
|
|
94
|
+
assert is_local_endpoint("offline") is True
|
|
95
|
+
assert is_local_endpoint("https://test.169pi.ai/v1") is False
|
|
96
|
+
assert is_local_endpoint("https://api.openai.com/v1") is False
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_openai_backend_is_offline_property():
|
|
100
|
+
cfg_local = Config(base_url="http://127.0.0.1:8080/v1")
|
|
101
|
+
b_local = OpenAIBackend(cfg_local)
|
|
102
|
+
assert b_local.is_local is True
|
|
103
|
+
assert b_local.is_offline is True
|
|
104
|
+
|
|
105
|
+
cfg_remote = Config(base_url="https://test.169pi.ai/v1")
|
|
106
|
+
b_remote = OpenAIBackend(cfg_remote)
|
|
107
|
+
assert b_remote.is_local is False
|
|
108
|
+
assert b_remote.is_offline is False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_rephraser_bypasses_for_local_backend():
|
|
112
|
+
from codeagent.rephraser import PromptRephraser
|
|
113
|
+
rephraser = PromptRephraser()
|
|
114
|
+
cfg = Config(base_url="http://127.0.0.1:8080/v1")
|
|
115
|
+
backend = OpenAIBackend(cfg)
|
|
116
|
+
|
|
117
|
+
# Calling rephrase should return original task immediately without calling backend.chat_completion
|
|
118
|
+
backend.chat_completion = MagicMock(side_effect=Exception("Should not be called!"))
|
|
119
|
+
raw_task = "Write a bash script to monitor memory usage"
|
|
120
|
+
res = rephraser.rephrase(raw_task, backend)
|
|
121
|
+
assert res == raw_task
|
|
122
|
+
backend.chat_completion.assert_not_called()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_prompt_builder_lean_offline_prompt():
|
|
126
|
+
from pathlib import Path
|
|
127
|
+
from codeagent.prompt import PromptBuilder, OFFLINE_SYSTEM_PROMPT
|
|
128
|
+
from codeagent.discovery import build_task_context
|
|
129
|
+
|
|
130
|
+
pb = PromptBuilder()
|
|
131
|
+
ctx = build_task_context("Build a full stack web app with auth", Path("."))
|
|
132
|
+
# When offline, prompt must be OFFLINE_SYSTEM_PROMPT (lean) and NOT include GOAL_DRIVEN_RULES
|
|
133
|
+
prompt = pb.build_system_prompt(Path("."), is_offline=True, complexity="high", task_context=ctx)
|
|
134
|
+
assert "You are running in OFFLINE mode" in prompt
|
|
135
|
+
assert "Execution & Goal Convergence Rules" not in prompt
|
|
136
|
+
assert len(prompt) < 3500 # Lean offline prompt under 3500 chars (~600 tokens vs 4500 cloud tokens)
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from unittest.mock import MagicMock, patch
|
|
3
|
-
from codeagent.config import Config
|
|
4
|
-
from codeagent.backends.openai_backend import _HTTPXClient, OpenAIBackend
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def test_httpx_client_uses_http1_for_local_bridge():
|
|
8
|
-
"""Verify that localhost/127.0.0.1 uses an unbuffered HTTP/1.1 client for llama-server."""
|
|
9
|
-
client_local = _HTTPXClient("http://127.0.0.1:8080/v1", "not-needed")
|
|
10
|
-
# http2 is disabled for local server
|
|
11
|
-
assert client_local.http_client._transport._pool._http2 is False or not getattr(client_local.http_client, "_http2", True)
|
|
12
|
-
|
|
13
|
-
client_remote = _HTTPXClient("https://test.169pi.ai/v1", "not-needed")
|
|
14
|
-
assert client_remote.http_client is not None
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_force_no_thinking_flag_on_config():
|
|
18
|
-
"""Verify that force_no_thinking flag defaults to False and can be set to True."""
|
|
19
|
-
cfg = Config()
|
|
20
|
-
assert cfg.force_no_thinking is False
|
|
21
|
-
|
|
22
|
-
cfg_no_think = Config(force_no_thinking=True)
|
|
23
|
-
assert cfg_no_think.force_no_thinking is True
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_openai_backend_suppresses_think_in_non_thinking_mode():
|
|
27
|
-
"""Verify that OpenAIBackend suppresses think tokens when enable_thinking=False."""
|
|
28
|
-
cfg = Config(base_url="http://127.0.0.1:8080/v1")
|
|
29
|
-
backend = OpenAIBackend(cfg)
|
|
30
|
-
|
|
31
|
-
# Mock response chunks containing a <think> block
|
|
32
|
-
class MockDelta:
|
|
33
|
-
def __init__(self, content=None, tool_calls=None):
|
|
34
|
-
self.content = content
|
|
35
|
-
self.tool_calls = tool_calls
|
|
36
|
-
|
|
37
|
-
class MockChoice:
|
|
38
|
-
def __init__(self, delta):
|
|
39
|
-
self.delta = delta
|
|
40
|
-
|
|
41
|
-
class MockChunk:
|
|
42
|
-
def __init__(self, content):
|
|
43
|
-
self.choices = [MockChoice(MockDelta(content=content))]
|
|
44
|
-
|
|
45
|
-
mock_stream = [
|
|
46
|
-
MockChunk("<think>"),
|
|
47
|
-
MockChunk("Reasoning about prime numbers..."),
|
|
48
|
-
MockChunk("</think>"),
|
|
49
|
-
MockChunk("def is_prime(n):"),
|
|
50
|
-
MockChunk(" return n > 1"),
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
mock_client = MagicMock()
|
|
54
|
-
mock_client.chat.completions.create.return_value = mock_stream
|
|
55
|
-
backend._client = mock_client
|
|
56
|
-
backend._resolved_model = "alpie_9b"
|
|
57
|
-
|
|
58
|
-
events = list(backend.chat_completion_stream(
|
|
59
|
-
messages=[{"role": "user", "content": "Write a prime checker"}],
|
|
60
|
-
enable_thinking=False,
|
|
61
|
-
))
|
|
62
|
-
|
|
63
|
-
# In non-thinking mode, thinking_start and thinking_delta must NOT be emitted
|
|
64
|
-
event_types = [e[0] for e in events]
|
|
65
|
-
assert "thinking_start" not in event_types
|
|
66
|
-
assert "thinking_delta" not in event_types
|
|
67
|
-
|
|
68
|
-
# Only real content tokens must be emitted
|
|
69
|
-
token_events = [e[1]["delta"] for e in events if e[0] == "token"]
|
|
70
|
-
assert "".join(token_events).strip() == "def is_prime(n): return n > 1"
|
|
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
|
|
File without changes
|
|
File without changes
|