lorai-workspace 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 LorAI Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: lorai-workspace
3
+ Version: 0.1.0
4
+ Summary: All of AI. One Command. Port 1842.
5
+ Author-email: LorAI Team <hello@getlorai.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://getlorai.com
8
+ Project-URL: Repository, https://github.com/getlorai/lorai-sdk
9
+ Keywords: ai,llm,openai,ollama,docker,lora
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: openai>=1.30.0
23
+ Requires-Dist: httpx>=0.27.0
24
+ Dynamic: license-file
25
+
26
+ # LorAI SDK
27
+
28
+ **All of AI. One Command. Port 1842.**
29
+
30
+ LorAI gives you a local, free, OpenAI-compatible AI platform with 50+ tools — LLMs, image gen, video, voice, code, agents, RAG, vision — all running in a Docker container on port 1842.
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ pip install lorai-workspace
36
+ ```
37
+
38
+ ```python
39
+ from lorai import LorAI
40
+
41
+ ai = LorAI() # auto-pulls Docker image, starts container
42
+ print(ai.chat("Hello!")) # gets response from local LLM
43
+ ```
44
+
45
+ ## CLI
46
+
47
+ ```bash
48
+ lorai-workspace start # Start the LorAI container
49
+ lorai-workspace chat "Hello!" # Chat with your local AI
50
+ lorai-workspace status # Check system status
51
+ lorai-workspace desktop # Open the browser desktop
52
+ lorai-workspace stop # Stop the container
53
+ ```
54
+
55
+ ## Features
56
+
57
+ - **OpenAI-compatible**: Drop-in replacement for `openai.OpenAI()`
58
+ - **Auto-managed Docker**: Container pulls and starts automatically
59
+ - **10 AI services**: Chat, Image, Video, Voice, Knowledge, Agents, Code, Vision, LoRA, Hub
60
+ - **Port 1842**: Named after Ada Lovelace's year
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,39 @@
1
+ # LorAI SDK
2
+
3
+ **All of AI. One Command. Port 1842.**
4
+
5
+ LorAI gives you a local, free, OpenAI-compatible AI platform with 50+ tools — LLMs, image gen, video, voice, code, agents, RAG, vision — all running in a Docker container on port 1842.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ pip install lorai-workspace
11
+ ```
12
+
13
+ ```python
14
+ from lorai import LorAI
15
+
16
+ ai = LorAI() # auto-pulls Docker image, starts container
17
+ print(ai.chat("Hello!")) # gets response from local LLM
18
+ ```
19
+
20
+ ## CLI
21
+
22
+ ```bash
23
+ lorai-workspace start # Start the LorAI container
24
+ lorai-workspace chat "Hello!" # Chat with your local AI
25
+ lorai-workspace status # Check system status
26
+ lorai-workspace desktop # Open the browser desktop
27
+ lorai-workspace stop # Stop the container
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - **OpenAI-compatible**: Drop-in replacement for `openai.OpenAI()`
33
+ - **Auto-managed Docker**: Container pulls and starts automatically
34
+ - **10 AI services**: Chat, Image, Video, Voice, Knowledge, Agents, Code, Vision, LoRA, Hub
35
+ - **Port 1842**: Named after Ada Lovelace's year
36
+
37
+ ## License
38
+
39
+ MIT
@@ -0,0 +1,5 @@
1
+ from lorai.client import LorAI
2
+
3
+ __version__ = "0.1.0"
4
+ PORT = 1842
5
+ __all__ = ["LorAI", "PORT"]
@@ -0,0 +1,227 @@
1
+ """LorAI CLI — All of AI. One Command. Port 1842.
2
+
3
+ Usage:
4
+ lorai-workspace start [--gpu] [--port PORT]
5
+ lorai-workspace stop
6
+ lorai-workspace status
7
+ lorai-workspace chat "message"
8
+ lorai-workspace desktop
9
+ lorai-workspace pull <model>
10
+ lorai-workspace bench
11
+ lorai-workspace logs
12
+ lorai-workspace version
13
+ lorai-workspace help
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import subprocess
21
+ import sys
22
+ import webbrowser
23
+
24
+ import httpx
25
+
26
+ BANNER = r"""
27
+ ██╗ ██████╗ ██████╗ █████╗ ██╗
28
+ ██║ ██╔═══██╗██╔══██╗██╔══██╗██║
29
+ ██║ ██║ ██║██████╔╝███████║██║
30
+ ██║ ██║ ██║██╔══██╗██╔══██║██║
31
+ ███████╗╚██████╔╝██║ ██║██║ ██║██║
32
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝
33
+
34
+ All of AI. One Command. Port 1842.
35
+ """
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> None:
39
+ parser = argparse.ArgumentParser(
40
+ prog="lorai",
41
+ description="LorAI — All of AI. One Command.",
42
+ )
43
+ sub = parser.add_subparsers(dest="command")
44
+
45
+ # start
46
+ p_start = sub.add_parser("start", help="Start the LorAI container")
47
+ p_start.add_argument("--gpu", action="store_true", help="Enable GPU passthrough")
48
+ p_start.add_argument("--port", type=int, default=1842, help="API port (default: 1842)")
49
+
50
+ # stop
51
+ sub.add_parser("stop", help="Stop the LorAI container")
52
+
53
+ # status
54
+ sub.add_parser("status", help="Show LorAI status")
55
+
56
+ # chat
57
+ p_chat = sub.add_parser("chat", help="Chat with the local AI")
58
+ p_chat.add_argument("message", help="Message to send")
59
+ p_chat.add_argument("--model", default=None, help="Model to use")
60
+
61
+ # desktop
62
+ sub.add_parser("desktop", help="Open LorAI desktop in browser")
63
+
64
+ # pull
65
+ p_pull = sub.add_parser("pull", help="Pull/download an AI model")
66
+ p_pull.add_argument("model", help="Model name (e.g. llama3, phi3:mini)")
67
+
68
+ # bench
69
+ sub.add_parser("bench", help="Benchmark your hardware")
70
+
71
+ # logs
72
+ sub.add_parser("logs", help="Show container logs")
73
+
74
+ # version
75
+ sub.add_parser("version", help="Show LorAI version")
76
+
77
+ # help (explicit)
78
+ sub.add_parser("help", help="Show help with banner")
79
+
80
+ args = parser.parse_args(argv)
81
+
82
+ if args.command is None or args.command == "help":
83
+ _cmd_help()
84
+ elif args.command == "start":
85
+ _cmd_start(port=args.port, gpu=args.gpu)
86
+ elif args.command == "stop":
87
+ _cmd_stop()
88
+ elif args.command == "status":
89
+ _cmd_status()
90
+ elif args.command == "chat":
91
+ _cmd_chat(args.message, model=args.model)
92
+ elif args.command == "desktop":
93
+ _cmd_desktop()
94
+ elif args.command == "pull":
95
+ _cmd_pull(args.model)
96
+ elif args.command == "bench":
97
+ _cmd_bench()
98
+ elif args.command == "logs":
99
+ _cmd_logs()
100
+ elif args.command == "version":
101
+ _cmd_version()
102
+
103
+
104
+ def _cmd_help() -> None:
105
+ print(BANNER)
106
+ print("Usage: lorai <command> [options]\n")
107
+ print("Commands:")
108
+ print(" start [--gpu] [--port N] Start the LorAI container")
109
+ print(" stop Stop the LorAI container")
110
+ print(" status Show system status")
111
+ print(' chat "message" Chat with the local AI')
112
+ print(" desktop Open browser desktop (noVNC)")
113
+ print(" pull <model> Download an AI model")
114
+ print(" bench Benchmark your hardware")
115
+ print(" logs Show container logs")
116
+ print(" version Show version info")
117
+ print(" help Show this help")
118
+ print()
119
+ print("Examples:")
120
+ print(" lorai-workspace start")
121
+ print(' lorai-workspace chat "What is the meaning of life?"')
122
+ print(" lorai-workspace pull llama3")
123
+ print(" lorai-workspace desktop")
124
+ print(" lorai-workspace stop")
125
+
126
+
127
+ def _cmd_start(port: int = 1842, gpu: bool = False) -> None:
128
+ from lorai.docker import ensure_running
129
+ ensure_running(port=port, gpu=gpu)
130
+ print(f"\nLorAI is running at http://localhost:{port}")
131
+ print("Desktop available at http://localhost:6080")
132
+
133
+
134
+ def _cmd_stop() -> None:
135
+ from lorai.docker import stop_container
136
+ stop_container()
137
+ print("LorAI stopped.")
138
+
139
+
140
+ def _cmd_status() -> None:
141
+ from lorai.docker import status
142
+ s = status()
143
+ print(BANNER)
144
+ print(f" Docker installed: {'yes' if s['docker_installed'] else 'NO'}")
145
+ print(f" Image pulled: {'yes' if s['image_pulled'] else 'no'}")
146
+ print(f" Container running: {'yes' if s['container_running'] else 'no'}")
147
+ print(f" API healthy: {'yes' if s['api_healthy'] else 'no'}")
148
+ print(f" API URL: {s['url']}")
149
+ print(f" Desktop URL: {s['desktop_url']}")
150
+
151
+
152
+ def _cmd_chat(message: str, model: str | None = None) -> None:
153
+ payload = {
154
+ "model": model or "auto",
155
+ "messages": [{"role": "user", "content": message}],
156
+ }
157
+ try:
158
+ resp = httpx.post(
159
+ "http://localhost:1842/v1/chat/completions",
160
+ json=payload, timeout=120,
161
+ )
162
+ resp.raise_for_status()
163
+ data = resp.json()
164
+ print(data["choices"][0]["message"]["content"])
165
+ except httpx.ConnectError:
166
+ print("Error: LorAI is not running. Start it with: lorai-workspace start")
167
+ sys.exit(1)
168
+ except Exception as e:
169
+ print(f"Error: {e}")
170
+ sys.exit(1)
171
+
172
+
173
+ def _cmd_desktop() -> None:
174
+ url = "http://localhost:6080"
175
+ print(f"Opening LorAI desktop: {url}")
176
+ webbrowser.open(url)
177
+
178
+
179
+ def _cmd_pull(model: str) -> None:
180
+ print(f"Pulling model: {model}")
181
+ try:
182
+ resp = httpx.post(
183
+ "http://localhost:1842/lorai/hub/pull",
184
+ json={"name": model}, timeout=600,
185
+ )
186
+ resp.raise_for_status()
187
+ print(json.dumps(resp.json(), indent=2))
188
+ except httpx.ConnectError:
189
+ print("Error: LorAI is not running. Start it with: lorai-workspace start")
190
+ sys.exit(1)
191
+
192
+
193
+ def _cmd_bench() -> None:
194
+ print("Running benchmark...")
195
+ try:
196
+ resp = httpx.post(
197
+ "http://localhost:1842/lorai/hub/bench",
198
+ json={}, timeout=300,
199
+ )
200
+ resp.raise_for_status()
201
+ print(json.dumps(resp.json(), indent=2))
202
+ except httpx.ConnectError:
203
+ print("Error: LorAI is not running. Start it with: lorai-workspace start")
204
+ sys.exit(1)
205
+
206
+
207
+ def _cmd_logs() -> None:
208
+ try:
209
+ subprocess.run(
210
+ ["docker", "logs", "--tail", "50", "-f", "lorai"],
211
+ check=True,
212
+ )
213
+ except FileNotFoundError:
214
+ print("Error: Docker is not installed.")
215
+ sys.exit(1)
216
+ except subprocess.CalledProcessError:
217
+ print("Error: LorAI container not found. Start it with: lorai-workspace start")
218
+ sys.exit(1)
219
+
220
+
221
+ def _cmd_version() -> None:
222
+ from lorai import __version__, PORT
223
+ print(BANNER)
224
+ print(f" Version: {__version__}")
225
+ print(f" Port: {PORT}")
226
+ print(f" URL: http://localhost:{PORT}")
227
+ print(" Desktop: http://localhost:6080")
@@ -0,0 +1,339 @@
1
+ """LorAI client — extends OpenAI with 10 AI services.
2
+
3
+ Usage:
4
+ from lorai import LorAI
5
+ ai = LorAI()
6
+ print(ai.chat("Hello!"))
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ import httpx
16
+ from openai import OpenAI
17
+
18
+ from lorai.docker import ensure_running, stop_container
19
+
20
+
21
+ class LorAI(OpenAI):
22
+ """OpenAI-compatible client backed by a local LorAI Docker container.
23
+
24
+ Extends the official OpenAI Python SDK so that any existing OpenAI code
25
+ works unchanged — just swap ``OpenAI()`` for ``LorAI()``.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ base_url: str | None = None,
32
+ api_key: str | None = None,
33
+ port: int = 1842,
34
+ auto_start: bool = True,
35
+ gpu: bool = False,
36
+ default_model: str = "auto",
37
+ ) -> None:
38
+ self._port = port
39
+ self._default_model = default_model
40
+
41
+ if auto_start:
42
+ ensure_running(port=port, gpu=gpu)
43
+
44
+ super().__init__(
45
+ base_url=base_url or f"http://localhost:{port}/v1",
46
+ api_key=api_key or "not-needed",
47
+ )
48
+
49
+ # Initialize service objects
50
+ self.image = _ImageService(self)
51
+ self.video = _VideoService(self)
52
+ self.voice = _VoiceService(self)
53
+ self.knowledge = _KnowledgeService(self)
54
+ self.agents = _AgentsService(self)
55
+ self.code = _CodeService(self)
56
+ self.vision = _VisionService(self)
57
+ self.lora = _LoRAService(self)
58
+ self.hub = _HubService(self)
59
+
60
+ # ------------------------------------------------------------------
61
+ # Convenience helpers
62
+ # ------------------------------------------------------------------
63
+
64
+ def chat(
65
+ self,
66
+ message: str,
67
+ *,
68
+ model: str | None = None,
69
+ system: str | None = None,
70
+ lora: str | None = None,
71
+ temperature: float = 0.7,
72
+ max_tokens: int | None = None,
73
+ stream: bool = False,
74
+ json_mode: bool = False,
75
+ ) -> str:
76
+ """Simple one-shot chat. Returns the assistant's text reply."""
77
+ messages: list[dict[str, str]] = []
78
+ if system:
79
+ messages.append({"role": "system", "content": system})
80
+ messages.append({"role": "user", "content": message})
81
+
82
+ kwargs: dict[str, Any] = {
83
+ "model": model or self._default_model,
84
+ "messages": messages,
85
+ "temperature": temperature,
86
+ }
87
+ if max_tokens is not None:
88
+ kwargs["max_tokens"] = max_tokens
89
+ if json_mode:
90
+ kwargs["response_format"] = {"type": "json_object"}
91
+
92
+ # Attach LoRA header if requested
93
+ extra_headers = {}
94
+ if lora:
95
+ extra_headers["X-LorAI-LoRA"] = lora
96
+
97
+ if stream:
98
+ chunks = []
99
+ response = self.chat.completions.create(
100
+ **kwargs, stream=True, extra_headers=extra_headers or None,
101
+ )
102
+ for chunk in response:
103
+ delta = chunk.choices[0].delta.content
104
+ if delta:
105
+ chunks.append(delta)
106
+ return "".join(chunks)
107
+
108
+ response = self.chat.completions.create(
109
+ **kwargs, extra_headers=extra_headers or None,
110
+ )
111
+ return response.choices[0].message.content
112
+
113
+ def stop(self) -> None:
114
+ """Stop the LorAI Docker container."""
115
+ stop_container()
116
+
117
+ @property
118
+ def _native_base(self) -> str:
119
+ return f"http://localhost:{self._port}"
120
+
121
+
122
+ # ======================================================================
123
+ # Service classes
124
+ # ======================================================================
125
+
126
+ class _BaseNativeService:
127
+ """Base for services that call LorAI-native endpoints."""
128
+
129
+ def __init__(self, client: LorAI) -> None:
130
+ self._client = client
131
+
132
+ def _post(self, path: str, **kwargs: Any) -> Any:
133
+ resp = httpx.post(
134
+ f"{self._client._native_base}{path}",
135
+ json=kwargs, timeout=300,
136
+ )
137
+ resp.raise_for_status()
138
+ return resp.json()
139
+
140
+ def _get(self, path: str) -> Any:
141
+ resp = httpx.get(
142
+ f"{self._client._native_base}{path}",
143
+ timeout=30,
144
+ )
145
+ resp.raise_for_status()
146
+ return resp.json()
147
+
148
+
149
+ class _ImageService:
150
+ """Image generation via OpenAI /v1/images endpoints."""
151
+
152
+ def __init__(self, client: LorAI) -> None:
153
+ self._client = client
154
+
155
+ def generate(
156
+ self,
157
+ prompt: str,
158
+ *,
159
+ model: str = "dall-e-3",
160
+ size: str = "1024x1024",
161
+ lora: str | None = None,
162
+ save_to: str | None = None,
163
+ ) -> Any:
164
+ extra_headers = {}
165
+ if lora:
166
+ extra_headers["X-LorAI-LoRA"] = lora
167
+ response = self._client.images.generate(
168
+ model=model, prompt=prompt, size=size,
169
+ extra_headers=extra_headers or None,
170
+ )
171
+ if save_to and response.data and response.data[0].b64_json:
172
+ Path(save_to).write_bytes(base64.b64decode(response.data[0].b64_json))
173
+ return response
174
+
175
+ def edit(self, image_path: str, prompt: str, **kwargs: Any) -> Any:
176
+ with open(image_path, "rb") as f:
177
+ return self._client.images.edit(image=f, prompt=prompt, **kwargs)
178
+
179
+
180
+ class _VideoService(_BaseNativeService):
181
+ """Video generation via LorAI-native /lorai/video endpoints."""
182
+
183
+ def generate(
184
+ self,
185
+ prompt: str,
186
+ *,
187
+ model: str = "auto",
188
+ duration: float = 4.0,
189
+ fps: int = 24,
190
+ image_path: str | None = None,
191
+ save_to: str | None = None,
192
+ ) -> Any:
193
+ payload: dict[str, Any] = {
194
+ "prompt": prompt, "model": model,
195
+ "duration": duration, "fps": fps,
196
+ }
197
+ if image_path:
198
+ payload["image"] = base64.b64encode(Path(image_path).read_bytes()).decode()
199
+ return self._post("/lorai/video/generate", **payload)
200
+
201
+
202
+ class _VoiceService:
203
+ """Voice services via OpenAI /v1/audio endpoints."""
204
+
205
+ def __init__(self, client: LorAI) -> None:
206
+ self._client = client
207
+
208
+ def transcribe(self, audio_path: str, *, model: str = "whisper-1") -> str:
209
+ with open(audio_path, "rb") as f:
210
+ result = self._client.audio.transcriptions.create(model=model, file=f)
211
+ return result.text
212
+
213
+ def speak(
214
+ self, text: str, *, voice: str = "alloy", save_to: str | None = None,
215
+ ) -> Any:
216
+ response = self._client.audio.speech.create(
217
+ model="tts-1", voice=voice, input=text,
218
+ )
219
+ if save_to:
220
+ response.write_to_file(save_to)
221
+ return response
222
+
223
+
224
+ class _KnowledgeService(_BaseNativeService):
225
+ """RAG / knowledge base via LorAI-native /lorai/knowledge endpoints."""
226
+
227
+ def ingest(self, source: str, *, collection: str = "default") -> Any:
228
+ return self._post("/lorai/knowledge/ingest", source=source, collection=collection)
229
+
230
+ def search(self, query: str, *, top_k: int = 5) -> Any:
231
+ return self._post("/lorai/knowledge/search", query=query, top_k=top_k)
232
+
233
+ def ask(self, question: str) -> Any:
234
+ return self._post("/lorai/knowledge/ask", question=question)
235
+
236
+
237
+ class _AgentsService(_BaseNativeService):
238
+ """Agent workflows via LorAI-native /lorai/agents endpoints."""
239
+
240
+ def run(
241
+ self,
242
+ task: str,
243
+ *,
244
+ agents: list[str] | None = None,
245
+ tools: list[str] | None = None,
246
+ max_steps: int = 10,
247
+ ) -> Any:
248
+ return self._post(
249
+ "/lorai/agents/run",
250
+ task=task, agents=agents or [], tools=tools or [],
251
+ max_steps=max_steps,
252
+ )
253
+
254
+ def list_agents(self) -> Any:
255
+ return self._get("/lorai/agents/list")
256
+
257
+ def list_tools(self) -> Any:
258
+ return self._get("/lorai/agents/tools")
259
+
260
+
261
+ class _CodeService(_BaseNativeService):
262
+ """Code generation and execution."""
263
+
264
+ def __init__(self, client: LorAI) -> None:
265
+ super().__init__(client)
266
+
267
+ def generate(
268
+ self, prompt: str, *, language: str = "python", execute: bool = False,
269
+ ) -> Any:
270
+ return self._post(
271
+ "/lorai/code/execute",
272
+ prompt=prompt, language=language, execute=execute,
273
+ )
274
+
275
+ def review(self, code: str) -> str:
276
+ response = self._client.chat.completions.create(
277
+ model=self._client._default_model,
278
+ messages=[
279
+ {"role": "system", "content": "You are a code reviewer. Review the following code and provide feedback."},
280
+ {"role": "user", "content": code},
281
+ ],
282
+ )
283
+ return response.choices[0].message.content
284
+
285
+
286
+ class _VisionService:
287
+ """Vision analysis via OpenAI multimodal endpoints."""
288
+
289
+ def __init__(self, client: LorAI) -> None:
290
+ self._client = client
291
+
292
+ def analyze(self, image_path: str, prompt: str = "Describe this image.") -> str:
293
+ b64 = base64.b64encode(Path(image_path).read_bytes()).decode()
294
+ response = self._client.chat.completions.create(
295
+ model=self._client._default_model,
296
+ messages=[{
297
+ "role": "user",
298
+ "content": [
299
+ {"type": "text", "text": prompt},
300
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
301
+ ],
302
+ }],
303
+ )
304
+ return response.choices[0].message.content
305
+
306
+ def ocr(self, image_path: str) -> str:
307
+ return self.analyze(image_path, prompt="Extract all text from this image (OCR).")
308
+
309
+
310
+ class _LoRAService(_BaseNativeService):
311
+ """LoRA adapter management via /lorai/lora endpoints."""
312
+
313
+ def list(self) -> Any:
314
+ return self._get("/lorai/lora/list")
315
+
316
+ def load(self, name: str, *, base_model: str = "auto") -> Any:
317
+ return self._post("/lorai/lora/load", name=name, base_model=base_model)
318
+
319
+ def unload(self, name: str) -> Any:
320
+ return self._post("/lorai/lora/unload", name=name)
321
+
322
+
323
+ class _HubService(_BaseNativeService):
324
+ """Model hub management via /lorai/hub endpoints."""
325
+
326
+ def models(self) -> Any:
327
+ return self._get("/lorai/hub/models")
328
+
329
+ def pull(self, model: str) -> Any:
330
+ return self._post("/lorai/hub/pull", name=model)
331
+
332
+ def remove(self, model: str) -> Any:
333
+ return self._post("/lorai/hub/remove", name=model)
334
+
335
+ def status(self) -> Any:
336
+ return self._get("/lorai/hub/status")
337
+
338
+ def bench(self) -> Any:
339
+ return self._post("/lorai/hub/bench")
@@ -0,0 +1,134 @@
1
+ """Docker container management for LorAI.
2
+
3
+ Handles pulling, starting, stopping, and health-checking the
4
+ LorAI Desktop Docker container.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import time
11
+
12
+ import httpx
13
+
14
+ IMAGE = os.environ.get("LORAI_IMAGE", "gajapathiks/lorai-workspace:latest")
15
+ CONTAINER_NAME = "lorai"
16
+ HEALTH_TIMEOUT = 120 # seconds to wait for healthy state
17
+
18
+
19
+ def is_docker_installed() -> bool:
20
+ """Check if the docker CLI is available on PATH."""
21
+ return shutil.which("docker") is not None
22
+
23
+
24
+ def is_image_pulled() -> bool:
25
+ """Check if the LorAI Desktop image exists locally."""
26
+ result = subprocess.run(
27
+ ["docker", "images", "-q", IMAGE],
28
+ capture_output=True, text=True,
29
+ )
30
+ return bool(result.stdout.strip())
31
+
32
+
33
+ def is_container_running() -> bool:
34
+ """Check if a container named 'lorai' is currently running."""
35
+ result = subprocess.run(
36
+ ["docker", "inspect", "-f", "{{.State.Running}}", CONTAINER_NAME],
37
+ capture_output=True, text=True,
38
+ )
39
+ return result.stdout.strip() == "true"
40
+
41
+
42
+ def is_lorai_healthy(port: int = 1842) -> bool:
43
+ """Check if the LorAI API responds on the given port."""
44
+ try:
45
+ resp = httpx.get(f"http://localhost:{port}/api/health", timeout=5)
46
+ return resp.status_code == 200
47
+ except (httpx.ConnectError, httpx.TimeoutException):
48
+ return False
49
+
50
+
51
+ def pull_image() -> None:
52
+ """Pull the LorAI Desktop Docker image."""
53
+ print("Pulling LorAI Docker image (this may take a while)...")
54
+ subprocess.run(["docker", "pull", IMAGE], check=True)
55
+
56
+
57
+ def start_container(port: int = 1842, vnc_port: int = 6080, gpu: bool = False) -> None:
58
+ """Start the LorAI Docker container."""
59
+ cmd = [
60
+ "docker", "run", "-d",
61
+ "--name", CONTAINER_NAME,
62
+ "-p", f"{port}:1842",
63
+ "-p", f"{vnc_port}:6080",
64
+ "-v", f"{_data_dir()}:/data",
65
+ "-e", "LORAI_MODE=hybrid",
66
+ "-e", "LORAI_MODEL=phi3:mini",
67
+ ]
68
+ if gpu:
69
+ cmd.extend(["--gpus", "all"])
70
+ cmd.append(IMAGE)
71
+
72
+ print(f"Starting LorAI container on port {port}...")
73
+ subprocess.run(cmd, check=True)
74
+
75
+
76
+ def stop_container() -> None:
77
+ """Stop and remove the LorAI container."""
78
+ subprocess.run(["docker", "stop", CONTAINER_NAME], capture_output=True)
79
+ subprocess.run(["docker", "rm", CONTAINER_NAME], capture_output=True)
80
+
81
+
82
+ def ensure_running(port: int = 1842, gpu: bool = False) -> None:
83
+ """Orchestrate: check -> pull -> start -> health check."""
84
+ if not is_docker_installed():
85
+ raise RuntimeError(
86
+ "Docker is not installed. Please install Docker first: https://docs.docker.com/get-docker/"
87
+ )
88
+
89
+ if is_container_running() and is_lorai_healthy(port):
90
+ return
91
+
92
+ # Stop stale container if exists
93
+ if is_container_running():
94
+ stop_container()
95
+
96
+ if not is_image_pulled():
97
+ pull_image()
98
+
99
+ start_container(port=port, gpu=gpu)
100
+
101
+ # Wait for health
102
+ print("Waiting for LorAI to become ready...")
103
+ deadline = time.time() + HEALTH_TIMEOUT
104
+ while time.time() < deadline:
105
+ if is_lorai_healthy(port):
106
+ print(f"LorAI is ready at http://localhost:{port}")
107
+ return
108
+ time.sleep(2)
109
+
110
+ raise RuntimeError(
111
+ f"LorAI did not become healthy within {HEALTH_TIMEOUT}s. "
112
+ "Check logs with: docker logs lorai"
113
+ )
114
+
115
+
116
+ def status(port: int = 1842) -> dict:
117
+ """Return a dict describing the current state of LorAI."""
118
+ return {
119
+ "docker_installed": is_docker_installed(),
120
+ "image_pulled": is_image_pulled() if is_docker_installed() else False,
121
+ "container_running": is_container_running() if is_docker_installed() else False,
122
+ "api_healthy": is_lorai_healthy(port),
123
+ "port": port,
124
+ "url": f"http://localhost:{port}",
125
+ "desktop_url": "http://localhost:6080",
126
+ }
127
+
128
+
129
+ def _data_dir() -> str:
130
+ """Return the host-side data directory (~/.lorai/data)."""
131
+ from pathlib import Path
132
+ data = Path.home() / ".lorai" / "data"
133
+ data.mkdir(parents=True, exist_ok=True)
134
+ return str(data)
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: lorai-workspace
3
+ Version: 0.1.0
4
+ Summary: All of AI. One Command. Port 1842.
5
+ Author-email: LorAI Team <hello@getlorai.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://getlorai.com
8
+ Project-URL: Repository, https://github.com/getlorai/lorai-sdk
9
+ Keywords: ai,llm,openai,ollama,docker,lora
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: openai>=1.30.0
23
+ Requires-Dist: httpx>=0.27.0
24
+ Dynamic: license-file
25
+
26
+ # LorAI SDK
27
+
28
+ **All of AI. One Command. Port 1842.**
29
+
30
+ LorAI gives you a local, free, OpenAI-compatible AI platform with 50+ tools — LLMs, image gen, video, voice, code, agents, RAG, vision — all running in a Docker container on port 1842.
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ pip install lorai-workspace
36
+ ```
37
+
38
+ ```python
39
+ from lorai import LorAI
40
+
41
+ ai = LorAI() # auto-pulls Docker image, starts container
42
+ print(ai.chat("Hello!")) # gets response from local LLM
43
+ ```
44
+
45
+ ## CLI
46
+
47
+ ```bash
48
+ lorai-workspace start # Start the LorAI container
49
+ lorai-workspace chat "Hello!" # Chat with your local AI
50
+ lorai-workspace status # Check system status
51
+ lorai-workspace desktop # Open the browser desktop
52
+ lorai-workspace stop # Stop the container
53
+ ```
54
+
55
+ ## Features
56
+
57
+ - **OpenAI-compatible**: Drop-in replacement for `openai.OpenAI()`
58
+ - **Auto-managed Docker**: Container pulls and starts automatically
59
+ - **10 AI services**: Chat, Image, Video, Voice, Knowledge, Agents, Code, Vision, LoRA, Hub
60
+ - **Port 1842**: Named after Ada Lovelace's year
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ lorai/__init__.py
5
+ lorai/client.py
6
+ lorai/docker.py
7
+ lorai/cli/__init__.py
8
+ lorai_workspace.egg-info/PKG-INFO
9
+ lorai_workspace.egg-info/SOURCES.txt
10
+ lorai_workspace.egg-info/dependency_links.txt
11
+ lorai_workspace.egg-info/entry_points.txt
12
+ lorai_workspace.egg-info/requires.txt
13
+ lorai_workspace.egg-info/top_level.txt
14
+ tests/test_client.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ lorai-workspace = lorai.cli:main
@@ -0,0 +1,2 @@
1
+ openai>=1.30.0
2
+ httpx>=0.27.0
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "lorai-workspace"
7
+ version = "0.1.0"
8
+ description = "All of AI. One Command. Port 1842."
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ {name = "LorAI Team", email = "hello@getlorai.com"},
14
+ ]
15
+ keywords = ["ai", "llm", "openai", "ollama", "docker", "lora"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ ]
27
+ dependencies = [
28
+ "openai>=1.30.0",
29
+ "httpx>=0.27.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://getlorai.com"
34
+ Repository = "https://github.com/getlorai/lorai-sdk"
35
+
36
+ [project.scripts]
37
+ lorai-workspace = "lorai.cli:main"
38
+
39
+ [tool.setuptools.packages.find]
40
+ include = ["lorai*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,211 @@
1
+ """Tests for the LorAI SDK."""
2
+
3
+ from unittest.mock import patch
4
+
5
+
6
+ # ------------------------------------------------------------------
7
+ # Test constants
8
+ # ------------------------------------------------------------------
9
+
10
+ def test_port_is_1842():
11
+ import lorai
12
+ assert lorai.PORT == 1842
13
+
14
+
15
+ def test_version_is_set():
16
+ import lorai
17
+ assert lorai.__version__ == "0.1.0"
18
+
19
+
20
+ # ------------------------------------------------------------------
21
+ # Test LorAI client
22
+ # ------------------------------------------------------------------
23
+
24
+ @patch("lorai.client.ensure_running")
25
+ def test_lorai_inherits_from_openai(mock_ensure):
26
+ from openai import OpenAI
27
+ from lorai import LorAI
28
+ ai = LorAI(auto_start=False)
29
+ assert isinstance(ai, OpenAI)
30
+
31
+
32
+ @patch("lorai.client.ensure_running")
33
+ def test_auto_start_calls_ensure_running(mock_ensure):
34
+ from lorai import LorAI
35
+ LorAI(auto_start=True)
36
+ mock_ensure.assert_called_once_with(port=1842, gpu=False)
37
+
38
+
39
+ @patch("lorai.client.ensure_running")
40
+ def test_auto_start_false_skips_ensure_running(mock_ensure):
41
+ from lorai import LorAI
42
+ LorAI(auto_start=False)
43
+ mock_ensure.assert_not_called()
44
+
45
+
46
+ @patch("lorai.client.ensure_running")
47
+ def test_custom_port_passes_through(mock_ensure):
48
+ from lorai import LorAI
49
+ ai = LorAI(auto_start=True, port=9999)
50
+ mock_ensure.assert_called_once_with(port=9999, gpu=False)
51
+ assert ai._port == 9999
52
+
53
+
54
+ @patch("lorai.client.ensure_running")
55
+ def test_gpu_flag_passes_through(mock_ensure):
56
+ from lorai import LorAI
57
+ LorAI(auto_start=True, gpu=True)
58
+ mock_ensure.assert_called_once_with(port=1842, gpu=True)
59
+
60
+
61
+ # ------------------------------------------------------------------
62
+ # Test all 9 services exist with correct methods
63
+ # ------------------------------------------------------------------
64
+
65
+ @patch("lorai.client.ensure_running")
66
+ def test_image_service(mock_ensure):
67
+ from lorai import LorAI
68
+ ai = LorAI(auto_start=False)
69
+ svc = ai.image
70
+ assert hasattr(svc, "generate")
71
+ assert hasattr(svc, "edit")
72
+
73
+
74
+ @patch("lorai.client.ensure_running")
75
+ def test_video_service(mock_ensure):
76
+ from lorai import LorAI
77
+ ai = LorAI(auto_start=False)
78
+ svc = ai.video
79
+ assert hasattr(svc, "generate")
80
+
81
+
82
+ @patch("lorai.client.ensure_running")
83
+ def test_voice_service(mock_ensure):
84
+ from lorai import LorAI
85
+ ai = LorAI(auto_start=False)
86
+ svc = ai.voice
87
+ assert hasattr(svc, "transcribe")
88
+ assert hasattr(svc, "speak")
89
+
90
+
91
+ @patch("lorai.client.ensure_running")
92
+ def test_knowledge_service(mock_ensure):
93
+ from lorai import LorAI
94
+ ai = LorAI(auto_start=False)
95
+ svc = ai.knowledge
96
+ assert hasattr(svc, "ingest")
97
+ assert hasattr(svc, "search")
98
+ assert hasattr(svc, "ask")
99
+
100
+
101
+ @patch("lorai.client.ensure_running")
102
+ def test_agents_service(mock_ensure):
103
+ from lorai import LorAI
104
+ ai = LorAI(auto_start=False)
105
+ svc = ai.agents
106
+ assert hasattr(svc, "run")
107
+ assert hasattr(svc, "list_agents")
108
+ assert hasattr(svc, "list_tools")
109
+
110
+
111
+ @patch("lorai.client.ensure_running")
112
+ def test_code_service(mock_ensure):
113
+ from lorai import LorAI
114
+ ai = LorAI(auto_start=False)
115
+ svc = ai.code
116
+ assert hasattr(svc, "generate")
117
+ assert hasattr(svc, "review")
118
+
119
+
120
+ @patch("lorai.client.ensure_running")
121
+ def test_vision_service(mock_ensure):
122
+ from lorai import LorAI
123
+ ai = LorAI(auto_start=False)
124
+ svc = ai.vision
125
+ assert hasattr(svc, "analyze")
126
+ assert hasattr(svc, "ocr")
127
+
128
+
129
+ @patch("lorai.client.ensure_running")
130
+ def test_lora_service(mock_ensure):
131
+ from lorai import LorAI
132
+ ai = LorAI(auto_start=False)
133
+ svc = ai.lora
134
+ assert hasattr(svc, "list")
135
+ assert hasattr(svc, "load")
136
+ assert hasattr(svc, "unload")
137
+
138
+
139
+ @patch("lorai.client.ensure_running")
140
+ def test_hub_service(mock_ensure):
141
+ from lorai import LorAI
142
+ ai = LorAI(auto_start=False)
143
+ svc = ai.hub
144
+ assert hasattr(svc, "models")
145
+ assert hasattr(svc, "pull")
146
+ assert hasattr(svc, "remove")
147
+ assert hasattr(svc, "status")
148
+ assert hasattr(svc, "bench")
149
+
150
+
151
+ # ------------------------------------------------------------------
152
+ # Test docker.py functions exist
153
+ # ------------------------------------------------------------------
154
+
155
+ def test_docker_module_functions():
156
+ from lorai import docker
157
+ assert callable(docker.is_docker_installed)
158
+ assert callable(docker.is_image_pulled)
159
+ assert callable(docker.is_container_running)
160
+ assert callable(docker.is_lorai_healthy)
161
+ assert callable(docker.pull_image)
162
+ assert callable(docker.start_container)
163
+ assert callable(docker.stop_container)
164
+ assert callable(docker.ensure_running)
165
+ assert callable(docker.status)
166
+
167
+
168
+ # ------------------------------------------------------------------
169
+ # Test CLI entry point
170
+ # ------------------------------------------------------------------
171
+
172
+ def test_cli_entry_point():
173
+ """Verify the CLI main function is importable and callable."""
174
+ from lorai.cli import main
175
+ assert callable(main)
176
+
177
+
178
+ def test_cli_version_command(capsys):
179
+ """Test that 'lorai-workspace version' prints version and port."""
180
+ from lorai.cli import main
181
+ main(["version"])
182
+ captured = capsys.readouterr()
183
+ assert "0.1.0" in captured.out
184
+ assert "1842" in captured.out
185
+
186
+
187
+ def test_cli_help_command(capsys):
188
+ """Test that 'lorai-workspace help' prints the banner."""
189
+ from lorai.cli import main
190
+ main(["help"])
191
+ captured = capsys.readouterr()
192
+ assert "All of AI" in captured.out
193
+ assert "1842" in captured.out
194
+
195
+
196
+ # ------------------------------------------------------------------
197
+ # Test default model
198
+ # ------------------------------------------------------------------
199
+
200
+ @patch("lorai.client.ensure_running")
201
+ def test_default_model(mock_ensure):
202
+ from lorai import LorAI
203
+ ai = LorAI(auto_start=False, default_model="llama3")
204
+ assert ai._default_model == "llama3"
205
+
206
+
207
+ @patch("lorai.client.ensure_running")
208
+ def test_default_model_auto(mock_ensure):
209
+ from lorai import LorAI
210
+ ai = LorAI(auto_start=False)
211
+ assert ai._default_model == "auto"