Skip to content

Commit d99aa00

Browse files
authored
Merge pull request #39 from rmusser01/dev
Sync from Dev to Main
2 parents 49e94ba + 8b255d4 commit d99aa00

File tree

16 files changed

+780
-245
lines changed

16 files changed

+780
-245
lines changed

.github/workflows/python-app.yml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,8 @@ jobs:
2626
- name: Install dependencies
2727
run: |
2828
python -m pip install --upgrade pip
29-
pip install flake8 pytest
29+
pip install pytest
3030
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31-
- name: Lint with flake8
32-
run: |
33-
# stop the build if there are Python syntax errors or undefined names
34-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
35-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
36-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3731
- name: Test with pytest
3832
run: |
3933
pytest

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ If you have general questions, feel free to reach out on the [tldw_Project Disco
3939

4040
## Coding Style
4141

42-
* Please follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) for Python code. (I try)
42+
* Please follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) for Python code. (I try. I also try to use pylint but that's a whole rant.)
4343
* Use clear and descriptive variable and function names.
4444
* Add comments to your code where necessary to explain complex logic. over explained versus mysterious and demure.
4545
* Ensure your code is well-formatted. Consider using a tool like Black or Ruff. (Is appreciated, even though I don't use it currently, I may in the future)

Docs/Design/Chat_Page.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Chat Page Design Document
2+
3+
## Table of Contents
4+
- [Overview](#overview)
5+
- [UI Structure](#ui-structure)
6+
- [Components](#components)
7+
- [Event Handling](#event-handling)
8+
- [Data Management](#data-management)
9+
- [Integration with Backend](#integration-with-backend)
10+
- [Testing](#testing)
11+
- [Future Enhancements](#future-enhancements)
12+
- [Conclusion](#conclusion)
13+
- [References](#references)
14+
15+
16+
Below is LLM generated content, will revisit later to ensure it is accurate and complete.
17+
18+
------------------------------------------------------------
19+
### Overview
20+
- **101**
21+
- The Chat Page is a central component of the `tldw_chatbook` application, designed to facilitate user interactions with chat functionalities.
22+
- It provides a user-friendly interface for managing conversations, viewing chat history, and interacting with characters.
23+
- **Goals**
24+
- To create an intuitive and responsive chat interface that enhances user experience.
25+
- To ensure seamless integration with backend services for real-time chat functionalities.
26+
- To support character-specific interactions and manage chat history effectively.
27+
28+
29+
------------------------------------
30+
### UI Structure-
31+
- **Layout**
32+
- The Chat Page is structured to include a header, main content area, and footer.
33+
- The header contains the title and navigation options.
34+
- The main content area is divided into sections for chat history, input area, and character management.
35+
- The footer includes status indicators and additional controls.
36+
- **Responsive Design**
37+
- The layout is designed to be responsive, adapting to different screen sizes and orientations.
38+
- It uses flexible grid and box layouts to ensure components resize and reposition appropriately.
39+
- **Accessibility**
40+
- The UI is designed with accessibility in mind, ensuring that all components are navigable via keyboard and screen readers.
41+
- Color contrasts and font sizes are chosen to enhance readability for all users.
42+
- **Styling**
43+
- The Chat Page uses a consistent color scheme and typography that aligns with the overall application design.
44+
- Custom CSS styles are applied to enhance the visual appeal and usability of components.
45+
46+
---------------------------------------
47+
### Components
48+
- **Chat History**
49+
- Displays a list of previous messages in the conversation.
50+
- Supports scrolling and lazy loading for large chat histories.
51+
- **Message Input**
52+
- A text input field for users to type their messages.
53+
- Includes features like auto-complete, emoji support, and formatting options.
54+
- **Character Management**
55+
- Allows users to select and manage characters associated with the chat.
56+
- Includes options to view character details, switch characters, and manage character-specific settings.

Docs/Design/User_Settings.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# User_Settings
2+
3+
4+
-----------------
5+
## Table of Contents
6+
- [User Settings Overview](#user-settings-overview)
7+
- [Settings Structure](#settings-structure)
8+
- [Settings Management](#settings-management)
9+
- [Settings UI](#settings-ui)
10+
- [Settings Persistence](#settings-persistence)
11+
- [Settings Validation](#settings-validation)
12+
- [Settings Synchronization](#settings-synchronization)
13+
- [Settings Security](#settings-security)
14+
15+
------------------
16+
17+
Gist is that any item with a `# USER-SETTING` comment is a user setting that can be configured via the Settings tab of the app.
18+
- Goal is to allow the user to tweak application behavior and appearance without modifying the code or config files directly.
19+
20+

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ httpx
1717
#nltk
1818
#scikit-learn
1919
# LLM
20-
20+
vllm
21+
# transformers
2122
# Audio

static/PoC-Frontpage.PNG

20.9 KB
Loading

tldw_chatbook/Constants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@
1919
TAB_SEARCH = "search"
2020
TAB_INGEST = "ingest"
2121
TAB_TOOLS_SETTINGS = "tools_settings"
22-
TAB_LLM = "llm"
22+
TAB_LLM = "llm_management"
2323
TAB_STATS = "stats"
2424
TAB_LOGS = "logs"
25+
TAB_EVALS = "evals"
2526
ALL_TABS = [TAB_CHAT, TAB_CCP, TAB_NOTES, TAB_MEDIA, TAB_SEARCH, TAB_INGEST,
26-
TAB_TOOLS_SETTINGS, TAB_LLM, TAB_LOGS, TAB_STATS]
27+
TAB_TOOLS_SETTINGS, TAB_LLM, TAB_LOGS, TAB_STATS, TAB_EVALS]
2728

2829

2930
# --- CSS definition ---

tldw_chatbook/Event_Handlers/llm_management_events.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from pathlib import Path
3535
from typing import TYPE_CHECKING, List, Optional
3636

37+
from textual.containers import Container
3738
from textual.css.query import QueryError
3839
from textual.worker import Worker, WorkerState
3940
from textual.widgets import Input, RichLog, TextArea
@@ -57,9 +58,12 @@
5758
"handle_vllm_browse_python_button_pressed",
5859
"handle_vllm_browse_model_button_pressed",
5960
"handle_start_vllm_server_button_pressed",
61+
"handle_stop_vllm_server_button_pressed",
6062
# ─── Model download ───────────────────────────────────────────────────────
6163
"handle_browse_models_dir_button_pressed",
6264
"handle_start_model_download_button_pressed",
65+
# ─── Ollama ───────────────────────────────────────────────────────────────
66+
"handle_ollama_nav_button_pressed",
6367
]
6468

6569
###############################################################################
@@ -220,6 +224,7 @@ def run_vllm_server_worker(app_instance: "TldwCli", command: List[str]):
220224

221225
logger = getattr(app_instance, "loguru_logger", logging.getLogger(__name__))
222226
logger.info("vLLM worker begins: %s", " ".join(command))
227+
app_instance.vllm_server_process = None # Clear any old process reference
223228

224229
try:
225230
process = subprocess.Popen(
@@ -230,6 +235,7 @@ def run_vllm_server_worker(app_instance: "TldwCli", command: List[str]):
230235
bufsize=1,
231236
universal_newlines=True,
232237
)
238+
app_instance.vllm_server_process = process # Store the process
233239

234240
app_instance.call_from_thread(
235241
app_instance._update_vllm_log, f"vLLM server started (PID: {process.pid})…\n"
@@ -238,15 +244,19 @@ def run_vllm_server_worker(app_instance: "TldwCli", command: List[str]):
238244
process.wait()
239245
yield f"vLLM server exited with code: {process.returncode}\n"
240246
except FileNotFoundError:
247+
app_instance.vllm_server_process = None # Clear process on error
241248
msg = f"ERROR: vLLM interpreter not found: {command[0]}\n"
242249
logger.error(msg.rstrip())
243250
app_instance.call_from_thread(app_instance._update_vllm_log, msg)
244251
yield msg
245252
except Exception as err:
253+
app_instance.vllm_server_process = None # Clear process on error
246254
msg = f"ERROR in vLLM worker: {err}\n"
247255
logger.error(msg.rstrip(), exc_info=True)
248256
app_instance.call_from_thread(app_instance._update_vllm_log, msg)
249257
yield msg
258+
finally:
259+
app_instance.vllm_server_process = None # Ensure process is cleared
250260

251261

252262
def run_model_download_worker(app_instance: "TldwCli", command: List[str]):
@@ -590,6 +600,48 @@ async def handle_start_vllm_server_button_pressed(app: "TldwCli") -> None:
590600
app.notify("Error setting up vLLM server start.", severity="error")
591601

592602

603+
async def handle_stop_vllm_server_button_pressed(app: "TldwCli") -> None:
604+
"""Stops the vLLM server process if it's running."""
605+
logger = getattr(app, "loguru_logger", logging.getLogger(__name__))
606+
logger.info("User requested to stop vLLM server.")
607+
608+
log_output_widget = app.query_one("#vllm-log-output", RichLog)
609+
610+
if hasattr(app, "vllm_server_process") and app.vllm_server_process:
611+
process = app.vllm_server_process
612+
if process.poll() is None: # Process is running
613+
logger.info(f"Stopping vLLM server process (PID: {process.pid}).")
614+
log_output_widget.write(f"Stopping vLLM server (PID: {process.pid})...\n")
615+
process.terminate() # or process.kill()
616+
try:
617+
process.wait(timeout=10) # Wait for up to 10 seconds
618+
logger.info("vLLM server process terminated.")
619+
log_output_widget.write("vLLM server stopped.\n")
620+
app.notify("vLLM server stopped.")
621+
except subprocess.TimeoutExpired:
622+
logger.warning("Timeout waiting for vLLM server to terminate. Killing.")
623+
log_output_widget.write("vLLM server did not stop gracefully, killing...\n")
624+
process.kill()
625+
process.wait() # Ensure it's killed
626+
log_output_widget.write("vLLM server killed.\n")
627+
app.notify("vLLM server killed after timeout.", severity="warning")
628+
except Exception as e: # pylint: disable=broad-except
629+
logger.error(f"Error during vLLM server termination: {e}", exc_info=True)
630+
log_output_widget.write(f"Error stopping vLLM server: {e}\n")
631+
app.notify(f"Error stopping vLLM server: {e}", severity="error")
632+
finally:
633+
app.vllm_server_process = None
634+
else:
635+
logger.info("vLLM server process was found but is not running.")
636+
log_output_widget.write("vLLM server is not currently running.\n")
637+
app.notify("vLLM server is not running.", severity="warning")
638+
app.vllm_server_process = None # Clear the stale process reference
639+
else:
640+
logger.info("No vLLM server process found to stop.")
641+
log_output_widget.write("vLLM server is not currently running.\n")
642+
app.notify("vLLM server is not running.", severity="warning")
643+
644+
593645
###############################################################################
594646
# ─── Model download UI helpers ──────────────────────────────────────────────
595647
###############################################################################
@@ -666,3 +718,41 @@ async def handle_start_model_download_button_pressed(app: "TldwCli") -> None:
666718
except Exception as err: # pragma: no cover
667719
logger.error("Error preparing model download: %s", err, exc_info=True)
668720
app.notify("Error setting up model download.", severity="error")
721+
722+
723+
###############################################################################
724+
# ─── Ollama UI helpers ──────────────────────────────────────────────────────
725+
###############################################################################
726+
727+
728+
async def handle_ollama_nav_button_pressed(app: "TldwCli") -> None:
729+
"""Handle the Ollama navigation button press."""
730+
logger = getattr(app, "loguru_logger", logging.getLogger(__name__))
731+
logger.debug("Ollama nav button pressed.")
732+
733+
try:
734+
content_pane = app.query_one("#llm-content-pane", Container)
735+
view_areas = content_pane.query(".llm-view-area")
736+
737+
for view in view_areas:
738+
if view.id: # Only hide if it has an ID
739+
logger.debug(f"Hiding view #{view.id}")
740+
view.styles.display = "none"
741+
else: # pragma: no cover
742+
logger.warning("Found a .llm-view-area without an ID, not hiding it.")
743+
744+
ollama_view = app.query_one("#llm-view-ollama", Container)
745+
logger.debug(f"Showing view #{ollama_view.id}")
746+
ollama_view.styles.display = "block"
747+
#app.notify("Switched to Ollama view.")
748+
749+
except QueryError as e: # pragma: no cover
750+
logger.error(f"QueryError in handle_ollama_nav_button_pressed: {e}", exc_info=True)
751+
app.notify("Error switching to Ollama view: Could not find required UI elements.", severity="error")
752+
except Exception as e: # pragma: no cover
753+
logger.error(f"Unexpected error in handle_ollama_nav_button_pressed: {e}", exc_info=True)
754+
app.notify("An unexpected error occurred while switching to Ollama view.", severity="error")
755+
756+
#
757+
# End of llm_management_events.py
758+
########################################################################################################################

tldw_chatbook/Event_Handlers/llm_nav_events.py

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,79 @@
1313
if TYPE_CHECKING:
1414
from ..app import TldwCli # pragma: no cover – runtime import only
1515

16+
# Import the specific handler
17+
from .llm_management_events import handle_ollama_nav_button_pressed
18+
1619
__all__ = [
1720
"handle_llm_nav_button_pressed",
1821
]
1922

2023
async def handle_llm_nav_button_pressed(app: "TldwCli", button_id: str) -> None:
2124
"""
2225
Handles the navigation button presses in the LLM Management tab.
23-
26+
Dispatches to specific handlers if available, otherwise uses a generic approach.
27+
2428
Args:
2529
app: The TldwCli app instance
2630
button_id: The ID of the button that was pressed
2731
"""
2832
logger = getattr(app, "loguru_logger", logging.getLogger(__name__))
2933
logger.info(f"LLM nav button pressed: {button_id}")
30-
34+
3135
# Map button IDs to view IDs
3236
view_to_activate = button_id.replace("llm-nav-", "llm-view-")
3337
logger.debug(f"Activating LLM view: {view_to_activate}")
34-
38+
3539
try:
3640
# Update app's reactive property to show the selected view
3741
app.llm_active_view = view_to_activate
38-
42+
3943
# Remove active class from all nav buttons
4044
nav_pane = app.query_one("#llm-nav-pane")
4145
for nav_button in nav_pane.query(".llm-nav-button"):
4246
nav_button.remove_class("-active")
43-
44-
# Add active class to the clicked button
47+
except QueryError as e:
48+
logger.error(f"Could not query #llm-nav-pane or .llm-nav-button: {e}", exc_info=True)
49+
# Proceeding because view switching might still work
50+
51+
# Add active class to the specifically clicked button
52+
try:
4553
clicked_button = app.query_one(f"#{button_id}", Button)
4654
clicked_button.add_class("-active")
47-
48-
logger.info(f"Successfully switched to LLM view: {view_to_activate}")
4955
except QueryError as e:
50-
logger.error(f"UI component not found during LLM view switch: {e}", exc_info=True)
51-
except Exception as e:
52-
logger.error(f"Unexpected error in handle_llm_nav_button_pressed: {e}", exc_info=True)
56+
logger.error(f"Could not query clicked button #{button_id}: {e}", exc_info=True)
57+
# Proceeding because view switching might still work
58+
59+
# Specific handlers
60+
if button_id == "llm-nav-ollama":
61+
await handle_ollama_nav_button_pressed(app)
62+
# The reactive variable app.llm_active_view should also be set if the
63+
# specific handler doesn't do it, to keep external state consistent.
64+
# However, handle_ollama_nav_button_pressed directly manipulates view display.
65+
# For consistency, we might want specific handlers to also update app.llm_active_view.
66+
# For now, let's assume specific handlers manage the view entirely.
67+
# If watch_llm_active_view is still active, this might cause double view changes or conflicts.
68+
# The handle_ollama_nav_button_pressed already sets display properties.
69+
# To prevent conflict with the watcher, we might avoid setting app.llm_active_view here for this specific case,
70+
# OR ensure the watcher is idempotent / specific handlers also set app.llm_active_view.
71+
# For now, let the specific handler do its job. The watcher will run if app.llm_active_view changes.
72+
# If handle_ollama_nav_button_pressed doesn't change app.llm_active_view, the watcher won't re-hide other views.
73+
# This seems acceptable.
74+
# Add other specific handlers here:
75+
# elif button_id == "llm-nav-another":
76+
# await handle_another_llm_nav_button_pressed(app)
77+
else:
78+
# Generic fallback for other LLM navigation buttons
79+
view_to_activate = button_id.replace("llm-nav-", "llm-view-")
80+
logger.debug(f"Using generic activation for LLM view: {view_to_activate}")
81+
try:
82+
# Update app's reactive property to show the selected view
83+
# This relies on the watcher `watch_llm_active_view` in app.py
84+
app.llm_active_view = view_to_activate
85+
logger.info(f"Successfully set app.llm_active_view to: {view_to_activate} for generic handling.")
86+
except Exception as e: # Catch errors related to setting reactive or if watcher fails
87+
logger.error(f"Error in generic LLM view activation for '{view_to_activate}': {e}", exc_info=True)
88+
89+
# Note: The original code had a single try-except.
90+
# Splitting it can help isolate where QueryErrors happen (nav pane vs content pane switching).
91+
# The specific handlers (like handle_ollama_nav_button_pressed) have their own try-except.

0 commit comments

Comments
 (0)