Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ basilisk_config.yml
*.mo
user_data/
coverage.xml
.worktrees/
3 changes: 2 additions & 1 deletion basilisk/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from .conversation_profile import (
get_conversation_profile_config as conversation_profiles,
)
from .main_config import BasiliskConfig
from .main_config import BasiliskConfig, TemplatesSettings
from .main_config import get_basilisk_config as conf

__all__ = [
Expand All @@ -37,4 +37,5 @@
"KeyStorageMethodEnum",
"LogLevelEnum",
"ReleaseChannelEnum",
"TemplatesSettings",
]
9 changes: 9 additions & 0 deletions basilisk/config/main_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from datetime import datetime
from functools import cache
from pathlib import Path

from pydantic import BaseModel, Field, model_validator

Expand Down Expand Up @@ -81,6 +82,13 @@ class NetworkSettings(BaseModel):
use_system_cert_store: bool = Field(default=True)


class TemplatesSettings(BaseModel):
"""Template path settings."""

html_message_template_path: Path | None = Field(default=None)
html_export_template_path: Path | None = Field(default=None)


class BasiliskConfig(BasiliskBaseSettings):
"""BasiliskLLM configuration settings."""

Expand All @@ -94,6 +102,7 @@ class BasiliskConfig(BasiliskBaseSettings):
recordings: RecordingsSettings = Field(default_factory=RecordingsSettings)
network: NetworkSettings = Field(default_factory=NetworkSettings)
server: ServerSettings = Field(default_factory=ServerSettings)
templates: TemplatesSettings = Field(default_factory=TemplatesSettings)

@model_validator(mode="before")
@classmethod
Expand Down
2 changes: 2 additions & 0 deletions basilisk/global_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
resource_path = base_path / Path("res")
# sounds directory (contains sound, etc.)
sounds_path = resource_path / "sounds"
# templates directory (contains default Mako templates)
templates_path = resource_path / "templates"

# command-line arguments parsed by the application
args = None
Expand Down
34 changes: 34 additions & 0 deletions basilisk/presenters/base_conversation_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import basilisk.config as config
from basilisk.services.account_model_service import AccountModelService
from basilisk.services.template_service import TemplateService

if TYPE_CHECKING:
from basilisk.provider_engine.base_engine import BaseEngine
Expand Down Expand Up @@ -84,6 +85,39 @@ def get_display_models(self, engine: BaseEngine | None) -> list[tuple]:
return []
return [m.display_model for m in engine.models]

def render_system_prompt(
self,
profile: config.ConversationProfile,
account: config.Account | None,
model,
) -> str:
Comment thread
clementb49 marked this conversation as resolved.
"""Render the profile's system_prompt as a Mako template.

Falls back to the raw string on error, showing a log warning.

Args:
profile: The conversation profile whose system_prompt to render.
account: Active account (injected as context variable).
model: Active model (injected as context variable).

Returns:
The rendered prompt string, or the original on template error.
"""
template_str = profile.system_prompt or ""
if not template_str:
return template_str
try:
return TemplateService.render_system_prompt(
template_str, profile, account, model
)
except ValueError:
log.warning(
"Failed to render system prompt template for profile %r",
profile.name,
exc_info=True,
)
return template_str

