alpiecode 0.7.2__tar.gz → 0.8.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-0.7.2 → alpiecode-0.8.0}/PKG-INFO +5 -1
- {alpiecode-0.7.2 → alpiecode-0.8.0}/pyproject.toml +8 -1
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/alpiecode.egg-info/PKG-INFO +5 -1
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/alpiecode.egg-info/SOURCES.txt +11 -1
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/alpiecode.egg-info/requires.txt +5 -0
- alpiecode-0.8.0/src/codeagent/agent.py +331 -0
- alpiecode-0.8.0/src/codeagent/backends/__init__.py +1 -0
- alpiecode-0.8.0/src/codeagent/backends/base.py +61 -0
- alpiecode-0.8.0/src/codeagent/backends/local_backend.py +96 -0
- alpiecode-0.8.0/src/codeagent/backends/openai_backend.py +85 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/cli.py +12 -1
- alpiecode-0.8.0/src/codeagent/context.py +79 -0
- alpiecode-0.8.0/src/codeagent/executor.py +214 -0
- alpiecode-0.8.0/src/codeagent/orchestrator.py +181 -0
- alpiecode-0.8.0/src/codeagent/prompt.py +377 -0
- alpiecode-0.8.0/src/codeagent/server.py +183 -0
- alpiecode-0.8.0/src/codeagent/session.py +76 -0
- alpiecode-0.7.2/src/codeagent/agent.py +0 -1060
- {alpiecode-0.7.2 → alpiecode-0.8.0}/README.md +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/setup.cfg +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/__init__.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/compaction.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/config.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/github.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/guardian.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/local_model.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/media.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/memory.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/tools.py +0 -0
- {alpiecode-0.7.2 → alpiecode-0.8.0}/src/codeagent/updater.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
@@ -12,3 +12,7 @@ Requires-Dist: duckduckgo-search>=7.0.0
|
|
|
12
12
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
13
13
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
14
14
|
Requires-Dist: imageio-ffmpeg>=0.4.0
|
|
15
|
+
Provides-Extra: server
|
|
16
|
+
Requires-Dist: fastapi>=0.100.0; extra == "server"
|
|
17
|
+
Requires-Dist: uvicorn>=0.20.0; extra == "server"
|
|
18
|
+
Requires-Dist: sse-starlette>=2.0.0; extra == "server"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "alpiecode"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
|
|
9
9
|
requires-python = ">=3.9"
|
|
10
10
|
dependencies = [
|
|
@@ -19,6 +19,13 @@ dependencies = [
|
|
|
19
19
|
"imageio-ffmpeg>=0.4.0",
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
server = [
|
|
24
|
+
"fastapi>=0.100.0",
|
|
25
|
+
"uvicorn>=0.20.0",
|
|
26
|
+
"sse-starlette>=2.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
22
29
|
[project.scripts]
|
|
23
30
|
alpiecode = "codeagent.cli:main"
|
|
24
31
|
codeagent = "codeagent.cli:main"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alpiecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
|
|
5
5
|
Requires-Python: >=3.9
|
|
6
6
|
Requires-Dist: huggingface_hub>=0.20.0
|
|
@@ -12,3 +12,7 @@ Requires-Dist: duckduckgo-search>=7.0.0
|
|
|
12
12
|
Requires-Dist: beautifulsoup4>=4.12.0
|
|
13
13
|
Requires-Dist: yt-dlp>=2024.0.0
|
|
14
14
|
Requires-Dist: imageio-ffmpeg>=0.4.0
|
|
15
|
+
Provides-Extra: server
|
|
16
|
+
Requires-Dist: fastapi>=0.100.0; extra == "server"
|
|
17
|
+
Requires-Dist: uvicorn>=0.20.0; extra == "server"
|
|
18
|
+
Requires-Dist: sse-starlette>=2.0.0; extra == "server"
|
|
@@ -11,10 +11,20 @@ src/codeagent/agent.py
|
|
|
11
11
|
src/codeagent/cli.py
|
|
12
12
|
src/codeagent/compaction.py
|
|
13
13
|
src/codeagent/config.py
|
|
14
|
+
src/codeagent/context.py
|
|
15
|
+
src/codeagent/executor.py
|
|
14
16
|
src/codeagent/github.py
|
|
15
17
|
src/codeagent/guardian.py
|
|
16
18
|
src/codeagent/local_model.py
|
|
17
19
|
src/codeagent/media.py
|
|
18
20
|
src/codeagent/memory.py
|
|
21
|
+
src/codeagent/orchestrator.py
|
|
22
|
+
src/codeagent/prompt.py
|
|
23
|
+
src/codeagent/server.py
|
|
24
|
+
src/codeagent/session.py
|
|
19
25
|
src/codeagent/tools.py
|
|
20
|
-
src/codeagent/updater.py
|
|
26
|
+
src/codeagent/updater.py
|
|
27
|
+
src/codeagent/backends/__init__.py
|
|
28
|
+
src/codeagent/backends/base.py
|
|
29
|
+
src/codeagent/backends/local_backend.py
|
|
30
|
+
src/codeagent/backends/openai_backend.py
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI agent wrapper and presentation adapter for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Delegates agent orchestration to AgentOrchestrator and handles Rich terminal UI output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from .config import Config, is_server_reachable
|
|
13
|
+
from .context import ContextManager, _serialize_assistant_message
|
|
14
|
+
from .executor import READ_ONLY_TOOLS, ToolExecutor, parse_text_tool_calls as _parse_text_tool_calls
|
|
15
|
+
from .orchestrator import AgentEvent, AgentOrchestrator, resolve_backend
|
|
16
|
+
from .prompt import (
|
|
17
|
+
OFFLINE_SYSTEM_PROMPT,
|
|
18
|
+
OFFLINE_TOOLS,
|
|
19
|
+
SYSTEM_PROMPT,
|
|
20
|
+
PromptBuilder,
|
|
21
|
+
is_simple_task as _is_simple_task,
|
|
22
|
+
)
|
|
23
|
+
from .session import SessionManager
|
|
24
|
+
|
|
25
|
+
# Re-export for backward compatibility
|
|
26
|
+
_build_system_prompt = lambda workdir, is_offline=False: PromptBuilder().build_system_prompt(workdir, is_offline)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Rich console setup ────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from rich.console import Console
|
|
33
|
+
from rich.markdown import Markdown
|
|
34
|
+
from rich.panel import Panel
|
|
35
|
+
from rich.rule import Rule
|
|
36
|
+
from rich.text import Text
|
|
37
|
+
|
|
38
|
+
console = Console()
|
|
39
|
+
HAS_RICH = True
|
|
40
|
+
except ImportError:
|
|
41
|
+
HAS_RICH = False
|
|
42
|
+
|
|
43
|
+
class _FallbackConsole:
|
|
44
|
+
def print(self, *args, **kwargs):
|
|
45
|
+
kwargs.pop("style", None)
|
|
46
|
+
kwargs.pop("highlight", None)
|
|
47
|
+
print(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
def rule(self, title="", **kwargs):
|
|
50
|
+
print(f"\n{'─' * 20} {title} {'─' * 20}")
|
|
51
|
+
|
|
52
|
+
console = _FallbackConsole()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _print_reasoning(reasoning: str):
|
|
56
|
+
if not reasoning or not reasoning.strip():
|
|
57
|
+
return
|
|
58
|
+
if HAS_RICH:
|
|
59
|
+
text = Text(reasoning.strip(), style="dim italic")
|
|
60
|
+
console.print(Panel(text, title="💭 Thinking", border_style="dim blue", padding=(0, 1)))
|
|
61
|
+
else:
|
|
62
|
+
console.print(f"\n💭 Thinking: {reasoning.strip()}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _print_tool_call(turn: int, name: str, args: dict):
|
|
66
|
+
display_args = {}
|
|
67
|
+
for k, v in args.items():
|
|
68
|
+
if isinstance(v, str) and len(v) > 200:
|
|
69
|
+
display_args[k] = v[:200] + "..."
|
|
70
|
+
else:
|
|
71
|
+
display_args[k] = v
|
|
72
|
+
if HAS_RICH:
|
|
73
|
+
args_str = json.dumps(display_args, indent=2)
|
|
74
|
+
console.print(f"\n🔧 [bold cyan]Tool:[/bold cyan] [bold]{name}[/bold]", highlight=False)
|
|
75
|
+
console.print(f" {args_str}", style="cyan", highlight=False)
|
|
76
|
+
else:
|
|
77
|
+
console.print(f"\n🔧 Tool: {name}({display_args})")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _print_tool_result(result: str):
|
|
81
|
+
truncated = result[:1500] + ("..." if len(result) > 1500 else "")
|
|
82
|
+
if HAS_RICH:
|
|
83
|
+
console.print(f" → {truncated}", style="green", highlight=False)
|
|
84
|
+
else:
|
|
85
|
+
console.print(f" → {truncated}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _print_assistant_message(content: str):
|
|
89
|
+
if HAS_RICH:
|
|
90
|
+
try:
|
|
91
|
+
md = Markdown(content)
|
|
92
|
+
console.print(Panel(md, title="🤖 Assistant", border_style="green", padding=(0, 1)))
|
|
93
|
+
except Exception:
|
|
94
|
+
console.print(Panel(content, title="🤖 Assistant", border_style="green", padding=(0, 1)))
|
|
95
|
+
else:
|
|
96
|
+
console.print(f"\n🤖 Assistant: {content}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Git helpers ───────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def _is_safe_git_dir(workdir: Path) -> bool:
|
|
102
|
+
try:
|
|
103
|
+
home = Path.home().resolve()
|
|
104
|
+
wd = workdir.resolve()
|
|
105
|
+
if wd == home or wd == Path("/") or wd == Path("C:\\"):
|
|
106
|
+
return False
|
|
107
|
+
try:
|
|
108
|
+
items = list(wd.iterdir())
|
|
109
|
+
if len(items) > 500:
|
|
110
|
+
return False
|
|
111
|
+
except PermissionError:
|
|
112
|
+
return False
|
|
113
|
+
except Exception:
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _ensure_git(workdir: Path) -> None:
|
|
119
|
+
if not _is_safe_git_dir(workdir):
|
|
120
|
+
return
|
|
121
|
+
if not (workdir / ".git").exists():
|
|
122
|
+
try:
|
|
123
|
+
subprocess.run(["git", "init"], cwd=workdir, capture_output=True, timeout=10)
|
|
124
|
+
subprocess.run(["git", "add", "-A"], cwd=workdir, capture_output=True, timeout=30)
|
|
125
|
+
subprocess.run(["git", "commit", "-m", "initial commit", "--allow-empty"], cwd=workdir, capture_output=True, timeout=10)
|
|
126
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
127
|
+
pass
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _checkpoint(workdir: Path, message: str) -> None:
|
|
131
|
+
if not (workdir / ".git").exists():
|
|
132
|
+
return
|
|
133
|
+
try:
|
|
134
|
+
subprocess.run(["git", "add", "-A"], cwd=workdir, capture_output=True, timeout=30)
|
|
135
|
+
subprocess.run(["git", "commit", "-m", message, "--allow-empty"], cwd=workdir, capture_output=True, timeout=10)
|
|
136
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── Main CLI Agent Functions ──────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def run_agent(
|
|
143
|
+
task: str,
|
|
144
|
+
workdir: Path,
|
|
145
|
+
cfg: Config,
|
|
146
|
+
verbose: bool = True,
|
|
147
|
+
image_path: str = None,
|
|
148
|
+
video_path: str = None,
|
|
149
|
+
url: str = None,
|
|
150
|
+
github_repo: str = None,
|
|
151
|
+
) -> list:
|
|
152
|
+
"""Run non-interactive agent task with Rich presentation."""
|
|
153
|
+
workdir = workdir.resolve()
|
|
154
|
+
_ensure_git(workdir)
|
|
155
|
+
|
|
156
|
+
backend = resolve_backend(cfg)
|
|
157
|
+
orchestrator = AgentOrchestrator(backend)
|
|
158
|
+
session_mgr = SessionManager()
|
|
159
|
+
session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
|
|
160
|
+
|
|
161
|
+
_checkpoint(workdir, "checkpoint: start")
|
|
162
|
+
|
|
163
|
+
current_turn = 0
|
|
164
|
+
for event in orchestrator.run_task(
|
|
165
|
+
session=session,
|
|
166
|
+
task=task,
|
|
167
|
+
cfg=cfg,
|
|
168
|
+
image_path=image_path,
|
|
169
|
+
video_path=video_path,
|
|
170
|
+
url=url,
|
|
171
|
+
github_repo=github_repo,
|
|
172
|
+
):
|
|
173
|
+
if event.type == "start" and verbose:
|
|
174
|
+
data = event.data
|
|
175
|
+
if HAS_RICH:
|
|
176
|
+
console.rule("[bold blue]Agent Started[/bold blue]")
|
|
177
|
+
console.print(f"📋 Task: {task.splitlines()[0]}", style="bold")
|
|
178
|
+
if github_repo:
|
|
179
|
+
console.print(f"🐙 GitHub Repo: {github_repo}", style="cyan")
|
|
180
|
+
if image_path:
|
|
181
|
+
console.print(f"🖼️ Image: {image_path}", style="cyan")
|
|
182
|
+
if video_path:
|
|
183
|
+
console.print(f"🎬 Video: {video_path}", style="cyan")
|
|
184
|
+
if url:
|
|
185
|
+
console.print(f"📺 URL: {url}", style="cyan")
|
|
186
|
+
console.print(f"📂 Workdir: {workdir}", style="dim")
|
|
187
|
+
if not data["is_offline"]:
|
|
188
|
+
console.print(f"🌐 Mode: [bold green]ONLINE[/bold green] (Server: {cfg.base_url})", style="dim")
|
|
189
|
+
console.print(f"🤖 Model: {cfg.model}", style="dim")
|
|
190
|
+
else:
|
|
191
|
+
console.print(f"🧠 Mode: [bold yellow]OFFLINE[/bold yellow] (Local GGUF GPU Engine)", style="dim")
|
|
192
|
+
console.print(f"🧠 Local Model: {cfg.model_repo}", style="dim")
|
|
193
|
+
console.print(f"⚡ Context Window: {cfg.n_ctx} tokens", style="dim")
|
|
194
|
+
console.print(f"🧠 Reasoning: {'ON' if cfg.enable_thinking else 'OFF'}", style="dim")
|
|
195
|
+
console.print(f"🔧 Tools: {data['tool_count']} available", style="dim")
|
|
196
|
+
else:
|
|
197
|
+
console.rule("Agent Started")
|
|
198
|
+
console.print(f"📋 Task: {task.splitlines()[0]}")
|
|
199
|
+
console.print(f"📂 Workdir: {workdir}")
|
|
200
|
+
|
|
201
|
+
elif event.type == "adaptive_mode" and verbose and HAS_RICH:
|
|
202
|
+
console.print("⚡ [dim]Adaptive mode: simple task detected, skipping deep reasoning for speed[/dim]")
|
|
203
|
+
|
|
204
|
+
elif event.type == "turn_start":
|
|
205
|
+
current_turn = event.data["turn"]
|
|
206
|
+
if verbose:
|
|
207
|
+
if HAS_RICH:
|
|
208
|
+
console.rule(f"[bold]Turn {current_turn}[/bold]", style="blue")
|
|
209
|
+
else:
|
|
210
|
+
console.rule(f"Turn {current_turn}")
|
|
211
|
+
|
|
212
|
+
elif event.type == "compaction" and verbose:
|
|
213
|
+
console.print("🗜️ Compacting context (approaching token limit)...", style="yellow")
|
|
214
|
+
|
|
215
|
+
elif event.type == "thinking" and verbose:
|
|
216
|
+
_print_reasoning(event.data["content"])
|
|
217
|
+
|
|
218
|
+
elif event.type == "tool_call" and verbose:
|
|
219
|
+
_print_tool_call(event.data["turn"], event.data["name"], event.data["arguments"])
|
|
220
|
+
|
|
221
|
+
elif event.type == "tool_result":
|
|
222
|
+
if verbose:
|
|
223
|
+
_print_tool_result(event.data["content"])
|
|
224
|
+
_checkpoint(workdir, f"checkpoint: turn {event.data['turn']}")
|
|
225
|
+
|
|
226
|
+
elif event.type == "message" and verbose:
|
|
227
|
+
_print_assistant_message(event.data["content"])
|
|
228
|
+
_checkpoint(workdir, "checkpoint: response")
|
|
229
|
+
|
|
230
|
+
elif event.type == "fallback" and verbose:
|
|
231
|
+
if HAS_RICH:
|
|
232
|
+
console.print(f"\n⚠️ [bold yellow]Online Server Error / Timeout[/bold yellow] ({event.data['error']})", style="yellow")
|
|
233
|
+
console.print("🔄 [bold cyan]Auto-falling back to local GGUF engine...[/bold cyan]", style="cyan")
|
|
234
|
+
else:
|
|
235
|
+
print(f"\n⚠️ Online Server Error: {event.data['error']}")
|
|
236
|
+
print("🔄 Auto-falling back to local GGUF engine...")
|
|
237
|
+
|
|
238
|
+
elif event.type == "error" and verbose:
|
|
239
|
+
if HAS_RICH:
|
|
240
|
+
console.print(f"\n❌ [bold red]Model Error[/bold red]\n Error: {event.data['error']}\n")
|
|
241
|
+
else:
|
|
242
|
+
print(f"\n❌ Model Error: {event.data['error']}")
|
|
243
|
+
|
|
244
|
+
elif event.type == "done":
|
|
245
|
+
summary = event.data["summary"]
|
|
246
|
+
_checkpoint(workdir, "checkpoint: done")
|
|
247
|
+
if verbose and HAS_RICH:
|
|
248
|
+
if "DONE" in summary.upper():
|
|
249
|
+
console.rule("[bold green]✅ Task Complete[/bold green]")
|
|
250
|
+
else:
|
|
251
|
+
console.rule("[bold yellow]💬 Agent Replied[/bold yellow]")
|
|
252
|
+
|
|
253
|
+
elif event.type == "max_turns_reached" and verbose:
|
|
254
|
+
console.print(f"\n⚠️ Max turns ({event.data['max_turns']}) reached without completion.", style="bold yellow")
|
|
255
|
+
|
|
256
|
+
return session.context.messages
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
260
|
+
"""Run interactive chat session with Rich presentation."""
|
|
261
|
+
workdir = workdir.resolve()
|
|
262
|
+
_ensure_git(workdir)
|
|
263
|
+
|
|
264
|
+
backend = resolve_backend(cfg)
|
|
265
|
+
orchestrator = AgentOrchestrator(backend)
|
|
266
|
+
session_mgr = SessionManager()
|
|
267
|
+
session = session_mgr.create_session(workdir, max_tokens=cfg.n_ctx if not backend.is_available else 262_144)
|
|
268
|
+
|
|
269
|
+
is_offline = not backend.is_available or isinstance(backend, type(resolve_backend(cfg)))
|
|
270
|
+
active_tools = PromptBuilder().get_tools(is_offline=is_offline)
|
|
271
|
+
|
|
272
|
+
if HAS_RICH:
|
|
273
|
+
console.print()
|
|
274
|
+
console.print(
|
|
275
|
+
Panel(
|
|
276
|
+
"[bold cyan]AlpieCode[/bold cyan] interactive mode\n"
|
|
277
|
+
f"📂 Working in: [cyan]{workdir}[/cyan]\n"
|
|
278
|
+
+ (f"🌐 Mode: [bold green]ONLINE[/bold green] (Server: {cfg.base_url})\n" if not is_offline else "🧠 Mode: [bold yellow]OFFLINE[/bold yellow] (Local GGUF Engine)\n")
|
|
279
|
+
+ f"🔧 Tools: [cyan]{len(active_tools)} available[/cyan]\n\n"
|
|
280
|
+
"Type your request, or [bold red]exit[/bold red] / [bold red]quit[/bold red] to stop.",
|
|
281
|
+
title="💬 Chat Mode",
|
|
282
|
+
border_style="blue",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
else:
|
|
286
|
+
console.print("\n💬 Chat Mode — type your request, or 'exit' to stop.")
|
|
287
|
+
console.print(f"📂 Working in: {workdir}")
|
|
288
|
+
|
|
289
|
+
while True:
|
|
290
|
+
try:
|
|
291
|
+
if HAS_RICH:
|
|
292
|
+
user_input = console.input("\n[bold green]You ❯[/bold green] ").strip()
|
|
293
|
+
else:
|
|
294
|
+
user_input = input("\nYou ❯ ").strip()
|
|
295
|
+
except (EOFError, KeyboardInterrupt):
|
|
296
|
+
console.print("\nGoodbye! 👋")
|
|
297
|
+
break
|
|
298
|
+
|
|
299
|
+
if not user_input:
|
|
300
|
+
continue
|
|
301
|
+
if user_input.lower() in ("exit", "quit", "q"):
|
|
302
|
+
console.print("Goodbye! 👋")
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
for event in orchestrator.run_task(session, user_input, cfg):
|
|
306
|
+
if event.type == "turn_start" and verbose:
|
|
307
|
+
if HAS_RICH:
|
|
308
|
+
console.rule(f"[bold]Turn {event.data['turn']}[/bold]", style="blue")
|
|
309
|
+
else:
|
|
310
|
+
console.rule(f"Turn {event.data['turn']}")
|
|
311
|
+
|
|
312
|
+
elif event.type == "thinking" and verbose:
|
|
313
|
+
_print_reasoning(event.data["content"])
|
|
314
|
+
|
|
315
|
+
elif event.type == "tool_call" and verbose:
|
|
316
|
+
_print_tool_call(event.data["turn"], event.data["name"], event.data["arguments"])
|
|
317
|
+
|
|
318
|
+
elif event.type == "tool_result":
|
|
319
|
+
if verbose:
|
|
320
|
+
_print_tool_result(event.data["content"])
|
|
321
|
+
_checkpoint(workdir, f"checkpoint: chat turn {event.data['turn']}")
|
|
322
|
+
|
|
323
|
+
elif event.type == "message":
|
|
324
|
+
_print_assistant_message(event.data["content"])
|
|
325
|
+
_checkpoint(workdir, "checkpoint: done")
|
|
326
|
+
|
|
327
|
+
elif event.type == "error":
|
|
328
|
+
console.print(f"❌ Model error: {event.data['error']}", style="bold red" if HAS_RICH else None)
|
|
329
|
+
|
|
330
|
+
elif event.type == "done":
|
|
331
|
+
break
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Inference backends package for AlpieCode."""
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inference backend interface for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Defines the contract (Protocol) and dataclasses for all inference providers
|
|
5
|
+
(OpenAI-compatible server, local GGUF model, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, List, Optional, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ToolCall:
|
|
14
|
+
"""Normalized tool call representation."""
|
|
15
|
+
id: str
|
|
16
|
+
name: str
|
|
17
|
+
arguments: dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ChatResponse:
|
|
22
|
+
"""Normalized inference response structure."""
|
|
23
|
+
content: Optional[str]
|
|
24
|
+
reasoning: Optional[str]
|
|
25
|
+
tool_calls: Optional[List[ToolCall]]
|
|
26
|
+
raw: Any = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class InferenceBackend(Protocol):
|
|
31
|
+
"""Contract that all inference backends must implement."""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
"""Human-readable backend name."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def is_available(self) -> bool:
|
|
40
|
+
"""Whether the backend is available/reachable."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def context_window(self) -> int:
|
|
45
|
+
"""Maximum context window in tokens."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
def chat_completion(
|
|
49
|
+
self,
|
|
50
|
+
messages: List[dict],
|
|
51
|
+
tools: Optional[List[dict]] = None,
|
|
52
|
+
temperature: float = 0.2,
|
|
53
|
+
max_tokens: int = 4096,
|
|
54
|
+
enable_thinking: bool = True,
|
|
55
|
+
) -> ChatResponse:
|
|
56
|
+
"""Execute chat completion and return a normalized ChatResponse."""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def shutdown(self) -> None:
|
|
60
|
+
"""Clean up resources."""
|
|
61
|
+
...
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local GGUF model inference backend.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from ..config import Config
|
|
8
|
+
from ..local_model import LocalModel
|
|
9
|
+
from .base import ChatResponse, ToolCall
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalBackend:
|
|
13
|
+
"""Backend for local GGUF model inference via llama-cpp-python."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cfg: Config):
|
|
16
|
+
self._cfg = cfg
|
|
17
|
+
self._model: Optional[LocalModel] = None
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return f"Local GGUF ({self._cfg.model_repo})"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
return True # Local model is always available (downloads if missing)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def context_window(self) -> int:
|
|
29
|
+
return self._cfg.n_ctx
|
|
30
|
+
|
|
31
|
+
def _ensure_model(self) -> LocalModel:
|
|
32
|
+
if self._model is None:
|
|
33
|
+
self._model = LocalModel(
|
|
34
|
+
repo_id=self._cfg.model_repo,
|
|
35
|
+
n_ctx=self._cfg.n_ctx,
|
|
36
|
+
n_gpu_layers=self._cfg.n_gpu_layers,
|
|
37
|
+
token=self._cfg.hf_token,
|
|
38
|
+
)
|
|
39
|
+
return self._model
|
|
40
|
+
|
|
41
|
+
def load_model(self) -> None:
|
|
42
|
+
"""Explicitly load model into VRAM/RAM (warmup)."""
|
|
43
|
+
model = self._ensure_model()
|
|
44
|
+
model.load()
|
|
45
|
+
|
|
46
|
+
def chat_completion(
|
|
47
|
+
self,
|
|
48
|
+
messages: List[dict],
|
|
49
|
+
tools: Optional[List[dict]] = None,
|
|
50
|
+
temperature: float = 0.2,
|
|
51
|
+
max_tokens: int = 2048,
|
|
52
|
+
enable_thinking: bool = True,
|
|
53
|
+
) -> ChatResponse:
|
|
54
|
+
model = self._ensure_model()
|
|
55
|
+
resp = model.create_chat_completion(
|
|
56
|
+
messages=messages,
|
|
57
|
+
tools=tools,
|
|
58
|
+
tool_choice="auto",
|
|
59
|
+
temperature=temperature,
|
|
60
|
+
max_tokens=max_tokens,
|
|
61
|
+
enable_thinking=enable_thinking,
|
|
62
|
+
)
|
|
63
|
+
msg = resp.choices[0].message
|
|
64
|
+
|
|
65
|
+
tool_calls = None
|
|
66
|
+
if msg.tool_calls:
|
|
67
|
+
import json
|
|
68
|
+
tool_calls = []
|
|
69
|
+
for tc in msg.tool_calls:
|
|
70
|
+
args = tc.function.arguments
|
|
71
|
+
if isinstance(args, str):
|
|
72
|
+
try:
|
|
73
|
+
args = json.loads(args or "{}")
|
|
74
|
+
except Exception:
|
|
75
|
+
args = {}
|
|
76
|
+
tool_calls.append(
|
|
77
|
+
ToolCall(
|
|
78
|
+
id=tc.id,
|
|
79
|
+
name=tc.function.name,
|
|
80
|
+
arguments=args if isinstance(args, dict) else {},
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
|
|
85
|
+
|
|
86
|
+
return ChatResponse(
|
|
87
|
+
content=msg.content,
|
|
88
|
+
reasoning=reasoning,
|
|
89
|
+
tool_calls=tool_calls,
|
|
90
|
+
raw=resp,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def shutdown(self) -> None:
|
|
94
|
+
if self._model:
|
|
95
|
+
self._model._llm = None
|
|
96
|
+
self._model = None
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI-compatible API inference backend.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
from openai import OpenAI
|
|
7
|
+
|
|
8
|
+
from ..config import Config, get_shared_http_client, is_server_reachable
|
|
9
|
+
from .base import ChatResponse, ToolCall
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenAIBackend:
|
|
13
|
+
"""Backend for remote OpenAI-compatible servers (vLLM, Ollama, etc.)."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, cfg: Config):
|
|
16
|
+
self._cfg = cfg
|
|
17
|
+
self._client: Optional[OpenAI] = None
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return f"Online API ({self._cfg.base_url})"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_available(self) -> bool:
|
|
25
|
+
return is_server_reachable(self._cfg.base_url)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def context_window(self) -> int:
|
|
29
|
+
return 262_144
|
|
30
|
+
|
|
31
|
+
def _ensure_client(self) -> OpenAI:
|
|
32
|
+
if self._client is None:
|
|
33
|
+
self._client = OpenAI(
|
|
34
|
+
base_url=self._cfg.base_url,
|
|
35
|
+
api_key=self._cfg.api_key or "not-needed",
|
|
36
|
+
http_client=get_shared_http_client(),
|
|
37
|
+
)
|
|
38
|
+
return self._client
|
|
39
|
+
|
|
40
|
+
def chat_completion(
|
|
41
|
+
self,
|
|
42
|
+
messages: List[dict],
|
|
43
|
+
tools: Optional[List[dict]] = None,
|
|
44
|
+
temperature: float = 0.2,
|
|
45
|
+
max_tokens: int = 4096,
|
|
46
|
+
enable_thinking: bool = True,
|
|
47
|
+
) -> ChatResponse:
|
|
48
|
+
client = self._ensure_client()
|
|
49
|
+
params = {
|
|
50
|
+
"model": self._cfg.model,
|
|
51
|
+
"messages": messages,
|
|
52
|
+
"temperature": temperature,
|
|
53
|
+
"max_tokens": max_tokens,
|
|
54
|
+
"extra_body": {"chat_template_kwargs": {"enable_thinking": enable_thinking}},
|
|
55
|
+
}
|
|
56
|
+
if tools:
|
|
57
|
+
params["tools"] = tools
|
|
58
|
+
params["tool_choice"] = "auto"
|
|
59
|
+
|
|
60
|
+
resp = client.chat.completions.create(**params)
|
|
61
|
+
msg = resp.choices[0].message
|
|
62
|
+
|
|
63
|
+
tool_calls = None
|
|
64
|
+
if msg.tool_calls:
|
|
65
|
+
import json
|
|
66
|
+
tool_calls = [
|
|
67
|
+
ToolCall(
|
|
68
|
+
id=tc.id,
|
|
69
|
+
name=tc.function.name,
|
|
70
|
+
arguments=json.loads(tc.function.arguments or "{}"),
|
|
71
|
+
)
|
|
72
|
+
for tc in msg.tool_calls
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
reasoning = getattr(msg, "reasoning", None) or getattr(msg, "reasoning_content", None)
|
|
76
|
+
|
|
77
|
+
return ChatResponse(
|
|
78
|
+
content=msg.content,
|
|
79
|
+
reasoning=reasoning,
|
|
80
|
+
tool_calls=tool_calls,
|
|
81
|
+
raw=resp,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def shutdown(self) -> None:
|
|
85
|
+
self._client = None
|