atakan commited on
Commit
3e59ef3
·
1 Parent(s): 9936912

feat: Add universal PyTorch/Transformers backend for Linux and Google Colab

Browse files
ControlAI_Colab_Demo.ipynb CHANGED
@@ -7,13 +7,10 @@
7
  "# ControlAI: Open-Source Safety-Critical AI Agent for Control Systems\n",
8
  "\n",
9
  "[![GitHub Repo](https://img.shields.io/badge/GitHub-atakankahya%2Fcontrolai--agent-blue?logo=github)](https://github.com/atakankahya/controlai-agent)\n",
10
- "[![Hugging Face Model](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-ControlAI--Agent-yellow)](https://huggingface.co/atakankahya/ControlAI-Agent)\n",
11
  "[![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)\n",
12
  "\n",
13
- "**ControlAI** is an open-source, safety-oriented AI agent specialized in Control Systems Engineering, Applied Mathematics, and Dynamical Systems Simulation. Unlike black-box general LLMs that hallucinate stability margins and invert singular matrices, ControlAI enforces:\n",
14
- "1. **Deterministic Python Sandbox:** Exact SciPy/Control/CVXPY ODE simulations and plot generation.\n",
15
- "2. **4-Stage Mathematical Proof Standard:** Formal Lyapunov, PBH rank, and Bode sensitivity trade-offs.\n",
16
- "3. **Zero-Hallucination Guarantees:** Rigorous numerical verification with real engineering bounds.\n",
17
  "\n",
18
  "---"
19
  ]
@@ -22,7 +19,7 @@
22
  "cell_type": "markdown",
23
  "metadata": {},
24
  "source": [
25
- "### Step 1: Clone Repository & Install Dependencies"
26
  ]
27
  },
28
  {
@@ -35,15 +32,15 @@
35
  "!git clone https://github.com/atakankahya/controlai-agent.git\n",
36
  "%cd controlai-agent\n",
37
  "\n",
38
- "# Install core scientific control engineering and web dependencies\n",
39
- "!pip install -q scipy numpy matplotlib control cvxpy rich pyngrok uvicorn fastapi"
40
  ]
41
  },
42
  {
43
  "cell_type": "markdown",
44
  "metadata": {},
45
  "source": [
46
- "### Step 2: Quick CLI Interactive Test (Terminal Mode in Colab)"
47
  ]
48
  },
49
  {
@@ -52,16 +49,16 @@
52
  "metadata": {},
53
  "outputs": [],
54
  "source": [
55
- "# Test single control engineering query\n",
56
- "!python cli.py \"Design an LQR controller for A=[[0, 1], [-2, -3]], B=[[0], [1]], Q=diag([10, 1]), R=1 and simulate step response.\""
57
  ]
58
  },
59
  {
60
  "cell_type": "markdown",
61
  "metadata": {},
62
  "source": [
63
- "### Step 3: Launch Live Public Web Interface (Cloud Demo Link)\n",
64
- "Run the cell below to generate a public URL and open the full ControlAI Web Console in your browser!"
65
  ]
66
  },
67
  {
@@ -70,10 +67,11 @@
70
  "metadata": {},
71
  "outputs": [],
72
  "source": [
 
73
  "import subprocess\n",
74
  "import time\n",
75
  "\n",
76
- "# Start the FastAPI server in background\n",
77
  "server_process = subprocess.Popen([\"python\", \"-m\", \"uvicorn\", \"app:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"])\n",
78
  "time.sleep(3)\n",
79
  "\n",
 
7
  "# ControlAI: Open-Source Safety-Critical AI Agent for Control Systems\n",
8
  "\n",
9
  "[![GitHub Repo](https://img.shields.io/badge/GitHub-atakankahya%2Fcontrolai--agent-blue?logo=github)](https://github.com/atakankahya/controlai-agent)\n",
10
+ "[![Hugging Face Model](https://img.shields.io/badge/Hugging%20Face-ControlAI--Agent-blue)](https://huggingface.co/atakankahya/ControlAI-Agent)\n",
11
  "[![License](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)\n",
12
  "\n",
13
+ "**ControlAI** is an open-source AI agent specialized in Control Systems Engineering, Applied Mathematics, and Dynamical Systems Simulation.\n",
 
 
 
