aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--llamachat/ui.py33
-rwxr-xr-xtest_llamachat.py4
2 files changed, 33 insertions, 4 deletions
diff --git a/llamachat/ui.py b/llamachat/ui.py
index f29b39c..2d4ded5 100644
--- a/llamachat/ui.py
+++ b/llamachat/ui.py
@@ -260,6 +260,7 @@ class StreamWorker(QObject):
usage_all = Signal(str) # every round's usage block, as a JSON array
search_start = Signal(str)
search_done = Signal(str)
+ skill_loaded = Signal(str)
finished = Signal()
failed = Signal(str)
@@ -269,12 +270,14 @@ class StreamWorker(QObject):
model: str,
messages: list[dict],
search_cfg: SearchConfig | None = None,
+ skills_cfg: backend.SkillsConfig | None = None,
):
super().__init__()
self.client = client
self.model = model
self.messages = messages
self.search_cfg = search_cfg
+ self.skills_cfg = skills_cfg
self._stop = False
self._usage_blocks: list[str] = []
@@ -296,7 +299,7 @@ class StreamWorker(QObject):
return
try:
stream = client.stream_chat(
- wire, self.messages, self.search_cfg
+ wire, self.messages, self.search_cfg, self.skills_cfg
)
for kind, piece in stream:
if self._stop:
@@ -307,6 +310,8 @@ class StreamWorker(QObject):
self.search_start.emit(piece)
elif kind == "search_done":
self.search_done.emit(piece)
+ elif kind == "skill_loaded":
+ self.skill_loaded.emit(piece)
elif kind == "usage":
stats = json.loads(piece)
# A searched turn makes several API calls (one per search
@@ -603,6 +608,15 @@ class ChatWindow(QMainWindow):
top.addWidget(self.search_box)
outer.addLayout(top)
+ # Loaded-skills chips, hidden unless at least one skill is loaded.
+ self.skills_bar = QWidget()
+ self.skills_layout = QHBoxLayout(self.skills_bar)
+ self.skills_layout.setContentsMargins(8, 0, 8, 0)
+ self.skills_layout.setSpacing(6)
+ self.skill_chips: dict[str, QPushButton] = {}
+ self.skills_bar.setVisible(False)
+ outer.addWidget(self.skills_bar)
+
# Body: history list beside the transcript and input.
self.splitter = QSplitter(Qt.Horizontal)
splitter = self.splitter
@@ -1298,6 +1312,12 @@ class ChatWindow(QMainWindow):
self._refresh_skill_chips()
return True
+ @Slot(str)
+ def _on_skill_loaded(self, name: str) -> None:
+ """A skill the model just loaded via its tool, recorded for later."""
+ if self._load_skill(name):
+ self.show_status(f"Loaded skill: {name}")
+
def _unload_skill(self, name: str) -> None:
if name not in self.loaded_skills:
return
@@ -1451,7 +1471,8 @@ class ChatWindow(QMainWindow):
self.thread = QThread(self)
self.worker = StreamWorker(
- self.client, model, messages, self._search_config()
+ self.client, model, messages,
+ self._search_config(), self._skills_config(),
)
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
@@ -1459,6 +1480,7 @@ class ChatWindow(QMainWindow):
self.worker.reasoning.connect(self._on_reasoning)
self.worker.search_start.connect(self._on_search_start)
self.worker.search_done.connect(self._on_search_done)
+ self.worker.skill_loaded.connect(self._on_skill_loaded)
self.worker.usage.connect(self._on_usage)
self.worker.usage_all.connect(self._on_usage_all)
self.worker.finished.connect(self._on_stream_finished)
@@ -1567,6 +1589,10 @@ class ChatWindow(QMainWindow):
@Slot()
def _on_stream_finished(self) -> None:
+ # One-shot mode: a skill loaded mid-reply is gone when the reply ends.
+ if self.mode == MODE_ONESHOT:
+ self.loaded_skills = []
+ self._refresh_skill_chips()
if self.assistant_message_id is not None:
self.history.update_message(
self.assistant_message_id,
@@ -1868,6 +1894,9 @@ class ChatWindow(QMainWindow):
self.prompt_custom = _column(session, "prompt_custom")
self.select_prompt(_column(session, "prompt_name"))
+ self.loaded_skills = _skills_list(_column(session, "skills"))
+ self._refresh_skill_chips()
+
self.oneshot_radio.blockSignals(True)
self.chat_radio.blockSignals(True)
self.oneshot_radio.setChecked(self.mode == MODE_ONESHOT)
diff --git a/test_llamachat.py b/test_llamachat.py
index 85e69a6..d96b599 100755
--- a/test_llamachat.py
+++ b/test_llamachat.py
@@ -2157,7 +2157,7 @@ def test_usage_accumulation():
def wire_name(self, model):
return model
- def stream_chat(self, wire, messages, search_cfg=None):
+ def stream_chat(self, wire, messages, search_cfg=None, skills_cfg=None):
# One usage block per round: a tool round, then the final answer.
yield ("usage", '{"prompt_tokens": 100, "completion_tokens": 5,'
' "total_tokens": 105}')
@@ -2177,7 +2177,7 @@ def test_usage_accumulation():
# A turn with no usage chunk at all (no include_usage support) yields
# an empty array, which the window stores as NULL.
class SilentClient(FakeClient):
- def stream_chat(self, wire, messages, search_cfg=None):
+ def stream_chat(self, wire, messages, search_cfg=None, skills_cfg=None):
yield ("content", "hi")
return