Spaces:
Running on Zero
Running on Zero
atakan Claude Sonnet 5 commited on
Commit ·
6be46a5
1
Parent(s): cb61511
fix: Load the actual fine-tuned ControlAI model on HF Spaces, not stock Qwen2.5
Browse filesThe Linux/Docker inference path was hardcoded to download and run the
public Qwen/Qwen2.5-3B-Instruct-GGUF model, completely bypassing the
fine-tuned LoRA/adapter. It now loads atakankahya/ControlAI-Agent
(the real fused-and-quantized model, freshly re-built and re-uploaded
after the previous fused checkpoint turned out to be an MLX-only
quantized artifact that standard tooling couldn't read).
Also:
- Cap llama-cpp-python's build parallelism in the Dockerfile so the
Spaces build machine stops getting OOM-killed during pip install.
- Fix cli.py's PROJECT_ROOT path (it was resolving one directory above
the actual project root, copy-pasted from the nested package copy).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Dockerfile +5 -1
- cli.py +62 -144
- controlai_agent/orchestrator.py +17 -9
Dockerfile
CHANGED
|
@@ -23,8 +23,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
| 23 |
RUN useradd -m -u 1000 user
|
| 24 |
WORKDIR /home/user/app
|
| 25 |
|
| 26 |
-
# Install Python dependencies
|
|
|
|
|
|
|
| 27 |
COPY requirements.txt .
|
|
|
|
|
|
|
| 28 |
RUN pip install --no-cache-dir --upgrade pip && \
|
| 29 |
pip install --no-cache-dir -r requirements.txt
|
| 30 |
|
|
|
|
| 23 |
RUN useradd -m -u 1000 user
|
| 24 |
WORKDIR /home/user/app
|
| 25 |
|
| 26 |
+
# Install Python dependencies. llama-cpp-python compiles from source (no
|
| 27 |
+
# prebuilt wheel is published for recent versions); cap build parallelism so
|
| 28 |
+
# the compiler doesn't spawn enough jobs to OOM-kill the Spaces build machine.
|
| 29 |
COPY requirements.txt .
|
| 30 |
+
ENV CMAKE_BUILD_PARALLEL_LEVEL=1 \
|
| 31 |
+
CMAKE_ARGS="-DGGML_NATIVE=OFF"
|
| 32 |
RUN pip install --no-cache-dir --upgrade pip && \
|
| 33 |
pip install --no-cache-dir -r requirements.txt
|
| 34 |
|
cli.py
CHANGED
|
@@ -1,25 +1,11 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
|
| 4 |
-
Run standalone in terminal:
|
| 5 |
-
python cli.py
|
| 6 |
-
Or single query:
|
| 7 |
-
python cli.py "Design an LQR controller for A=[[0, 1], [-2, -3]], B=[[0], [1]]"
|
| 8 |
-
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
| 12 |
import argparse
|
| 13 |
-
import os
|
| 14 |
import sys
|
| 15 |
from pathlib import Path
|
| 16 |
-
from typing import Any
|
| 17 |
-
|
| 18 |
-
from rich.console import Console
|
| 19 |
-
from rich.markdown import Markdown
|
| 20 |
-
from rich.panel import Panel
|
| 21 |
-
from rich.prompt import Prompt
|
| 22 |
-
from rich.text import Text
|
| 23 |
|
| 24 |
PROJECT_ROOT = Path(__file__).resolve().parent
|
| 25 |
if str(PROJECT_ROOT) not in sys.path:
|
|
@@ -27,142 +13,74 @@ if str(PROJECT_ROOT) not in sys.path:
|
|
| 27 |
|
| 28 |
from controlai_agent.orchestrator import ControlAIAgent
|
| 29 |
|
| 30 |
-
console = Console()
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def print_banner() -> None:
|
| 34 |
-
banner_text = Text()
|
| 35 |
-
banner_text.append("ControlAI Terminal Assistant\n", style="bold cyan")
|
| 36 |
-
banner_text.append("Control Theory • Computational Python • Mathematical Simulation\n", style="dim white")
|
| 37 |
-
banner_text.append("Commands: ", style="bold")
|
| 38 |
-
banner_text.append("/clear ", style="yellow")
|
| 39 |
-
banner_text.append("(new session) • ", style="dim")
|
| 40 |
-
banner_text.append("/exit ", style="yellow")
|
| 41 |
-
banner_text.append("(quit) • ", style="dim")
|
| 42 |
-
banner_text.append("/help", style="yellow")
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
while True:
|
| 52 |
try:
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
except (KeyboardInterrupt, EOFError):
|
| 56 |
-
|
| 57 |
-
break
|
| 58 |
-
|
| 59 |
-
if not user_input:
|
| 60 |
-
continue
|
| 61 |
-
|
| 62 |
-
if user_input.lower() in ("/exit", "/quit", "exit", "quit"):
|
| 63 |
-
console.print("[dim]Goodbye![/dim]")
|
| 64 |
break
|
| 65 |
|
| 66 |
-
|
| 67 |
-
history.clear()
|
| 68 |
-
console.clear()
|
| 69 |
-
print_banner()
|
| 70 |
-
console.print("[yellow]Started a new chat session.[/yellow]")
|
| 71 |
-
continue
|
| 72 |
-
|
| 73 |
-
if user_input.lower() in ("/help", "help"):
|
| 74 |
-
console.print(Panel(
|
| 75 |
-
"• Type your question or control engineering prompt directly.\n"
|
| 76 |
-
"• Ask to simulate ODEs, discretize state space matrices, or design LQR.\n"
|
| 77 |
-
"• Plots are saved automatically to outputs/plots/.\n"
|
| 78 |
-
"• /clear - Clear session history.\n"
|
| 79 |
-
"• /exit - Quit the terminal session.",
|
| 80 |
-
title="Help & Guide",
|
| 81 |
-
border_style="blue",
|
| 82 |
-
))
|
| 83 |
-
continue
|
| 84 |
-
|
| 85 |
-
# Run streaming agent response
|
| 86 |
-
console.print()
|
| 87 |
-
console.print("[bold cyan]ControlAI[/bold cyan]")
|
| 88 |
-
|
| 89 |
-
accumulated_text = ""
|
| 90 |
-
plots_generated = []
|
| 91 |
-
|
| 92 |
-
try:
|
| 93 |
-
for event in agent.run_stream(user_input, history=history):
|
| 94 |
-
etype = event.get("type")
|
| 95 |
-
|
| 96 |
-
if etype == "thought":
|
| 97 |
-
content = event.get("content", "")
|
| 98 |
-
console.print(f" [dim cyan]› {content}[/dim cyan]")
|
| 99 |
-
|
| 100 |
-
elif etype == "token":
|
| 101 |
-
content = event.get("content", "")
|
| 102 |
-
accumulated_text += content
|
| 103 |
-
sys.stdout.write(content)
|
| 104 |
-
sys.stdout.flush()
|
| 105 |
-
|
| 106 |
-
elif etype == "plot":
|
| 107 |
-
plot_url = event.get("url", "")
|
| 108 |
-
plots_generated.append(plot_url)
|
| 109 |
-
console.print(f"\n [bold green]📊 Plot saved:[/bold green] [underline white]{plot_url}[/underline white]")
|
| 110 |
-
|
| 111 |
-
elif etype == "done":
|
| 112 |
-
final_resp = event.get("response", "")
|
| 113 |
-
if not accumulated_text:
|
| 114 |
-
accumulated_text = final_resp
|
| 115 |
-
console.print(Markdown(final_resp))
|
| 116 |
-
|
| 117 |
-
sys.stdout.write("\n")
|
| 118 |
-
sys.stdout.flush()
|
| 119 |
-
|
| 120 |
-
# Record history
|
| 121 |
-
history.append({"role": "user", "content": user_input})
|
| 122 |
-
history.append({"role": "assistant", "content": accumulated_text})
|
| 123 |
-
|
| 124 |
-
except Exception as err:
|
| 125 |
-
console.print(f"[bold red]Error:[/bold red] {err}")
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def run_oneshot(agent: ControlAIAgent, prompt: str) -> None:
|
| 129 |
-
accumulated = ""
|
| 130 |
-
for event in agent.run_stream(prompt):
|
| 131 |
-
etype = event.get("type")
|
| 132 |
-
if etype == "thought":
|
| 133 |
-
console.print(f"[dim cyan]› {event.get('content')}[/dim cyan]")
|
| 134 |
-
elif etype == "token":
|
| 135 |
-
content = event.get("content", "")
|
| 136 |
-
accumulated += content
|
| 137 |
-
sys.stdout.write(content)
|
| 138 |
-
sys.stdout.flush()
|
| 139 |
-
elif etype == "plot":
|
| 140 |
-
console.print(f"\n[bold green]📊 Plot saved:[/bold green] {event.get('url')}")
|
| 141 |
-
elif etype == "done":
|
| 142 |
-
if not accumulated:
|
| 143 |
-
console.print(Markdown(event.get("response", "")))
|
| 144 |
-
|
| 145 |
-
sys.stdout.write("\n")
|
| 146 |
-
sys.stdout.flush()
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
def main() -> None:
|
| 150 |
-
parser = argparse.ArgumentParser(description="ControlAI Terminal Client")
|
| 151 |
-
parser.add_argument("query", nargs="*", help="Optional one-shot query string")
|
| 152 |
-
parser.add_argument("--adapter", type=str, default=None, help="Path to LoRA adapter")
|
| 153 |
-
parser.add_argument("--model", type=str, default="mlx-community/Qwen3-4B-Instruct-2507-4bit", help="Model path")
|
| 154 |
-
|
| 155 |
-
args = parser.parse_args()
|
| 156 |
-
|
| 157 |
-
with console.status("[bold cyan]Loading ControlAI Engine on Apple Silicon (MLX)...[/bold cyan]"):
|
| 158 |
-
agent = ControlAIAgent(model_path=args.model, adapter_path=args.adapter)
|
| 159 |
-
|
| 160 |
-
if args.query:
|
| 161 |
-
query_str = " ".join(args.query)
|
| 162 |
-
run_oneshot(agent, query_str)
|
| 163 |
-
else:
|
| 164 |
-
run_interactive(agent)
|
| 165 |
|
| 166 |
|
| 167 |
if __name__ == "__main__":
|
| 168 |
-
main()
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""Interactive CLI for ControlAI Agent with deterministic tool calling."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
| 6 |
import argparse
|
|
|
|
| 7 |
import sys
|
| 8 |
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
PROJECT_ROOT = Path(__file__).resolve().parent
|
| 11 |
if str(PROJECT_ROOT) not in sys.path:
|
|
|
|
| 13 |
|
| 14 |
from controlai_agent.orchestrator import ControlAIAgent
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
def main() -> int:
|
| 18 |
+
parser = argparse.ArgumentParser(description="ControlAI Offline Engineering Agent CLI")
|
| 19 |
+
parser.add_argument(
|
| 20 |
+
"--model",
|
| 21 |
+
type=str,
|
| 22 |
+
default="mlx-community/Qwen3-4B-Instruct-2507-4bit",
|
| 23 |
+
help="Base model path or HuggingFace repo",
|
| 24 |
+
)
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"--adapter-path",
|
| 27 |
+
type=str,
|
| 28 |
+
default=None,
|
| 29 |
+
help="Optional fine-tuned LoRA adapter path",
|
| 30 |
+
)
|
| 31 |
+
parser.add_argument(
|
| 32 |
+
"--prompt",
|
| 33 |
+
type=str,
|
| 34 |
+
default=None,
|
| 35 |
+
help="Single prompt to execute in non-interactive mode",
|
| 36 |
+
)
|
| 37 |
+
parser.add_argument(
|
| 38 |
+
"--verbose",
|
| 39 |
+
action="store_true",
|
| 40 |
+
help="Print raw tool calls and intermediate steps",
|
| 41 |
+
)
|
| 42 |
+
args = parser.parse_args()
|
| 43 |
|
| 44 |
+
print("Initializing ControlAI Agent...")
|
| 45 |
+
agent = ControlAIAgent(model_path=args.model, adapter_path=args.adapter_path)
|
| 46 |
+
print("ControlAI Agent initialized with deterministic control tools.")
|
| 47 |
+
|
| 48 |
+
if args.prompt:
|
| 49 |
+
result = agent.run(args.prompt, verbose=args.verbose)
|
| 50 |
+
print("\n" + "=" * 50)
|
| 51 |
+
print("AGENT RESPONSE:")
|
| 52 |
+
print("=" * 50)
|
| 53 |
+
print(result.final_response)
|
| 54 |
+
if result.tool_traces:
|
| 55 |
+
print("\n" + "-" * 50)
|
| 56 |
+
print(f"EXECUTED TOOLS ({len(result.tool_traces)}):")
|
| 57 |
+
for trace in result.tool_traces:
|
| 58 |
+
print(f" * {trace.tool_name}({trace.arguments}) -> {trace.result.get('status', 'done')}")
|
| 59 |
+
return 0
|
| 60 |
+
|
| 61 |
+
print("\n" + "=" * 60)
|
| 62 |
+
print("🤖 Welcome to Control-LLM (Offline Control Engineering Agent)")
|
| 63 |
+
print("=" * 60)
|
| 64 |
+
print("Type your control engineering question or design problem.")
|
| 65 |
+
print("Commands: 'exit' or 'quit' to end, 'verbose' to toggle tool details.\n")
|
| 66 |
while True:
|
| 67 |
try:
|
| 68 |
+
user_input = input("\n[Engineer] > ").strip()
|
| 69 |
+
if not user_input:
|
| 70 |
+
continue
|
| 71 |
+
if user_input.lower() in ("exit", "quit", "q"):
|
| 72 |
+
break
|
| 73 |
+
result = agent.run(user_input, verbose=args.verbose)
|
| 74 |
+
print(f"\n[ControlAI]\n{result.final_response}")
|
| 75 |
+
if result.tool_traces and not args.verbose:
|
| 76 |
+
tools_used = ", ".join(t.tool_name for t in result.tool_traces)
|
| 77 |
+
print(f"\n(Verified using tools: {tools_used})")
|
| 78 |
except (KeyboardInterrupt, EOFError):
|
| 79 |
+
print("\nExiting ControlAI.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
break
|
| 81 |
|
| 82 |
+
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
if __name__ == "__main__":
|
| 86 |
+
raise SystemExit(main())
|
controlai_agent/orchestrator.py
CHANGED
|
@@ -183,27 +183,33 @@ class ControlAIAgent:
|
|
| 183 |
self.hf_tokenizer = AutoTokenizer.from_pretrained(model_path)
|
| 184 |
else:
|
| 185 |
# Universal Linux / Cloud / HuggingFace Spaces backend: Fast 4-bit C++ GGUF
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
self.llama_model = None
|
| 187 |
if HAS_LLAMA_CPP:
|
| 188 |
try:
|
| 189 |
threads = min(os.cpu_count() or 2, 4)
|
| 190 |
-
print(f"Loading high-speed GGUF Q4_K_M
|
| 191 |
self.llama_model = llama_cpp.Llama.from_pretrained(
|
| 192 |
-
repo_id=
|
| 193 |
-
filename=
|
| 194 |
-
n_ctx=
|
| 195 |
n_threads=threads,
|
| 196 |
verbose=False,
|
| 197 |
)
|
| 198 |
self.is_gguf = True
|
| 199 |
-
self.hf_tokenizer = AutoTokenizer.from_pretrained(
|
| 200 |
-
print("GGUF Q4_K_M model loaded successfully via llama_cpp.")
|
| 201 |
except Exception as exc:
|
| 202 |
print(f"Notice: llama_cpp GGUF auto-load failed, falling back to PyTorch: {exc}")
|
| 203 |
self.is_gguf = False
|
| 204 |
|
| 205 |
if not self.is_gguf:
|
| 206 |
-
# PyTorch fallback on CUDA or if GGUF is
|
|
|
|
| 207 |
import torch
|
| 208 |
num_threads = min(os.cpu_count() or 2, 4)
|
| 209 |
try:
|
|
@@ -211,7 +217,7 @@ class ControlAIAgent:
|
|
| 211 |
except Exception:
|
| 212 |
pass
|
| 213 |
|
| 214 |
-
hf_id =
|
| 215 |
print(f"Loading PyTorch model: {hf_id} (threads: {num_threads})...")
|
| 216 |
self.hf_tokenizer = AutoTokenizer.from_pretrained(hf_id, trust_remote_code=True)
|
| 217 |
dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
|
|
@@ -222,7 +228,9 @@ class ControlAIAgent:
|
|
| 222 |
device_map="auto",
|
| 223 |
trust_remote_code=True,
|
| 224 |
)
|
| 225 |
-
|
|
|
|
|
|
|
| 226 |
try:
|
| 227 |
from peft import PeftModel
|
| 228 |
self.model = PeftModel.from_pretrained(self.model, adapter_path)
|
|
|
|
| 183 |
self.hf_tokenizer = AutoTokenizer.from_pretrained(model_path)
|
| 184 |
else:
|
| 185 |
# Universal Linux / Cloud / HuggingFace Spaces backend: Fast 4-bit C++ GGUF
|
| 186 |
+
# of OUR OWN fine-tuned ControlAI model (never a generic base model).
|
| 187 |
+
CONTROLAI_HF_REPO = "atakankahya/ControlAI-Agent"
|
| 188 |
+
gguf_repo = os.environ.get("CONTROLAI_GGUF_REPO", CONTROLAI_HF_REPO)
|
| 189 |
+
gguf_filename = os.environ.get("CONTROLAI_GGUF_FILENAME", "*controlai-q4_k_m.gguf")
|
| 190 |
+
|
| 191 |
self.llama_model = None
|
| 192 |
if HAS_LLAMA_CPP:
|
| 193 |
try:
|
| 194 |
threads = min(os.cpu_count() or 2, 4)
|
| 195 |
+
print(f"Loading high-speed GGUF Q4_K_M ControlAI model from {gguf_repo} (threads: {threads})...")
|
| 196 |
self.llama_model = llama_cpp.Llama.from_pretrained(
|
| 197 |
+
repo_id=gguf_repo,
|
| 198 |
+
filename=gguf_filename,
|
| 199 |
+
n_ctx=4096,
|
| 200 |
n_threads=threads,
|
| 201 |
verbose=False,
|
| 202 |
)
|
| 203 |
self.is_gguf = True
|
| 204 |
+
self.hf_tokenizer = AutoTokenizer.from_pretrained(CONTROLAI_HF_REPO, trust_remote_code=True)
|
| 205 |
+
print("GGUF Q4_K_M ControlAI model loaded successfully via llama_cpp.")
|
| 206 |
except Exception as exc:
|
| 207 |
print(f"Notice: llama_cpp GGUF auto-load failed, falling back to PyTorch: {exc}")
|
| 208 |
self.is_gguf = False
|
| 209 |
|
| 210 |
if not self.is_gguf:
|
| 211 |
+
# PyTorch fallback on CUDA or if the GGUF engine is unavailable.
|
| 212 |
+
# Always our own fine-tuned model, never a generic base model.
|
| 213 |
import torch
|
| 214 |
num_threads = min(os.cpu_count() or 2, 4)
|
| 215 |
try:
|
|
|
|
| 217 |
except Exception:
|
| 218 |
pass
|
| 219 |
|
| 220 |
+
hf_id = CONTROLAI_HF_REPO if "mlx" in str(model_path) or str(model_path).startswith("Qwen/") else model_path
|
| 221 |
print(f"Loading PyTorch model: {hf_id} (threads: {num_threads})...")
|
| 222 |
self.hf_tokenizer = AutoTokenizer.from_pretrained(hf_id, trust_remote_code=True)
|
| 223 |
dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32
|
|
|
|
| 228 |
device_map="auto",
|
| 229 |
trust_remote_code=True,
|
| 230 |
)
|
| 231 |
+
# The fused ControlAI model already has the LoRA weights merged in;
|
| 232 |
+
# only apply a separate adapter when loading a plain base model.
|
| 233 |
+
if hf_id != CONTROLAI_HF_REPO and adapter_path and Path(adapter_path).exists():
|
| 234 |
try:
|
| 235 |
from peft import PeftModel
|
| 236 |
self.model = PeftModel.from_pretrained(self.model, adapter_path)
|