14
  "\n",
15
  "---"
16
  ]
 
19
  "cell_type": "markdown",
20
  "metadata": {},
21
  "source": [
22
+ "### Step 1: Clone Repository & Install PyTorch/Transformers Dependencies"
23
  ]
24
  },
25
  {
 
32
  "!git clone https://github.com/atakankahya/controlai-agent.git\n",
33
  "%cd controlai-agent\n",
34
  "\n",
35
+ "# Install PyTorch, Transformers, and Scientific Control Libraries\n",
36
+ "!pip install -q torch transformers accelerate scipy control cvxpy rich pyngrok uvicorn fastapi sentencepiece protobuf python-multipart"
37
  ]
38
  },
39
  {
40
  "cell_type": "markdown",
41
  "metadata": {},
42
  "source": [
43
+ "### Step 2: Quick CLI Test in Terminal Mode"
44
  ]
45
  },
46
  {
 
49
  "metadata": {},
50
  "outputs": [],
51
  "source": [
52
+ "# Run single control engineering query using PyTorch/Transformers\n",
53
+ "!python cli.py --model \"Qwen/Qwen2.5-3B-Instruct\" \"Design an LQR controller for A=[[0, 1], [-2, -3]], B=[[0], [1]], Q=diag([10, 1]), R=1 and simulate step response.\""
54
  ]
55
  },
56
  {
57
  "cell_type": "markdown",
58
  "metadata": {},
59
  "source": [
60
+ "### Step 3: Launch Live Public Web Interface (Interactive Playground Link)\n",
61
+ "Run the cell below to launch the web console and get your public web URL!"
62
  ]
63
  },
64
  {
 
67
  "metadata": {},
68
  "outputs": [],
69
  "source": [
70
+ "import os\n",
71
  "import subprocess\n",
72
  "import time\n",
73
  "\n",
74
+ "# Launch FastAPI server\n",
75
  "server_process = subprocess.Popen([\"python\", \"-m\", \"uvicorn\", \"app:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"])\n",
76
  "time.sleep(3)\n",
77
  "\n",
controlai_agent/orchestrator.py CHANGED
@@ -8,8 +8,16 @@ from dataclasses import dataclass, field
8
  from pathlib import Path
9
  from typing import Any, Generator
10
 
11
- from mlx_lm import generate, load, stream_generate
12
- from transformers import AutoTokenizer
 
 
 
 
 
 
 
 
13
 
14
  from controlai_agent.prompts import CONTROLAI_SYSTEM_PROMPT
15
  from controlai_agent.registry import ToolRegistry, registry
@@ -78,8 +86,7 @@ def _extract_tool_calls(text: str) -> tuple[list[dict[str, Any]], str]:
78
  calls.append(obj)
79
 
80
  # 3. Raw JSON object containing "name" and "arguments" / "parameters"
81
- if not calls and '"name"' in text or "'name'" in text:
82
- # Match starting from first '{' to last matching '}'
83
  match = re.search(r"(\{\s*[\"']name[\"']\s*:\s*[\"'][a-zA-Z0-9_]+[\"'][\s\S]*\})", text)
84
  if match:
85
  obj = parse_flexible_json(match.group(1))
@@ -95,7 +102,7 @@ def _extract_tool_calls(text: str) -> tuple[list[dict[str, Any]], str]:
95
 
96
 
97
  class ControlAIAgent:
98
- """Offline Control Engineering Agent using Qwen3-4B on MLX with deterministic tool calling and streaming."""
99
 