def resolve_account_and_model(
self,
profile: config.ConversationProfile,
Expand Down
23 changes: 23 additions & 0 deletions basilisk/presenters/conversation_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from basilisk.provider_ai_model import AIModelInfo
from basilisk.provider_capability import ProviderCapability
from basilisk.services.conversation_service import ConversationService
from basilisk.services.template_service import TemplateService
from basilisk.sound_manager import play_sound, stop_sound

if TYPE_CHECKING:
Expand Down Expand Up @@ -143,6 +144,28 @@ def on_stop_completion(self):
"""Stop the current completion."""
self.completion_handler.stop_completion()

def export_to_html(self, path: str) -> None:
"""Export the current conversation to an HTML file.

Args:
path: Destination file path for the HTML export.
"""
template_path = config.conf().templates.html_export_template_path
profile = getattr(self.view, "current_profile", None)
html = TemplateService.render_conversation_export(
self.conversation, profile, template_path
)
try:
with open(path, "w", encoding="utf-8") as f:
f.write(html)
except OSError as exc:
self.view.show_error(
# Translators: Error shown when conversation export fails
_("Failed to export conversation: {error}").format(error=exc),
# Translators: Title for the export error dialog
_("Export error"),
)
Comment thread
clementb49 marked this conversation as resolved.

def get_system_message(self) -> SystemMessage | None:
"""Get the system message from the view's system prompt input."""
system_prompt = self.view.system_prompt_txt.GetValue()
Expand Down
19 changes: 19 additions & 0 deletions basilisk/presenters/conversation_profile_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from basilisk.config import ConversationProfile
from basilisk.presenters.presenter_mixins import ManagerCrudMixin
from basilisk.services.template_service import TemplateService

if TYPE_CHECKING:
from basilisk.config.conversation_profile import ConversationProfileManager
Expand Down Expand Up @@ -41,6 +42,24 @@ def __init__(self, view, profile: ConversationProfile | None = None):
self.view = view
self.profile = profile

def preview_system_prompt(self) -> str:
"""Render the current system_prompt field as a Mako template preview.

Returns:
The rendered string, or an error message if rendering fails.
"""
template_str = self.view.system_prompt_txt.GetValue()
try:
return TemplateService.render_system_prompt(
template_str,
self.profile,
self.view.current_account,
self.view.current_model,
)
Comment thread
clementb49 marked this conversation as resolved.
except ValueError as exc:
# Translators: Error message shown in the system prompt preview
return _("Template error: {error}").format(error=exc)

def validate_and_build_profile(self) -> ConversationProfile | None:
"""Validate inputs and build a ConversationProfile.

Expand Down
17 changes: 17 additions & 0 deletions basilisk/presenters/main_frame_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,23 @@ def save_conversation_as(self, file_path: str) -> bool:
current_tab.bskc_path = file_path
return success

def export_conversation(self) -> None:
"""Export the current conversation to an HTML file chosen by the user."""
tab = self.view.current_tab
if not tab:
return
title = tab.GetLabel() or _("conversation")
with wx.FileDialog(
self.view,
# Translators: Dialog title when exporting a conversation to HTML
_("Export conversation as HTML"),
defaultFile=f"{title}.html",
wildcard="HTML files (*.html)|*.html",
style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT,
Comment thread
clementb49 marked this conversation as resolved.
Outdated
) as dlg:
if dlg.ShowModal() == wx.ID_OK:
tab.presenter.export_to_html(dlg.GetPath())

Comment thread
clementb49 marked this conversation as resolved.
Outdated
# -- Name conversation --

def name_conversation(self, auto: bool = False):
Expand Down
6 changes: 6 additions & 0 deletions basilisk/presenters/preferences_presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ def on_ok(self) -> None:
)
conf.server.enable = self.view.server_enable.GetValue()
conf.server.port = int(self.view.server_port.GetValue())
conf.templates.html_message_template_path = (
self.view.html_message_template_path.get_path()
)
conf.templates.html_export_template_path = (
self.view.html_export_template_path.get_path()
)

conf.save()
set_log_level(conf.general.log_level.name)
Expand Down
73 changes: 73 additions & 0 deletions basilisk/res/templates/conversation_export.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${conversation.title or _("Conversation") | h}</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; background: #fff; }
article { margin-bottom: 2rem; border-radius: 6px; padding: 1rem 1.25rem; }
article.user { background: #f0f4ff; }
article.assistant { background: #f6faf3; }
.meta { font-size: 0.8rem; color: #666; margin-top: 0.5rem; }
pre { background: #f4f4f4; padding: 0.75rem; border-radius: 4px; overflow-x: auto; }
code { font-family: monospace; }
@media (prefers-color-scheme: dark) {
body { background: #1a1a1a; color: #e8e8e8; }
article.user { background: #1e2a40; }
article.assistant { background: #1a2d1a; }
.meta { color: #aaa; }
pre { background: #2a2a2a; }
}
</style>
</head>
<body>
<header>
<h1>${conversation.title or _("Conversation") | h}</h1>
% if profile:
<p class="meta">${_("Profile")}: ${profile.name | h}</p>
% endif
</header>
<main role="log" aria-label="${_('Conversation history')}">
% for block in conversation.messages:
<article class="user" aria-label="${_('User message')}">
<div>${block.request.content | h}</div>
% if block.request.attachments:
% for att in block.request.attachments:
<%
mime = att.mime_type or "application/octet-stream"
is_image = mime.startswith("image/")
b64 = att.encoded_data if hasattr(att, "encoded_data") else ""
%>
% if is_image:
<img src="data:${mime};base64,${b64}" alt="${att.name | h}" style="max-width:100%">
% else:
<a href="#" onclick="downloadAttachment('${b64}','${att.name | h}','${mime}');return false">${_("Download")} ${att.name | h}</a>
% endif
% endfor
% endif
</article>
% if block.response:
<article class="assistant" aria-label="${_('Assistant message')}">
<div>${block.response.content | h}</div>
<p class="meta">
${block.model.name | h} &mdash;
<time datetime="${block.created_at.isoformat()}">${block.created_at.strftime('%Y-%m-%d %H:%M')}</time>
</p>
</article>
% endif
% endfor
</main>
<script>
function downloadAttachment(b64, filename, mime) {
const bytes = atob(b64);
const arr = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
const blob = new Blob([arr], {type: mime});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
</script>
</body>
</html>
10 changes: 10 additions & 0 deletions basilisk/res/templates/html_message.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${title | h}</title>
</head>
<body>
${content}
</body>
</html>
Loading