alpiecode 0.6.0__py3-none-any.whl
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-0.6.0.dist-info/METADATA +14 -0
- alpiecode-0.6.0.dist-info/RECORD +17 -0
- alpiecode-0.6.0.dist-info/WHEEL +5 -0
- alpiecode-0.6.0.dist-info/entry_points.txt +3 -0
- alpiecode-0.6.0.dist-info/top_level.txt +1 -0
- codeagent/__init__.py +1 -0
- codeagent/agent.py +989 -0
- codeagent/cli.py +215 -0
- codeagent/compaction.py +163 -0
- codeagent/config.py +195 -0
- codeagent/github.py +241 -0
- codeagent/guardian.py +160 -0
- codeagent/local_model.py +460 -0
- codeagent/media.py +286 -0
- codeagent/memory.py +130 -0
- codeagent/tools.py +718 -0
- codeagent/updater.py +126 -0
codeagent/cli.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for AlpieCode.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .config import CONFIG_PATH, interactive_init, load_config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
BANNER = r"""
|
|
14
|
+
_ _ _ ____ _
|
|
15
|
+
/ \ | |_ _| | ___ / ___|___ __| | ___
|
|
16
|
+
/ _ \ | | '_ \ |/ _ \ | / _ \ / _` |/ _ \
|
|
17
|
+
/ ___ \| | |_) | | __/ |__| (_) | (_| | __/
|
|
18
|
+
/_/ \_\_|_.__/|_|\___|\____\___/ \__,_|\___|
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _show_banner():
|
|
23
|
+
try:
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
console = Console()
|
|
26
|
+
console.print(BANNER, style="bold cyan", highlight=False)
|
|
27
|
+
except ImportError:
|
|
28
|
+
print(BANNER)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _normalize_args():
|
|
32
|
+
known_commands = {"init", "run", "chat", "plan", "diff", "-h", "--help", "--version"}
|
|
33
|
+
subcommand = None
|
|
34
|
+
|
|
35
|
+
# Check if a subcommand is present
|
|
36
|
+
for arg in sys.argv[1:]:
|
|
37
|
+
if arg in known_commands:
|
|
38
|
+
subcommand = arg
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
if not subcommand and len(sys.argv) > 1:
|
|
42
|
+
subcommand = "run"
|
|
43
|
+
sys.argv.insert(1, "run")
|
|
44
|
+
|
|
45
|
+
if subcommand in ("run", "plan") and len(sys.argv) > 2:
|
|
46
|
+
cmd_idx = sys.argv.index(subcommand)
|
|
47
|
+
sub_args = sys.argv[cmd_idx + 1:]
|
|
48
|
+
|
|
49
|
+
flags = []
|
|
50
|
+
positionals = []
|
|
51
|
+
|
|
52
|
+
i = 0
|
|
53
|
+
while i < len(sub_args):
|
|
54
|
+
arg = sub_args[i]
|
|
55
|
+
if arg.startswith("-"):
|
|
56
|
+
flags.append(arg)
|
|
57
|
+
if arg in ("--workdir", "--image", "--video", "--url", "--github", "--max-turns") and i + 1 < len(sub_args):
|
|
58
|
+
flags.append(sub_args[i + 1])
|
|
59
|
+
i += 1
|
|
60
|
+
else:
|
|
61
|
+
positionals.append(arg)
|
|
62
|
+
i += 1
|
|
63
|
+
|
|
64
|
+
sys.argv = sys.argv[:cmd_idx + 1] + flags + positionals
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main():
|
|
68
|
+
_normalize_args()
|
|
69
|
+
|
|
70
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
71
|
+
common.add_argument("--workdir", default=".", help="Repo directory (default: current dir)")
|
|
72
|
+
common.add_argument("--image", default=None, help="Path to an image file for vision analysis")
|
|
73
|
+
common.add_argument("--video", default=None, help="Path to a video file for multimodal analysis")
|
|
74
|
+
common.add_argument("--url", default=None, help="YouTube URL for video analysis")
|
|
75
|
+
common.add_argument("--github", default=None, help="GitHub repository (e.g. owner/repo or URL) for open-source analysis")
|
|
76
|
+
common.add_argument("--max-turns", type=int, default=None, help="Override max turns")
|
|
77
|
+
common.add_argument("--no-thinking", "--non-thinking", dest="no_thinking", action="store_true", help="Disable VLM reasoning/thinking mode")
|
|
78
|
+
common.add_argument("--no-update", action="store_true", help="Skip automatic update check")
|
|
79
|
+
common.add_argument("--quiet", action="store_true", help="Suppress per-turn logging")
|
|
80
|
+
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="alpiecode",
|
|
83
|
+
description="AlpieCode — Autonomous AI Coding Agent powered by 169Pi Alpie VLM",
|
|
84
|
+
parents=[common],
|
|
85
|
+
)
|
|
86
|
+
sub = parser.add_subparsers(dest="command")
|
|
87
|
+
|
|
88
|
+
# ── init ──
|
|
89
|
+
sub.add_parser("init", help="Configure your VLM/OpenAI-compatible endpoint")
|
|
90
|
+
|
|
91
|
+
# ── run ──
|
|
92
|
+
run_p = sub.add_parser("run", help="Run a coding task against a repository", parents=[common])
|
|
93
|
+
run_p.add_argument("task", help="Natural-language task description")
|
|
94
|
+
|
|
95
|
+
# ── chat ──
|
|
96
|
+
chat_p = sub.add_parser("chat", help="Interactive chat mode with AlpieCode", parents=[common])
|
|
97
|
+
|
|
98
|
+
# ── plan ──
|
|
99
|
+
plan_p = sub.add_parser("plan", help="Generate a plan without making changes (read-only)", parents=[common])
|
|
100
|
+
plan_p.add_argument("task", help="Natural-language task to plan for")
|
|
101
|
+
|
|
102
|
+
# ── diff ──
|
|
103
|
+
diff_p = sub.add_parser("diff", help="Show changes AlpieCode has made since last checkpoint", parents=[common])
|
|
104
|
+
|
|
105
|
+
args = parser.parse_args()
|
|
106
|
+
|
|
107
|
+
if not args.command:
|
|
108
|
+
parser.print_help()
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if args.command == "init":
|
|
112
|
+
interactive_init()
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Check for auto-updates from GitHub in background
|
|
116
|
+
if not getattr(args, "no_update", False):
|
|
117
|
+
try:
|
|
118
|
+
from .updater import auto_update
|
|
119
|
+
auto_update(quiet=getattr(args, "quiet", False))
|
|
120
|
+
except Exception:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
cfg = load_config()
|
|
124
|
+
|
|
125
|
+
if getattr(args, "no_thinking", False):
|
|
126
|
+
cfg.enable_thinking = False
|
|
127
|
+
|
|
128
|
+
if args.command == "run":
|
|
129
|
+
if args.max_turns:
|
|
130
|
+
cfg.max_turns = args.max_turns
|
|
131
|
+
_show_banner()
|
|
132
|
+
from .agent import run_agent
|
|
133
|
+
run_agent(
|
|
134
|
+
args.task, Path(args.workdir), cfg,
|
|
135
|
+
verbose=not args.quiet,
|
|
136
|
+
image_path=args.image,
|
|
137
|
+
video_path=getattr(args, "video", None),
|
|
138
|
+
url=getattr(args, "url", None),
|
|
139
|
+
github_repo=getattr(args, "github", None),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
elif args.command == "chat":
|
|
143
|
+
if args.max_turns:
|
|
144
|
+
cfg.max_turns = args.max_turns
|
|
145
|
+
_show_banner()
|
|
146
|
+
from .agent import run_chat
|
|
147
|
+
run_chat(Path(args.workdir), cfg, verbose=not args.quiet)
|
|
148
|
+
|
|
149
|
+
elif args.command == "plan":
|
|
150
|
+
_show_banner()
|
|
151
|
+
plan_task = (
|
|
152
|
+
f"PLANNING ONLY — Do NOT make any file edits. "
|
|
153
|
+
f"Analyze the codebase and create a detailed implementation plan for the following task. "
|
|
154
|
+
f"Use list_files, read_file, and file_search to understand the project. "
|
|
155
|
+
f"Then use update_plan to write a structured plan with deliverables and checks. "
|
|
156
|
+
f"Finish with DONE: when the plan is complete.\n\n"
|
|
157
|
+
f"Task: {args.task}"
|
|
158
|
+
)
|
|
159
|
+
from .agent import run_agent
|
|
160
|
+
run_agent(
|
|
161
|
+
plan_task, Path(args.workdir), cfg, verbose=True,
|
|
162
|
+
image_path=args.image,
|
|
163
|
+
video_path=getattr(args, "video", None),
|
|
164
|
+
url=getattr(args, "url", None),
|
|
165
|
+
github_repo=getattr(args, "github", None),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
elif args.command == "diff":
|
|
169
|
+
workdir = Path(args.workdir).resolve()
|
|
170
|
+
# Show git diff since the first checkpoint
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
["git", "log", "--oneline", "--all"],
|
|
173
|
+
cwd=workdir, capture_output=True, text=True
|
|
174
|
+
)
|
|
175
|
+
if result.returncode != 0:
|
|
176
|
+
print("Not a git repository or no commits found.")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Find the start checkpoint
|
|
180
|
+
log_lines = result.stdout.strip().splitlines()
|
|
181
|
+
start_sha = None
|
|
182
|
+
for line in reversed(log_lines):
|
|
183
|
+
if "checkpoint: start" in line:
|
|
184
|
+
start_sha = line.split()[0]
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
if not start_sha:
|
|
188
|
+
print("No AlpieCode checkpoint found. Run a task first.")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
diff_result = subprocess.run(
|
|
192
|
+
["git", "diff", start_sha, "HEAD", "--stat"],
|
|
193
|
+
cwd=workdir, capture_output=True, text=True
|
|
194
|
+
)
|
|
195
|
+
print(f"Changes since AlpieCode started (from {start_sha}):\n")
|
|
196
|
+
print(diff_result.stdout)
|
|
197
|
+
|
|
198
|
+
# Also show the full diff
|
|
199
|
+
full_diff = subprocess.run(
|
|
200
|
+
["git", "diff", start_sha, "HEAD"],
|
|
201
|
+
cwd=workdir, capture_output=True, text=True
|
|
202
|
+
)
|
|
203
|
+
if full_diff.stdout:
|
|
204
|
+
try:
|
|
205
|
+
from rich.console import Console
|
|
206
|
+
from rich.syntax import Syntax
|
|
207
|
+
console = Console()
|
|
208
|
+
syntax = Syntax(full_diff.stdout, "diff", theme="monokai")
|
|
209
|
+
console.print(syntax)
|
|
210
|
+
except ImportError:
|
|
211
|
+
print(full_diff.stdout)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
main()
|
codeagent/compaction.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context compaction for AlpieCode.
|
|
3
|
+
|
|
4
|
+
When the conversation history approaches the model's context window limit,
|
|
5
|
+
this module summarizes older turns to free up space while preserving
|
|
6
|
+
the essential information needed for the agent to continue working.
|
|
7
|
+
|
|
8
|
+
Strategy:
|
|
9
|
+
- Keep system prompt and last N turns intact
|
|
10
|
+
- Summarize older tool calls and results into compact descriptions
|
|
11
|
+
- Preserve all user messages verbatim
|
|
12
|
+
- Track approximate token count using a simple heuristic (4 chars ≈ 1 token)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from typing import List
|
|
17
|
+
|
|
18
|
+
# Our model's context window
|
|
19
|
+
MAX_CONTEXT_TOKENS = 262_144
|
|
20
|
+
# Start compacting when we hit this percentage of the context window
|
|
21
|
+
COMPACT_THRESHOLD = 0.70
|
|
22
|
+
# Number of recent turns to always keep intact
|
|
23
|
+
KEEP_RECENT_TURNS = 10
|
|
24
|
+
# Approximate chars per token (rough heuristic)
|
|
25
|
+
CHARS_PER_TOKEN = 4
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def estimate_tokens(messages: List[dict]) -> int:
|
|
29
|
+
"""Estimate token count from a list of messages."""
|
|
30
|
+
total_chars = 0
|
|
31
|
+
for msg in messages:
|
|
32
|
+
if isinstance(msg, dict):
|
|
33
|
+
content = msg.get("content") or ""
|
|
34
|
+
if isinstance(content, str):
|
|
35
|
+
total_chars += len(content)
|
|
36
|
+
# Account for tool call arguments
|
|
37
|
+
tool_calls = msg.get("tool_calls", [])
|
|
38
|
+
if tool_calls:
|
|
39
|
+
for tc in tool_calls:
|
|
40
|
+
if isinstance(tc, dict):
|
|
41
|
+
fn = tc.get("function", {})
|
|
42
|
+
total_chars += len(fn.get("arguments", ""))
|
|
43
|
+
total_chars += len(fn.get("name", ""))
|
|
44
|
+
return total_chars // CHARS_PER_TOKEN
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def needs_compaction(messages: List[dict], max_tokens: int = MAX_CONTEXT_TOKENS) -> bool:
|
|
48
|
+
"""Check if the conversation needs compaction."""
|
|
49
|
+
tokens = estimate_tokens(messages)
|
|
50
|
+
return tokens > (max_tokens * COMPACT_THRESHOLD)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _summarize_tool_result(tool_name: str, content: str) -> str:
|
|
54
|
+
"""Create a compact summary of a tool result."""
|
|
55
|
+
if len(content) <= 300:
|
|
56
|
+
return content
|
|
57
|
+
|
|
58
|
+
if tool_name == "bash":
|
|
59
|
+
try:
|
|
60
|
+
data = json.loads(content)
|
|
61
|
+
stdout = data.get("stdout", "")
|
|
62
|
+
stderr = data.get("stderr", "")
|
|
63
|
+
exit_code = data.get("exit_code", -1)
|
|
64
|
+
summary = f"exit_code={exit_code}"
|
|
65
|
+
if stdout:
|
|
66
|
+
summary += f", stdout({len(stdout)} chars): {stdout[:150]}..."
|
|
67
|
+
if stderr:
|
|
68
|
+
summary += f", stderr: {stderr[:100]}..."
|
|
69
|
+
return summary
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
if tool_name in ("read_file", "list_files"):
|
|
74
|
+
lines = content.splitlines()
|
|
75
|
+
if len(lines) > 20:
|
|
76
|
+
return "\n".join(lines[:10]) + f"\n... ({len(lines) - 20} lines omitted) ...\n" + "\n".join(lines[-10:])
|
|
77
|
+
|
|
78
|
+
# Generic truncation
|
|
79
|
+
return content[:250] + f"... ({len(content)} chars total)"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compact_messages(messages: List[dict]) -> List[dict]:
|
|
83
|
+
"""
|
|
84
|
+
Compact a message list by summarizing older turns.
|
|
85
|
+
|
|
86
|
+
Preserves:
|
|
87
|
+
- System prompt (index 0)
|
|
88
|
+
- All user messages (verbatim)
|
|
89
|
+
- Last KEEP_RECENT_TURNS messages (verbatim)
|
|
90
|
+
|
|
91
|
+
Summarizes:
|
|
92
|
+
- Older tool results (truncated)
|
|
93
|
+
- Older assistant reasoning (removed)
|
|
94
|
+
"""
|
|
95
|
+
if len(messages) <= KEEP_RECENT_TURNS + 2:
|
|
96
|
+
return messages
|
|
97
|
+
|
|
98
|
+
# Always keep system prompt
|
|
99
|
+
system = messages[0] if messages and messages[0].get("role") == "system" else None
|
|
100
|
+
|
|
101
|
+
# Split into old and recent
|
|
102
|
+
cutoff = len(messages) - KEEP_RECENT_TURNS
|
|
103
|
+
old_messages = messages[1:cutoff] if system else messages[:cutoff]
|
|
104
|
+
recent_messages = messages[cutoff:]
|
|
105
|
+
|
|
106
|
+
# Build a compacted summary of old messages
|
|
107
|
+
compacted_old = []
|
|
108
|
+
summary_parts = []
|
|
109
|
+
|
|
110
|
+
for msg in old_messages:
|
|
111
|
+
role = msg.get("role", "")
|
|
112
|
+
|
|
113
|
+
if role == "user":
|
|
114
|
+
# Keep user messages verbatim
|
|
115
|
+
compacted_old.append(msg)
|
|
116
|
+
|
|
117
|
+
elif role == "assistant":
|
|
118
|
+
# Compact assistant messages: keep tool calls but remove reasoning
|
|
119
|
+
compact_msg = {"role": "assistant"}
|
|
120
|
+
if msg.get("content"):
|
|
121
|
+
# Truncate long assistant content
|
|
122
|
+
content = msg["content"]
|
|
123
|
+
if len(content) > 200:
|
|
124
|
+
compact_msg["content"] = content[:200] + "..."
|
|
125
|
+
else:
|
|
126
|
+
compact_msg["content"] = content
|
|
127
|
+
else:
|
|
128
|
+
compact_msg["content"] = None
|
|
129
|
+
|
|
130
|
+
if msg.get("tool_calls"):
|
|
131
|
+
compact_msg["tool_calls"] = msg["tool_calls"]
|
|
132
|
+
compacted_old.append(compact_msg)
|
|
133
|
+
|
|
134
|
+
elif role == "tool":
|
|
135
|
+
# Summarize tool results
|
|
136
|
+
tool_call_id = msg.get("tool_call_id", "")
|
|
137
|
+
content = msg.get("content", "")
|
|
138
|
+
|
|
139
|
+
# Try to find the tool name from the preceding assistant message
|
|
140
|
+
tool_name = "unknown"
|
|
141
|
+
for prev in reversed(compacted_old):
|
|
142
|
+
if prev.get("tool_calls"):
|
|
143
|
+
for tc in prev["tool_calls"]:
|
|
144
|
+
tc_dict = tc if isinstance(tc, dict) else {}
|
|
145
|
+
if tc_dict.get("id") == tool_call_id:
|
|
146
|
+
tool_name = tc_dict.get("function", {}).get("name", "unknown")
|
|
147
|
+
break
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
compacted_old.append({
|
|
151
|
+
"role": "tool",
|
|
152
|
+
"tool_call_id": tool_call_id,
|
|
153
|
+
"content": _summarize_tool_result(tool_name, content),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
# Rebuild message list
|
|
157
|
+
result = []
|
|
158
|
+
if system:
|
|
159
|
+
result.append(system)
|
|
160
|
+
result.extend(compacted_old)
|
|
161
|
+
result.extend(recent_messages)
|
|
162
|
+
|
|
163
|
+
return result
|
codeagent/config.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-user config for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Resolution order (highest priority first):
|
|
5
|
+
1. Environment variables: HF_TOKEN, ALPIECODE_MODEL_REPO, etc.
|
|
6
|
+
2. ~/.alpiecode/config.json (written by `alpiecode init`)
|
|
7
|
+
3. Built-in defaults (Local GGUF model: 169Pi/Alpie_learn_prototype_GGUF_NEW)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import socket
|
|
13
|
+
from dataclasses import dataclass, asdict, fields
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
CONFIG_DIR = Path.home() / ".alpiecode"
|
|
18
|
+
CONFIG_PATH = CONFIG_DIR / "config.json"
|
|
19
|
+
|
|
20
|
+
# Config version — bump this when defaults change to trigger auto-migration
|
|
21
|
+
CONFIG_VERSION = 2 # v2: n_ctx upgraded 16384 → 32768
|
|
22
|
+
|
|
23
|
+
DEFAULTS = {
|
|
24
|
+
"base_url": "http://20.245.200.125:8000/v1", # Remote VLM server endpoint
|
|
25
|
+
"model": "169Pi/grpo_phase_2_merged",
|
|
26
|
+
"model_repo": "169Pi/Alpie_learn_prototype_GGUF_NEW",
|
|
27
|
+
"api_key": "not-needed",
|
|
28
|
+
"hf_token": None,
|
|
29
|
+
"max_turns": 30,
|
|
30
|
+
"temperature": 0.2,
|
|
31
|
+
"max_tokens": 16384,
|
|
32
|
+
"enable_thinking": True,
|
|
33
|
+
"n_ctx": 32768, # 32k context window (model supports up to 131k)
|
|
34
|
+
"n_gpu_layers": None, # None = auto-detect GPU
|
|
35
|
+
"config_version": CONFIG_VERSION,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_server_reachable(base_url: Optional[str], timeout: float = 0.4) -> bool:
|
|
40
|
+
"""Fast network ping check (0.4s max) to see if server endpoint is online."""
|
|
41
|
+
if not base_url:
|
|
42
|
+
return False
|
|
43
|
+
try:
|
|
44
|
+
from urllib.parse import urlparse
|
|
45
|
+
parsed = urlparse(base_url)
|
|
46
|
+
host = parsed.hostname
|
|
47
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
48
|
+
if not host:
|
|
49
|
+
return False
|
|
50
|
+
with socket.create_connection((host, port), timeout=timeout):
|
|
51
|
+
return True
|
|
52
|
+
except Exception:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def is_internet_available(timeout: float = 1.0) -> bool:
|
|
57
|
+
"""Quick check if general internet is available (ping Google DNS)."""
|
|
58
|
+
try:
|
|
59
|
+
with socket.create_connection(("8.8.8.8", 53), timeout=timeout):
|
|
60
|
+
return True
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Config:
|
|
67
|
+
base_url: Optional[str] = None
|
|
68
|
+
model: str = "169Pi/grpo_phase_2_merged" # Server API model name (vLLM)
|
|
69
|
+
model_repo: str = "169Pi/Alpie_learn_prototype_GGUF_NEW" # HuggingFace repo for offline GGUF
|
|
70
|
+
api_key: str = "not-needed"
|
|
71
|
+
hf_token: Optional[str] = None
|
|
72
|
+
max_turns: int = 30
|
|
73
|
+
temperature: float = 0.2
|
|
74
|
+
max_tokens: int = 16384
|
|
75
|
+
enable_thinking: bool = True
|
|
76
|
+
n_ctx: int = 32768
|
|
77
|
+
n_gpu_layers: Optional[int] = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_config() -> Config:
|
|
81
|
+
data = dict(DEFAULTS)
|
|
82
|
+
needs_save = False
|
|
83
|
+
if CONFIG_PATH.exists():
|
|
84
|
+
try:
|
|
85
|
+
saved_data = json.loads(CONFIG_PATH.read_text())
|
|
86
|
+
# Skip null values so they don't override smart defaults (e.g. base_url)
|
|
87
|
+
data.update({k: v for k, v in saved_data.items() if v is not None})
|
|
88
|
+
|
|
89
|
+
# ── Auto-migrate stale configs ────────────────────────────
|
|
90
|
+
saved_version = saved_data.get("config_version", 1)
|
|
91
|
+
if saved_version < CONFIG_VERSION:
|
|
92
|
+
# v1 → v2: n_ctx was 16384, upgrade to 32768
|
|
93
|
+
if saved_data.get("n_ctx") == 16384:
|
|
94
|
+
data["n_ctx"] = 32768
|
|
95
|
+
data["config_version"] = CONFIG_VERSION
|
|
96
|
+
needs_save = True
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# env vars override saved file
|
|
101
|
+
for prefix in ("ALPIECODE_", "CODEAGENT_"):
|
|
102
|
+
if os.environ.get(f"{prefix}BASE_URL"):
|
|
103
|
+
data["base_url"] = os.environ[f"{prefix}BASE_URL"]
|
|
104
|
+
if os.environ.get(f"{prefix}MODEL"):
|
|
105
|
+
data["model"] = os.environ[f"{prefix}MODEL"]
|
|
106
|
+
if os.environ.get(f"{prefix}MODEL_REPO"):
|
|
107
|
+
data["model_repo"] = os.environ[f"{prefix}MODEL_REPO"]
|
|
108
|
+
if os.environ.get(f"{prefix}API_KEY"):
|
|
109
|
+
data["api_key"] = os.environ[f"{prefix}API_KEY"]
|
|
110
|
+
if os.environ.get("HF_TOKEN"):
|
|
111
|
+
data["hf_token"] = os.environ["HF_TOKEN"]
|
|
112
|
+
if os.environ.get("ALPIECODE_CPU") == "1":
|
|
113
|
+
data["n_gpu_layers"] = 0
|
|
114
|
+
elif os.environ.get("ALPIECODE_GPU_LAYERS"):
|
|
115
|
+
try:
|
|
116
|
+
data["n_gpu_layers"] = int(os.environ["ALPIECODE_GPU_LAYERS"])
|
|
117
|
+
except ValueError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# Filter data to only keys present in Config fields
|
|
121
|
+
valid_keys = {f.name for f in fields(Config)}
|
|
122
|
+
filtered_data = {k: v for k, v in data.items() if k in valid_keys}
|
|
123
|
+
cfg = Config(**filtered_data)
|
|
124
|
+
|
|
125
|
+
# Auto-save migrated config
|
|
126
|
+
if needs_save:
|
|
127
|
+
try:
|
|
128
|
+
save_config(cfg)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
return cfg
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def save_config(cfg: Config) -> None:
|
|
136
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
CONFIG_PATH.write_text(json.dumps(asdict(cfg), indent=2))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def interactive_init() -> Config:
|
|
141
|
+
current = load_config()
|
|
142
|
+
print("AlpieCode Setup — Local GGUF Model Configuration\n")
|
|
143
|
+
|
|
144
|
+
print("AlpieCode uses the 169Pi/Alpie_learn_prototype_GGUF_NEW model from HuggingFace.")
|
|
145
|
+
print("Please enter your HuggingFace user access token (required to download model).\n")
|
|
146
|
+
|
|
147
|
+
# Mask token for display (show first 3 + last 3 chars only)
|
|
148
|
+
if current.hf_token:
|
|
149
|
+
t = current.hf_token
|
|
150
|
+
masked = f"{t[:3]}***{t[-3:]}" if len(t) > 6 else "***"
|
|
151
|
+
else:
|
|
152
|
+
masked = "none"
|
|
153
|
+
token_prompt = f"HuggingFace Token [{masked}]: "
|
|
154
|
+
hf_token = input(token_prompt).strip() or current.hf_token
|
|
155
|
+
|
|
156
|
+
repo_prompt = f"Model Repo [{current.model_repo}]: "
|
|
157
|
+
model_repo = input(repo_prompt).strip() or current.model_repo
|
|
158
|
+
|
|
159
|
+
cfg = Config(
|
|
160
|
+
base_url=current.base_url,
|
|
161
|
+
model=current.model,
|
|
162
|
+
model_repo=model_repo,
|
|
163
|
+
api_key=current.api_key,
|
|
164
|
+
hf_token=hf_token,
|
|
165
|
+
max_turns=current.max_turns,
|
|
166
|
+
temperature=current.temperature,
|
|
167
|
+
max_tokens=current.max_tokens,
|
|
168
|
+
enable_thinking=current.enable_thinking,
|
|
169
|
+
n_ctx=current.n_ctx,
|
|
170
|
+
n_gpu_layers=current.n_gpu_layers,
|
|
171
|
+
)
|
|
172
|
+
save_config(cfg)
|
|
173
|
+
print(f"\n✅ Config saved to {CONFIG_PATH}")
|
|
174
|
+
|
|
175
|
+
# Trigger model download test if token provided
|
|
176
|
+
try:
|
|
177
|
+
from .local_model import download_model
|
|
178
|
+
print("\n📥 Testing HuggingFace model download...")
|
|
179
|
+
local_path = download_model(repo_id=cfg.model_repo, token=cfg.hf_token)
|
|
180
|
+
print(f"✅ Model downloaded & cached at: {local_path}")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
print(f"⚠️ Could not download model right now: {e}")
|
|
183
|
+
print(" Model will be downloaded automatically on first task execution.")
|
|
184
|
+
|
|
185
|
+
# Pre-install llama-cpp-python binary wheel (so offline mode works immediately)
|
|
186
|
+
try:
|
|
187
|
+
from .local_model import _ensure_llama_cpp
|
|
188
|
+
print("\n⚙️ Setting up offline GGUF engine...")
|
|
189
|
+
_ensure_llama_cpp()
|
|
190
|
+
print("✅ Offline mode ready — works without internet from now on!")
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"⚠️ Could not setup offline engine: {e}")
|
|
193
|
+
print(" It will be installed automatically when needed.")
|
|
194
|
+
|
|
195
|
+
return cfg
|