100
  def __init__(
101
  self,
@@ -109,12 +116,31 @@ class ControlAIAgent:
109
  self.registry = tool_registry
110
  self.max_tool_steps = max_tool_steps
111
 
112
- # Load MLX model and HF tokenizer
113
- if adapter_path:
114
- self.model, self.mlx_tokenizer = load(model_path, adapter_path=adapter_path)
 
 
 
 
 
 
115
  else:
116
- self.model, self.mlx_tokenizer = load(model_path)
117
- self.hf_tokenizer = AutoTokenizer.from_pretrained(model_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  # Initialize local offline RAG index
120
  try:
@@ -122,6 +148,29 @@ class ControlAIAgent:
122
  except Exception:
123
  self.rag_index = None
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  def _get_grounded_instruction(self, user_prompt: str, base_instruction: str) -> str:
126
  """Retrieve relevant textbook theorems and inject grounding into system instructions."""
127
  if not self.rag_index or not self.rag_index.chunks:
@@ -181,13 +230,7 @@ class ControlAIAgent:
181
  add_generation_prompt=True,
182
  )
183
 
184
- model_output = generate(
185
- self.model,
186
- self.mlx_tokenizer,
187
- prompt=rendered_prompt,
188
- max_tokens=max_tokens_per_step,
189
- verbose=False,
190
- ).strip()
191
 
192
  tool_calls, pre_text = _extract_tool_calls(model_output)
193
 
@@ -240,13 +283,7 @@ class ControlAIAgent:
240
  tokenize=False,
241
  add_generation_prompt=True,
242
  )
243
- final_output = generate(
244
- self.model,
245
- self.mlx_tokenizer,
246
- prompt=forced_prompt,
247
- max_tokens=max_tokens_per_step,
248
- verbose=False,
249
- ).strip()
250
 
251
  _, clean_final = _extract_tool_calls(final_output)
252
  return AgentResult(
@@ -299,13 +336,7 @@ class ControlAIAgent:
299
  add_generation_prompt=True,
300
  )
301
 
302
- model_output = generate(
303
- self.model,
304
- self.mlx_tokenizer,
305
- prompt=rendered_prompt,
306
- max_tokens=max_tokens_per_step,
307
- verbose=False,
308
- ).strip()
309
 
310
  tool_calls, pre_text = _extract_tool_calls(model_output)
311
 
@@ -396,13 +427,7 @@ class ControlAIAgent:
396
  add_generation_prompt=True,
397
  )
398
 
399
- final_output = generate(
400
- self.model,
401
- self.mlx_tokenizer,
402
- prompt=forced_prompt,
403
- max_tokens=max_tokens_per_step,
404
- verbose=False,
405
- ).strip()
406
 
407
  # If model generated another tool call during final synthesis, execute it!
408
  synth_tool_calls, clean_synth = _extract_tool_calls(final_output)
@@ -421,7 +446,7 @@ class ControlAIAgent:
421
 
422
  # Re-generate synthesis after executing the tool
423
  re_prompt = self.hf_tokenizer.apply_chat_template(messages, tools=None, tokenize=False, add_generation_prompt=True)
424
- final_output = generate(self.model, self.mlx_tokenizer, prompt=re_prompt, max_tokens=max_tokens_per_step, verbose=False).strip()
425
  _, clean_synth = _extract_tool_calls(final_output)
426
 
427
  final_text = clean_synth or final_output
 
8
  from pathlib import Path
9
  from typing import Any, Generator
10
 
11
+ import os
12
+ import sys
13
+
14
+ try:
15
+ from mlx_lm import generate as mlx_generate, load as mlx_load
16
+ HAS_MLX = True
17
+ except ImportError:
18
+ HAS_MLX = False
19
+
20
+ from transformers import AutoModelForCausalLM, AutoTokenizer
21
 
22
  from controlai_agent.prompts import CONTROLAI_SYSTEM_PROMPT
23
  from controlai_agent.registry import ToolRegistry, registry
 
86
  calls.append(obj)
87
 
88
  # 3. Raw JSON object containing "name" and "arguments" / "parameters"
89
+ if not calls and ('"name"' in text or "'name'" in text):
 
90
  match = re.search(r"(\{\s*[\"']name[\"']\s*:\s*[\"'][a-zA-Z0-9_]+[\"'][\s\S]*\})", text)
91
  if match:
92
  obj = parse_flexible_json(match.group(1))
 
102
 
103
 
104
  class ControlAIAgent:
105
+ """Universal Control Engineering Agent supporting MLX (Apple Silicon) and PyTorch/Transformers (Linux/CUDA)."""
106
 
