alpiecode 0.7.2__tar.gz → 0.8.1__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.
Files changed (33) hide show
  1. {alpiecode-0.7.2 → alpiecode-0.8.1}/PKG-INFO +5 -1
  2. {alpiecode-0.7.2 → alpiecode-0.8.1}/pyproject.toml +8 -1
  3. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/alpiecode.egg-info/PKG-INFO +5 -1
  4. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/alpiecode.egg-info/SOURCES.txt +11 -1
  5. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/alpiecode.egg-info/requires.txt +5 -0
  6. alpiecode-0.8.1/src/codeagent/agent.py +331 -0
  7. alpiecode-0.8.1/src/codeagent/backends/__init__.py +1 -0
  8. alpiecode-0.8.1/src/codeagent/backends/base.py +61 -0
  9. alpiecode-0.8.1/src/codeagent/backends/local_backend.py +96 -0
  10. alpiecode-0.8.1/src/codeagent/backends/openai_backend.py +85 -0
  11. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/cli.py +12 -1
  12. alpiecode-0.8.1/src/codeagent/context.py +79 -0
  13. alpiecode-0.8.1/src/codeagent/executor.py +214 -0
  14. alpiecode-0.8.1/src/codeagent/orchestrator.py +181 -0
  15. alpiecode-0.8.1/src/codeagent/prompt.py +377 -0
  16. alpiecode-0.8.1/src/codeagent/server.py +183 -0
  17. alpiecode-0.8.1/src/codeagent/session.py +76 -0
  18. alpiecode-0.7.2/src/codeagent/agent.py +0 -1060
  19. {alpiecode-0.7.2 → alpiecode-0.8.1}/README.md +0 -0
  20. {alpiecode-0.7.2 → alpiecode-0.8.1}/setup.cfg +0 -0
  21. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  22. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/alpiecode.egg-info/entry_points.txt +0 -0
  23. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/alpiecode.egg-info/top_level.txt +0 -0
  24. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/__init__.py +0 -0
  25. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/compaction.py +0 -0
  26. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/config.py +0 -0
  27. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/github.py +0 -0
  28. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/guardian.py +0 -0
  29. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/local_model.py +0 -0
  30. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/media.py +0 -0
  31. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/memory.py +0 -0
  32. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/tools.py +0 -0
  33. {alpiecode-0.7.2 → alpiecode-0.8.1}/src/codeagent/updater.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 0.7.2
3
+ Version: 0.8.1
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.2"
7
+ version = "0.8.1"
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.7.2
3
+ Version: 0.8.1
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
@@ -7,3 +7,8 @@ duckduckgo-search>=7.0.0
7
7
  beautifulsoup4>=4.12.0
8
8
  yt-dlp>=2024.0.0
9
9
  imageio-ffmpeg>=0.4.0
10
+
11
+ [server]
12
+ fastapi>=0.100.0
13
+ uvicorn>=0.20.0
14
+ sse-starlette>=2.0.0
@@ -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 = isinstance(backend, LocalBackend) or not backend.is_available
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