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.
- lorai_workspace-0.1.0/LICENSE +21 -0
- lorai_workspace-0.1.0/PKG-INFO +64 -0
- lorai_workspace-0.1.0/README.md +39 -0
- lorai_workspace-0.1.0/lorai/__init__.py +5 -0
- lorai_workspace-0.1.0/lorai/cli/__init__.py +227 -0
- lorai_workspace-0.1.0/lorai/client.py +339 -0
- lorai_workspace-0.1.0/lorai/docker.py +134 -0
- lorai_workspace-0.1.0/lorai_workspace.egg-info/PKG-INFO +64 -0
- lorai_workspace-0.1.0/lorai_workspace.egg-info/SOURCES.txt +14 -0
- lorai_workspace-0.1.0/lorai_workspace.egg-info/dependency_links.txt +1 -0
- lorai_workspace-0.1.0/lorai_workspace.egg-info/entry_points.txt +2 -0
- lorai_workspace-0.1.0/lorai_workspace.egg-info/requires.txt +2 -0
- lorai_workspace-0.1.0/lorai_workspace.egg-info/top_level.txt +1 -0
- lorai_workspace-0.1.0/pyproject.toml +40 -0
- lorai_workspace-0.1.0/setup.cfg +4 -0
- lorai_workspace-0.1.0/tests/test_client.py +211 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lorai
|
|
@@ -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,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"
|