107
  def __init__(
108
  self,
 
116
  self.registry = tool_registry
117
  self.max_tool_steps = max_tool_steps
118
 
119
+ # Detect platform & backend
120
+ self.is_mlx = HAS_MLX and not model_path.startswith("Qwen/") and not os.environ.get("FORCE_TRANSFORMERS")
121
+
122
+ if self.is_mlx:
123
+ if adapter_path:
124
+ self.model, self.mlx_tokenizer = mlx_load(model_path, adapter_path=adapter_path)
125
+ else:
126
+ self.model, self.mlx_tokenizer = mlx_load(model_path)
127
+ self.hf_tokenizer = AutoTokenizer.from_pretrained(model_path)
128
  else:
129
+ # Universal PyTorch / Transformers fallback on Linux, Colab, HuggingFace, CUDA
130
+ hf_id = "Qwen/Qwen2.5-3B-Instruct" if "mlx" in model_path else model_path
131
+ self.hf_tokenizer = AutoTokenizer.from_pretrained(hf_id, trust_remote_code=True)
132
+ self.model = AutoModelForCausalLM.from_pretrained(
133
+ hf_id,
134
+ torch_dtype="auto",
135
+ device_map="auto",
136
+ trust_remote_code=True,
137
+ )
138
+ if adapter_path and Path(adapter_path).exists():
139
+ try:
140
+ from peft import PeftModel
141
+ self.model = PeftModel.from_pretrained(self.model, adapter_path)
142
+ except Exception:
143
+ pass
144
 
145
  # Initialize local offline RAG index
146
  try:
 
148
  except Exception:
149
  self.rag_index = None
150
 
151
+ def _generate(self, prompt: str, max_tokens: int = 2000) -> str:
152
+ """Universal text generation handling both MLX and PyTorch backends."""
153
+ if self.is_mlx:
154
+ return mlx_generate(
155
+ self.model,
156
+ self.mlx_tokenizer,
157
+ prompt=prompt,
158
+ max_tokens=max_tokens,
159
+ verbose=False,
160
+ ).strip()
161
+ else:
162
+ import torch
163
+ inputs = self.hf_tokenizer(prompt, return_tensors="pt").to(self.model.device)
164
+ with torch.no_grad():
165
+ outputs = self.model.generate(
166
+ **inputs,
167
+ max_new_tokens=max_tokens,
168
+ do_sample=False,
169
+ pad_token_id=self.hf_tokenizer.eos_token_id,
170
+ )
171
+ new_tokens = outputs[0][inputs["input_ids"].shape[1]:]
172
+ return self.hf_tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
173
+
174
  def _get_grounded_instruction(self, user_prompt: str, base_instruction: str) -> str:
175
  """Retrieve relevant textbook theorems and inject grounding into system instructions."""
176
  if not self.rag_index or not self.rag_index.chunks:
 
230
  add_generation_prompt=True,
231
  )
232
 
233
+ model_output = self._generate(rendered_prompt, max_tokens=max_tokens_per_step)
 
 
 
 
 
 
234
 
235
  tool_calls, pre_text = _extract_tool_calls(model_output)
236
 
 
283
  tokenize=False,
284
  add_generation_prompt=True,
285
  )
286
+ final_output = self._generate(forced_prompt, max_tokens=max_tokens_per_step)
 
 
 
 
 
 
287
 
288
  _, clean_final = _extract_tool_calls(final_output)
289
  return AgentResult(
 
336
  add_generation_prompt=True,
337
  )
338
 
339
+ model_output = self._generate(rendered_prompt, max_tokens=max_tokens_per_step)
 
 
 
 
 
 
340
 
341
  tool_calls, pre_text = _extract_tool_calls(model_output)
342
 
 
427
  add_generation_prompt=True,
428
  )
429
 
430
+ final_output = self._generate(forced_prompt, max_tokens=max_tokens_per_step)
 
 
 
 
 
 
431
 
432
  # If model generated another tool call during final synthesis, execute it!
433
  synth_tool_calls, clean_synth = _extract_tool_calls(final_output)
 
446
 
447
  # Re-generate synthesis after executing the tool
448
  re_prompt = self.hf_tokenizer.apply_chat_template(messages, tools=None, tokenize=False, add_generation_prompt=True)
449
+ final_output = self._generate(re_prompt, max_tokens=max_tokens_per_step)
450
  _, clean_synth = _extract_tool_calls(final_output)
451
 
452
  final_text = clean_synth or final_output