parllama 0.2.0__py3-none-any.whl
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.
- parllama/__init__.py +22 -0
- parllama/__main__.py +25 -0
- parllama/app.py +615 -0
- parllama/app.tcss +152 -0
- parllama/data_manager.py +246 -0
- parllama/dialogs/__init__.py +3 -0
- parllama/dialogs/error_dialog.py +26 -0
- parllama/dialogs/help_dialog.py +79 -0
- parllama/dialogs/information.py +13 -0
- parllama/dialogs/input_dialog.py +108 -0
- parllama/dialogs/model_details_dialog.py +132 -0
- parllama/dialogs/password_dialog.py +88 -0
- parllama/dialogs/text_dialog.py +75 -0
- parllama/dialogs/yes_no_dialog.py +111 -0
- parllama/docker_utils.py +116 -0
- parllama/help.md +95 -0
- parllama/icons.py +25 -0
- parllama/messages/__init__.py +3 -0
- parllama/messages/main.py +144 -0
- parllama/models/__init__.py +3 -0
- parllama/models/jobs.py +30 -0
- parllama/models/ollama_data.py +75 -0
- parllama/models/settings_data.py +100 -0
- parllama/par_logger.py +49 -0
- parllama/py.typed +0 -0
- parllama/screens/__init__.py +3 -0
- parllama/screens/local_models_screen.py +263 -0
- parllama/screens/local_models_screen.tcss +12 -0
- parllama/screens/log_screen.py +50 -0
- parllama/screens/log_screen.tcss +9 -0
- parllama/screens/model_tools_screen.py +57 -0
- parllama/screens/model_tools_screen.tcss +11 -0
- parllama/screens/site_models_screen.py +207 -0
- parllama/screens/site_models_screen.tcss +46 -0
- parllama/theme_manager.py +122 -0
- parllama/themes/__init__.py +3 -0
- parllama/themes/par.json +25 -0
- parllama/time_display.py +64 -0
- parllama/utils.py +592 -0
- parllama/widgets/__init__.py +3 -0
- parllama/widgets/clickable_label.py +67 -0
- parllama/widgets/dbl_click_list_item.py +29 -0
- parllama/widgets/field_set.py +90 -0
- parllama/widgets/filter_input.py +22 -0
- parllama/widgets/grid_list.py +284 -0
- parllama/widgets/hidden_input.py +59 -0
- parllama/widgets/input_tab_complete.py +44 -0
- parllama/widgets/local_model_list_item.py +68 -0
- parllama/widgets/site_model_list_item.py +73 -0
- parllama/widgets/site_model_list_view.py +20 -0
- parllama-0.2.0.dist-info/METADATA +194 -0
- parllama-0.2.0.dist-info/RECORD +55 -0
- parllama-0.2.0.dist-info/WHEEL +5 -0
- parllama-0.2.0.dist-info/entry_points.txt +2 -0
- parllama-0.2.0.dist-info/top_level.txt +1 -0
parllama/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""PAR LLAMA TUI - A terminal user interface for Ollama."""
|
|
2
|
+
|
|
3
|
+
__author__ = "Paul Robello"
|
|
4
|
+
__copyright__ = "Copyright 2024, Paul Robello"
|
|
5
|
+
__credits__ = ["Paul Robello"]
|
|
6
|
+
__maintainer__ = "Paul Robello"
|
|
7
|
+
__email__ = "probello@gmail.com"
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
__licence__ = "MIT"
|
|
10
|
+
__application_title__ = "PAR LLAMA"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__: list[str] = [
|
|
14
|
+
"__author__",
|
|
15
|
+
"__copyright__",
|
|
16
|
+
"__credits__",
|
|
17
|
+
"__maintainer__",
|
|
18
|
+
"__email__",
|
|
19
|
+
"__version__",
|
|
20
|
+
"__licence__",
|
|
21
|
+
"__application_title__",
|
|
22
|
+
]
|
parllama/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""The main entry point for the application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from parllama.app import ParLlamaApp
|
|
8
|
+
from parllama.models.settings_data import settings
|
|
9
|
+
|
|
10
|
+
if os.environ.get("DEBUG"):
|
|
11
|
+
import pydevd_pycharm # type: ignore
|
|
12
|
+
|
|
13
|
+
pydevd_pycharm.settrace(
|
|
14
|
+
"localhost", port=12345, suspend=False, patch_multiprocessing=True
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run() -> None:
|
|
19
|
+
"""Run the application."""
|
|
20
|
+
print(f"Settings folder {settings.data_dir}")
|
|
21
|
+
ParLlamaApp().run()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
run()
|
parllama/app.py
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"""The main application class."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
from queue import Empty, Queue
|
|
6
|
+
from typing import Any, Dict, Iterator, List
|
|
7
|
+
|
|
8
|
+
import ollama
|
|
9
|
+
import pyperclip # type: ignore
|
|
10
|
+
from rich.columns import Columns
|
|
11
|
+
from rich.console import ConsoleRenderable, RenderableType, RichCast
|
|
12
|
+
from rich.progress_bar import ProgressBar
|
|
13
|
+
from rich.segment import Segment
|
|
14
|
+
from rich.style import Style
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
from textual import LogGroup, LogVerbosity, on, work
|
|
17
|
+
from textual.app import App
|
|
18
|
+
from textual.binding import Binding
|
|
19
|
+
from textual.color import Color
|
|
20
|
+
from textual.message import Message
|
|
21
|
+
from textual.strip import Strip
|
|
22
|
+
from textual.widget import Widget
|
|
23
|
+
from textual.widgets import Input, Select, TextArea
|
|
24
|
+
|
|
25
|
+
from parllama import __application_title__
|
|
26
|
+
from parllama.data_manager import dm
|
|
27
|
+
from parllama.dialogs.help_dialog import HelpDialog
|
|
28
|
+
from parllama.messages.main import (
|
|
29
|
+
LocalModelCopied,
|
|
30
|
+
LocalModelCopyRequested,
|
|
31
|
+
LocalModelDelete,
|
|
32
|
+
LocalModelDeleted,
|
|
33
|
+
LocalModelListLoaded,
|
|
34
|
+
LocalModelListRefreshRequested,
|
|
35
|
+
ModelPulled,
|
|
36
|
+
ModelPullRequested,
|
|
37
|
+
ModelPushed,
|
|
38
|
+
ModelPushRequested,
|
|
39
|
+
NotifyErrorMessage,
|
|
40
|
+
NotifyInfoMessage,
|
|
41
|
+
PsMessage,
|
|
42
|
+
SendToClipboard,
|
|
43
|
+
SiteModelsLoaded,
|
|
44
|
+
SiteModelsRefreshRequested,
|
|
45
|
+
StatusMessage,
|
|
46
|
+
)
|
|
47
|
+
from parllama.models.jobs import CopyModelJob, PullModelJob, PushModelJob, QueueJob
|
|
48
|
+
from parllama.models.settings_data import settings
|
|
49
|
+
from parllama.par_logger import ParLogger
|
|
50
|
+
from parllama.screens.local_models_screen import LocalModelsScreen
|
|
51
|
+
from parllama.screens.log_screen import LogScreen
|
|
52
|
+
from parllama.screens.model_tools_screen import ModelToolsScreen
|
|
53
|
+
from parllama.screens.site_models_screen import SiteModelsScreen
|
|
54
|
+
from parllama.theme_manager import theme_manager
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ParLlamaApp(App[None]):
|
|
58
|
+
"""Main application class"""
|
|
59
|
+
|
|
60
|
+
TITLE = __application_title__
|
|
61
|
+
BINDINGS = [
|
|
62
|
+
Binding(key="f1", action="help", description="Help", show=True, priority=True),
|
|
63
|
+
Binding(key="ctrl+q", action="app.quit", description="Quit", show=True),
|
|
64
|
+
Binding(
|
|
65
|
+
key="ctrl+l",
|
|
66
|
+
action="app.switch_mode('local')",
|
|
67
|
+
description="Local",
|
|
68
|
+
show=True,
|
|
69
|
+
priority=True,
|
|
70
|
+
),
|
|
71
|
+
Binding(
|
|
72
|
+
key="ctrl+s",
|
|
73
|
+
action="app.switch_mode('site')",
|
|
74
|
+
description="Site",
|
|
75
|
+
show=True,
|
|
76
|
+
priority=True,
|
|
77
|
+
),
|
|
78
|
+
Binding(
|
|
79
|
+
key="ctrl+t",
|
|
80
|
+
action="app.switch_mode('tools')",
|
|
81
|
+
description="Tools",
|
|
82
|
+
show=True,
|
|
83
|
+
priority=True,
|
|
84
|
+
),
|
|
85
|
+
Binding(
|
|
86
|
+
key="ctrl+d",
|
|
87
|
+
action="app.switch_mode('logs')",
|
|
88
|
+
description="Debug",
|
|
89
|
+
show=True,
|
|
90
|
+
priority=True,
|
|
91
|
+
),
|
|
92
|
+
Binding(
|
|
93
|
+
key="f10",
|
|
94
|
+
action="toggle_dark",
|
|
95
|
+
description="Toggle Dark Mode",
|
|
96
|
+
show=True,
|
|
97
|
+
priority=True,
|
|
98
|
+
),
|
|
99
|
+
Binding(key="ctrl+c", action="noop", show=False),
|
|
100
|
+
]
|
|
101
|
+
SCREENS = {}
|
|
102
|
+
|
|
103
|
+
MODES = {"local": "local", "site": "site", "tools": "tools", "logs": "logs"}
|
|
104
|
+
|
|
105
|
+
commands: list[dict[str, str]] = [
|
|
106
|
+
{
|
|
107
|
+
"action": "action_quit",
|
|
108
|
+
"cmd": "Quit Application",
|
|
109
|
+
"help": "Quit the application as soon as possible",
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
CSS_PATH = "app.tcss"
|
|
113
|
+
|
|
114
|
+
DEFAULT_CSS = """
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
local_models_screen: LocalModelsScreen
|
|
118
|
+
site_models_screen: SiteModelsScreen
|
|
119
|
+
model_tools_screen: ModelToolsScreen
|
|
120
|
+
log_screen: LogScreen | None
|
|
121
|
+
job_queue: Queue[QueueJob]
|
|
122
|
+
is_busy: bool = False
|
|
123
|
+
last_status: RenderableType = ""
|
|
124
|
+
|
|
125
|
+
def __init__(self) -> None:
|
|
126
|
+
"""Initialize the application."""
|
|
127
|
+
super().__init__()
|
|
128
|
+
self._logger = ParLogger(self._log)
|
|
129
|
+
|
|
130
|
+
self.title = __application_title__
|
|
131
|
+
self.dark = settings.theme_mode != "light"
|
|
132
|
+
self.design = theme_manager.get_theme(settings.theme_name) # type: ignore
|
|
133
|
+
self.job_queue = Queue[QueueJob]()
|
|
134
|
+
self.is_busy = False
|
|
135
|
+
self.is_refreshing = False
|
|
136
|
+
self.last_status = ""
|
|
137
|
+
self.log_screen = None
|
|
138
|
+
|
|
139
|
+
def _watch_dark(self, value: bool) -> None:
|
|
140
|
+
"""Watch the dark property."""
|
|
141
|
+
settings.theme_mode = "dark" if value else "light"
|
|
142
|
+
settings.save_settings_to_file()
|
|
143
|
+
|
|
144
|
+
def get_css_variables(self) -> dict[str, str]:
|
|
145
|
+
"""Get a mapping of variables used to pre-populate CSS.
|
|
146
|
+
|
|
147
|
+
May be implemented in a subclass to add new CSS variables.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A mapping of variable name to value.
|
|
151
|
+
"""
|
|
152
|
+
return theme_manager.get_color_system_for_theme_mode(
|
|
153
|
+
settings.theme_name, self.dark
|
|
154
|
+
).generate()
|
|
155
|
+
|
|
156
|
+
@on(NotifyInfoMessage)
|
|
157
|
+
def notify_info(self, msg: NotifyInfoMessage) -> None:
|
|
158
|
+
"""Show info toast message for 3 seconds"""
|
|
159
|
+
self.notify(msg.message, timeout=3)
|
|
160
|
+
|
|
161
|
+
@on(NotifyErrorMessage)
|
|
162
|
+
def notify_error(self, msg: NotifyErrorMessage) -> None:
|
|
163
|
+
"""Show error toast message for 6 seconds"""
|
|
164
|
+
self.notify(msg.message, severity="error", timeout=6)
|
|
165
|
+
|
|
166
|
+
async def on_mount(self) -> None:
|
|
167
|
+
"""Display the main or locked screen."""
|
|
168
|
+
self.SCREENS["local"] = LocalModelsScreen()
|
|
169
|
+
self.SCREENS["site"] = SiteModelsScreen()
|
|
170
|
+
self.SCREENS["tools"] = ModelToolsScreen()
|
|
171
|
+
self.SCREENS["logs"] = LogScreen()
|
|
172
|
+
|
|
173
|
+
self._installed_screens.update(**self.SCREENS)
|
|
174
|
+
|
|
175
|
+
self.local_models_screen = self.SCREENS["local"] # type: ignore
|
|
176
|
+
self.site_models_screen = self.SCREENS["site"] # type: ignore
|
|
177
|
+
self.model_tools_screen = self.SCREENS["tools"] # type: ignore
|
|
178
|
+
self.log_screen = self.SCREENS["logs"] # type: ignore
|
|
179
|
+
|
|
180
|
+
await self.switch_mode("local")
|
|
181
|
+
|
|
182
|
+
self.set_timer(1, self.do_jobs)
|
|
183
|
+
self.set_timer(1, self.update_ps)
|
|
184
|
+
|
|
185
|
+
def action_noop(self) -> None:
|
|
186
|
+
"""Do nothing"""
|
|
187
|
+
|
|
188
|
+
def action_help(self) -> None:
|
|
189
|
+
"""Show help screen"""
|
|
190
|
+
self.app.push_screen(HelpDialog())
|
|
191
|
+
|
|
192
|
+
def action_clear_field(self) -> None:
|
|
193
|
+
"""Clear focused widget value"""
|
|
194
|
+
f: Widget | None = self.screen.focused
|
|
195
|
+
if not f:
|
|
196
|
+
return
|
|
197
|
+
if isinstance(f, Input):
|
|
198
|
+
f.value = ""
|
|
199
|
+
|
|
200
|
+
if isinstance(f, TextArea):
|
|
201
|
+
f.text = ""
|
|
202
|
+
|
|
203
|
+
def action_copy_to_clipboard(self) -> None:
|
|
204
|
+
"""Copy focused widget value to clipboard"""
|
|
205
|
+
f: Widget | None = self.screen.focused
|
|
206
|
+
if not f:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if isinstance(f, (Input, Select)):
|
|
210
|
+
self.app.post_message(SendToClipboard(str(f.value) if f.value else ""))
|
|
211
|
+
|
|
212
|
+
if isinstance(f, TextArea):
|
|
213
|
+
self.app.post_message(SendToClipboard(f.selected_text or f.text))
|
|
214
|
+
|
|
215
|
+
def action_cut_to_clipboard(self) -> None:
|
|
216
|
+
"""Cut focused widget value to clipboard"""
|
|
217
|
+
f: Widget | None = self.screen.focused
|
|
218
|
+
if not f:
|
|
219
|
+
return
|
|
220
|
+
if isinstance(f, Input):
|
|
221
|
+
pyperclip.copy(f.value)
|
|
222
|
+
f.value = ""
|
|
223
|
+
if isinstance(f, TextArea):
|
|
224
|
+
pyperclip.copy(f.selected_text or f.text)
|
|
225
|
+
f.text = ""
|
|
226
|
+
|
|
227
|
+
@on(SendToClipboard)
|
|
228
|
+
def send_to_clipboard(self, msg: SendToClipboard) -> None:
|
|
229
|
+
"""Send string to clipboard"""
|
|
230
|
+
# works for remote ssh sessions
|
|
231
|
+
self.copy_to_clipboard(msg.message)
|
|
232
|
+
# works for local sessions
|
|
233
|
+
pyperclip.copy(msg.message)
|
|
234
|
+
if msg.notify:
|
|
235
|
+
self.post_message(NotifyInfoMessage("Value copied to clipboard"))
|
|
236
|
+
|
|
237
|
+
@on(ModelPushRequested)
|
|
238
|
+
def on_model_push_requested(self, msg: ModelPushRequested) -> None:
|
|
239
|
+
"""Push requested model event"""
|
|
240
|
+
self.job_queue.put(PushModelJob(modelName=msg.model_name))
|
|
241
|
+
if self.local_models_screen:
|
|
242
|
+
self.local_models_screen.grid.set_item_loading(msg.model_name, True)
|
|
243
|
+
# self.notify(f"Model push {msg.model_name} requested")
|
|
244
|
+
|
|
245
|
+
# primary_style = Style(
|
|
246
|
+
# color=theme_manager.get_color_system_for_theme_mode(
|
|
247
|
+
# self.theme, self.dark
|
|
248
|
+
# ).primary.rich_color
|
|
249
|
+
# )
|
|
250
|
+
# background_style = Style(
|
|
251
|
+
# color=(
|
|
252
|
+
# theme_manager.get_color_system_for_theme_mode(
|
|
253
|
+
# self.theme, self.dark
|
|
254
|
+
# ).surface
|
|
255
|
+
|
|
256
|
+
@on(LocalModelDelete)
|
|
257
|
+
def on_local_model_delete(self, msg: LocalModelDelete) -> None:
|
|
258
|
+
"""Delete model event"""
|
|
259
|
+
if not dm.delete_model(msg.model_name):
|
|
260
|
+
if self.local_models_screen:
|
|
261
|
+
self.local_models_screen.grid.set_item_loading(msg.model_name, False)
|
|
262
|
+
self.notify(f"Error deleting model {msg.model_name}.", severity="error")
|
|
263
|
+
return
|
|
264
|
+
if self.local_models_screen:
|
|
265
|
+
self.local_models_screen.post_message(LocalModelDeleted(msg.model_name))
|
|
266
|
+
|
|
267
|
+
@on(LocalModelDeleted)
|
|
268
|
+
def on_model_deleted(self, msg: LocalModelDeleted) -> None:
|
|
269
|
+
"""Model deleted event"""
|
|
270
|
+
self.notify(f"Model {msg.model_name} deleted.")
|
|
271
|
+
|
|
272
|
+
@on(ModelPullRequested)
|
|
273
|
+
def on_model_pull_requested(self, msg: ModelPullRequested) -> None:
|
|
274
|
+
"""Pull requested model event"""
|
|
275
|
+
self.job_queue.put(PullModelJob(modelName=msg.model_name))
|
|
276
|
+
if self.local_models_screen:
|
|
277
|
+
self.local_models_screen.grid.set_item_loading(msg.model_name, True)
|
|
278
|
+
# self.notify(f"Model pull {msg.model_name} requested")
|
|
279
|
+
|
|
280
|
+
# primary_style = Style(
|
|
281
|
+
# color=theme_manager.get_color_system_for_theme_mode(
|
|
282
|
+
# self.theme, self.dark
|
|
283
|
+
# ).primary.rich_color
|
|
284
|
+
# )
|
|
285
|
+
# background_style = Style(
|
|
286
|
+
# color=(
|
|
287
|
+
# theme_manager.get_color_system_for_theme_mode(
|
|
288
|
+
# self.theme, self.dark
|
|
289
|
+
# ).surface
|
|
290
|
+
# or Color.parse("#111")
|
|
291
|
+
# ).rich_color
|
|
292
|
+
# )
|
|
293
|
+
# self.screen.post_message(
|
|
294
|
+
# StatusMessage(
|
|
295
|
+
# Columns(
|
|
296
|
+
# [
|
|
297
|
+
# Text.assemble("status", " "),
|
|
298
|
+
# ProgressBar(
|
|
299
|
+
# total=100,
|
|
300
|
+
# completed=50,
|
|
301
|
+
# width=40,
|
|
302
|
+
# style=background_style,
|
|
303
|
+
# complete_style=primary_style,
|
|
304
|
+
# finished_style=primary_style,
|
|
305
|
+
# ),
|
|
306
|
+
# ]
|
|
307
|
+
# )
|
|
308
|
+
# )
|
|
309
|
+
# )
|
|
310
|
+
|
|
311
|
+
@on(LocalModelCopyRequested)
|
|
312
|
+
def on_local_model_copy_requested(self, msg: LocalModelCopyRequested) -> None:
|
|
313
|
+
"""Local model copy request event"""
|
|
314
|
+
self.job_queue.put(
|
|
315
|
+
CopyModelJob(modelName=msg.src_model_name, dstModelName=msg.dst_model_name)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def do_copy_local_model(self, msg: CopyModelJob) -> None:
|
|
319
|
+
"""Copy local model"""
|
|
320
|
+
ret = dm.copy_model(msg.modelName, msg.dstModelName)
|
|
321
|
+
self.local_models_screen.post_message(
|
|
322
|
+
LocalModelCopied(
|
|
323
|
+
src_model_name=msg.modelName,
|
|
324
|
+
dst_model_name=msg.dstModelName,
|
|
325
|
+
success=ret["status"] == "success",
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
@on(LocalModelCopied)
|
|
330
|
+
def on_local_model_copied(self, msg: LocalModelCopied) -> None:
|
|
331
|
+
"""Local model copied event"""
|
|
332
|
+
if msg.success:
|
|
333
|
+
self.notify(f"Model {msg.src_model_name} copied to {msg.dst_model_name}")
|
|
334
|
+
else:
|
|
335
|
+
self.notify(
|
|
336
|
+
f"Copying model {msg.src_model_name} to {msg.dst_model_name} failed",
|
|
337
|
+
severity="error",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
async def do_progress(self, job: QueueJob, res: Iterator[Dict[str, Any]]) -> str:
|
|
341
|
+
"""Update progress bar"""
|
|
342
|
+
try:
|
|
343
|
+
last_status = ""
|
|
344
|
+
for msg in res:
|
|
345
|
+
last_status = msg["status"]
|
|
346
|
+
pb: ProgressBar | None = None
|
|
347
|
+
if "total" in msg and "completed" in msg:
|
|
348
|
+
msg["percent"] = (
|
|
349
|
+
str(int(msg["completed"] / msg["total"] * 100)) + "%"
|
|
350
|
+
)
|
|
351
|
+
primary_style = Style(
|
|
352
|
+
color=theme_manager.get_color_system_for_theme_mode(
|
|
353
|
+
settings.theme_name, self.dark
|
|
354
|
+
).primary.rich_color
|
|
355
|
+
)
|
|
356
|
+
background_style = Style(
|
|
357
|
+
color=(
|
|
358
|
+
theme_manager.get_color_system_for_theme_mode(
|
|
359
|
+
settings.theme_name, self.dark
|
|
360
|
+
).surface
|
|
361
|
+
or Color.parse("#111")
|
|
362
|
+
).rich_color
|
|
363
|
+
)
|
|
364
|
+
pb = ProgressBar(
|
|
365
|
+
total=msg["total"],
|
|
366
|
+
completed=msg["completed"],
|
|
367
|
+
width=25,
|
|
368
|
+
style=background_style,
|
|
369
|
+
complete_style=primary_style,
|
|
370
|
+
finished_style=primary_style,
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
msg["percent"] = ""
|
|
374
|
+
if msg["percent"] and msg["status"] == "success":
|
|
375
|
+
msg["percent"] = "100%"
|
|
376
|
+
parts: List[RenderableType] = [
|
|
377
|
+
Text.assemble(
|
|
378
|
+
job.modelName,
|
|
379
|
+
" ",
|
|
380
|
+
msg["status"],
|
|
381
|
+
" ",
|
|
382
|
+
msg["percent"],
|
|
383
|
+
" ",
|
|
384
|
+
)
|
|
385
|
+
]
|
|
386
|
+
if pb:
|
|
387
|
+
parts.append(pb)
|
|
388
|
+
|
|
389
|
+
self.post_message_all(StatusMessage(Columns(parts)))
|
|
390
|
+
return last_status
|
|
391
|
+
except ollama.ResponseError as e:
|
|
392
|
+
self.post_message_all(
|
|
393
|
+
StatusMessage(Text.assemble(("error:" + str(e), "red")))
|
|
394
|
+
)
|
|
395
|
+
raise e
|
|
396
|
+
|
|
397
|
+
async def do_pull(self, job: PullModelJob) -> None:
|
|
398
|
+
"""Pull a model"""
|
|
399
|
+
try:
|
|
400
|
+
res = dm.pull_model(job.modelName)
|
|
401
|
+
last_status = await self.do_progress(job, res)
|
|
402
|
+
|
|
403
|
+
self.local_models_screen.post_message(
|
|
404
|
+
ModelPulled(model_name=job.modelName, success=last_status == "success")
|
|
405
|
+
)
|
|
406
|
+
except ollama.ResponseError:
|
|
407
|
+
self.post_message_all(ModelPulled(model_name=job.modelName, success=False))
|
|
408
|
+
|
|
409
|
+
async def do_push(self, job: PushModelJob) -> None:
|
|
410
|
+
"""Push a model"""
|
|
411
|
+
try:
|
|
412
|
+
res = dm.push_model(job.modelName)
|
|
413
|
+
last_status = await self.do_progress(job, res)
|
|
414
|
+
|
|
415
|
+
self.post_message_all(
|
|
416
|
+
ModelPushed(model_name=job.modelName, success=last_status == "success")
|
|
417
|
+
)
|
|
418
|
+
except ollama.ResponseError:
|
|
419
|
+
self.post_message_all(ModelPushed(model_name=job.modelName, success=False))
|
|
420
|
+
|
|
421
|
+
@work(group="do_jobs", thread=True)
|
|
422
|
+
async def do_jobs(self) -> None:
|
|
423
|
+
"""Pull all requested models in queue"""
|
|
424
|
+
while True:
|
|
425
|
+
try:
|
|
426
|
+
job: QueueJob = self.job_queue.get(block=True, timeout=1)
|
|
427
|
+
if self._exit:
|
|
428
|
+
return
|
|
429
|
+
if not job:
|
|
430
|
+
continue
|
|
431
|
+
self.is_busy = True
|
|
432
|
+
if isinstance(job, PullModelJob):
|
|
433
|
+
await self.do_pull(job)
|
|
434
|
+
elif isinstance(job, PushModelJob):
|
|
435
|
+
await self.do_push(job)
|
|
436
|
+
elif isinstance(job, CopyModelJob):
|
|
437
|
+
await self.do_copy_local_model(job)
|
|
438
|
+
else:
|
|
439
|
+
self.notify(f"Unknown job type {type(job)}", severity="error")
|
|
440
|
+
except Empty:
|
|
441
|
+
if self._exit:
|
|
442
|
+
return
|
|
443
|
+
if self.is_busy:
|
|
444
|
+
self.post_message(LocalModelListRefreshRequested())
|
|
445
|
+
self.is_busy = False
|
|
446
|
+
continue
|
|
447
|
+
|
|
448
|
+
@on(ModelPulled)
|
|
449
|
+
def on_model_pulled(self, msg: ModelPulled) -> None:
|
|
450
|
+
"""Model pulled event"""
|
|
451
|
+
if msg.success:
|
|
452
|
+
self.notify(f"Model {msg.model_name} pulled.")
|
|
453
|
+
else:
|
|
454
|
+
self.notify(f"Model {msg.model_name} failed to pull.", severity="error")
|
|
455
|
+
|
|
456
|
+
def action_refresh_models(self):
|
|
457
|
+
"""Refresh models action."""
|
|
458
|
+
self.refresh_models()
|
|
459
|
+
|
|
460
|
+
@on(LocalModelListRefreshRequested)
|
|
461
|
+
def on_model_list_refresh_requested(self) -> None:
|
|
462
|
+
"""Model refresh request event"""
|
|
463
|
+
if self.is_refreshing:
|
|
464
|
+
self.notify("A model refresh is already in progress. Please wait.")
|
|
465
|
+
return
|
|
466
|
+
self.refresh_models()
|
|
467
|
+
|
|
468
|
+
@work(group="refresh_models", thread=True)
|
|
469
|
+
async def refresh_models(self):
|
|
470
|
+
"""Refresh the models."""
|
|
471
|
+
self.is_refreshing = True
|
|
472
|
+
try:
|
|
473
|
+
self.post_message_all(StatusMessage("Local model list refreshing..."))
|
|
474
|
+
dm.refresh_models()
|
|
475
|
+
self.screen.post_message(LocalModelListLoaded())
|
|
476
|
+
finally:
|
|
477
|
+
self.is_refreshing = False
|
|
478
|
+
|
|
479
|
+
@on(LocalModelListLoaded)
|
|
480
|
+
def on_model_data_loaded(self) -> None:
|
|
481
|
+
"""Refresh model completed"""
|
|
482
|
+
self.post_message_all(StatusMessage("Local model list refreshed"))
|
|
483
|
+
# self.notify("Local models refreshed.")
|
|
484
|
+
|
|
485
|
+
@on(SiteModelsRefreshRequested)
|
|
486
|
+
def on_site_models_refresh_requested(self, msg: SiteModelsRefreshRequested) -> None:
|
|
487
|
+
"""Site model refresh request event"""
|
|
488
|
+
if self.is_refreshing:
|
|
489
|
+
self.notify("A model refresh is already in progress. Please wait.")
|
|
490
|
+
return
|
|
491
|
+
self.refresh_site_models(msg)
|
|
492
|
+
|
|
493
|
+
@on(SiteModelsLoaded)
|
|
494
|
+
def on_site_models_loaded(self, msg: SiteModelsLoaded) -> None:
|
|
495
|
+
"""Site model refresh completed"""
|
|
496
|
+
self.notify(f"Site models refreshed for {msg.ollama_namespace or 'models'}")
|
|
497
|
+
|
|
498
|
+
@work(group="refresh_site_model", thread=True)
|
|
499
|
+
async def refresh_site_models(self, msg: SiteModelsRefreshRequested):
|
|
500
|
+
"""Refresh the site model."""
|
|
501
|
+
self.is_refreshing = True
|
|
502
|
+
try:
|
|
503
|
+
self.post_message_all(
|
|
504
|
+
StatusMessage(
|
|
505
|
+
f"Site models for {msg.ollama_namespace or 'models'} refreshing... force={msg.force}"
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
dm.refresh_site_models(msg.ollama_namespace, msg.force)
|
|
509
|
+
self.screen.post_message(
|
|
510
|
+
SiteModelsLoaded(ollama_namespace=msg.ollama_namespace)
|
|
511
|
+
)
|
|
512
|
+
self.post_message_all(
|
|
513
|
+
StatusMessage(
|
|
514
|
+
f"Site models for {msg.ollama_namespace or 'models'} loaded. force={msg.force}"
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
finally:
|
|
519
|
+
self.is_refreshing = False
|
|
520
|
+
|
|
521
|
+
@work(group="update_ps", thread=True)
|
|
522
|
+
async def update_ps(self) -> None:
|
|
523
|
+
"""Update ps msg"""
|
|
524
|
+
was_blank = False
|
|
525
|
+
while self.is_running:
|
|
526
|
+
await asyncio.sleep(1)
|
|
527
|
+
ret = dm.model_ps()
|
|
528
|
+
if not ret:
|
|
529
|
+
if not was_blank:
|
|
530
|
+
self.screen.post_message(PsMessage(msg=""))
|
|
531
|
+
was_blank = True
|
|
532
|
+
continue
|
|
533
|
+
was_blank = False
|
|
534
|
+
info = ret[0]
|
|
535
|
+
self.local_models_screen.post_message(
|
|
536
|
+
PsMessage(
|
|
537
|
+
msg=Text.assemble(
|
|
538
|
+
"Name: ",
|
|
539
|
+
info["name"],
|
|
540
|
+
" Size: ",
|
|
541
|
+
info["size"],
|
|
542
|
+
" Processor: ",
|
|
543
|
+
info["processor"],
|
|
544
|
+
" Until: ",
|
|
545
|
+
info["until"],
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
self.post_message_all(StatusMessage(msg="exited..."))
|
|
550
|
+
|
|
551
|
+
def post_message_all(self, msg: Message) -> None:
|
|
552
|
+
"""Post a message to all screens"""
|
|
553
|
+
if isinstance(msg, StatusMessage):
|
|
554
|
+
self.log(msg.msg)
|
|
555
|
+
self.last_status = msg.msg
|
|
556
|
+
|
|
557
|
+
if self.local_models_screen:
|
|
558
|
+
self.local_models_screen.post_message(msg)
|
|
559
|
+
if self.site_models_screen:
|
|
560
|
+
self.site_models_screen.post_message(msg)
|
|
561
|
+
|
|
562
|
+
def _log(
|
|
563
|
+
self,
|
|
564
|
+
group: LogGroup,
|
|
565
|
+
verbosity: LogVerbosity,
|
|
566
|
+
_textual_calling_frame: inspect.Traceback,
|
|
567
|
+
*objects: Any,
|
|
568
|
+
**kwargs,
|
|
569
|
+
) -> None:
|
|
570
|
+
"""Write to logs or devtools.
|
|
571
|
+
|
|
572
|
+
Positional args are logged. Keyword args will be prefixed with the key.
|
|
573
|
+
|
|
574
|
+
Example:
|
|
575
|
+
```python
|
|
576
|
+
data = [1,2,3]
|
|
577
|
+
self.log("Hello, World", state=data)
|
|
578
|
+
self.log(self.tree)
|
|
579
|
+
self.log(locals())
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
verbosity: Verbosity level 0-3.
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
if self.log_screen and self.log_screen.richlog:
|
|
587
|
+
if len(objects) == 1:
|
|
588
|
+
types_to_check = [ConsoleRenderable, RichCast, str]
|
|
589
|
+
renderable: RenderableType | None = None
|
|
590
|
+
if isinstance(objects[0], Columns):
|
|
591
|
+
renderable = objects[0].renderables[0]
|
|
592
|
+
else:
|
|
593
|
+
# make sure we have a renderable type
|
|
594
|
+
for ttc in types_to_check:
|
|
595
|
+
if isinstance(objects[0], ttc):
|
|
596
|
+
renderable = objects[0] # type: ignore
|
|
597
|
+
break
|
|
598
|
+
if renderable:
|
|
599
|
+
# dont log duplicate messages
|
|
600
|
+
if len(self.log_screen.richlog.lines) > 0:
|
|
601
|
+
if (
|
|
602
|
+
Strip.from_lines(
|
|
603
|
+
list(
|
|
604
|
+
Segment.split_lines(self.console.render(renderable))
|
|
605
|
+
)
|
|
606
|
+
)[0].text
|
|
607
|
+
!= self.log_screen.richlog.lines[-1].text
|
|
608
|
+
):
|
|
609
|
+
self.log_screen.richlog.write(renderable)
|
|
610
|
+
return
|
|
611
|
+
else:
|
|
612
|
+
self.log_screen.richlog.write(renderable)
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
super()._log(group, verbosity, _textual_calling_frame, *objects, **kwargs)
|