New features and changes for 0.2.0

This commit is contained in:
jeffser 2024-05-13 13:25:46 -06:00
parent 0614ddc39b
commit 190bf7017f
6 changed files with 266 additions and 67 deletions

View File

@ -65,7 +65,7 @@
{
"type" : "git",
"url" : "https://github.com/Jeffser/Alpaca.git",
"tag": "0.1.2"
"tag": "0.2.0"
}
]
}

View File

@ -35,7 +35,7 @@
<screenshots>
<screenshot type="default">
<image>https://jeffser.com/images/alpaca/screenie1.png</image>
<caption>Login into an Ollama instance</caption>
<caption>Welcome dialog</caption>
</screenshot>
<screenshot>
<image>https://jeffser.com/images/alpaca/screenie2.png</image>
@ -46,13 +46,29 @@
<caption>Managing models</caption>
</screenshot>
</screenshots>
<content_rating type="oars-1.1">
<content_attribute id="money-purchasing">mild</content_attribute>
</content_rating>
<content_rating type="oars-1.1" />
<url type="bugtracker">https://github.com/Jeffser/Alpaca/issues</url>
<url type="homepage">https://github.com/Jeffser/Alpaca</url>
<url type="donation">https://github.com/sponsors/Jeffser</url>
<releases>
<release version="0.2.0" date="2024-05-14">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.2.0</url>
<description>
<p>0.2.0</p>
<p>Big Update</p>
<p>New Features</p>
<ul>
<li>Restore chat after closing the app</li>
<li>A button to clear the chat</li>
<li>Fixed multiple bugs involving how messages are shown</li>
<li>Added welcome dialog</li>
<li>More stability</li>
</ul>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<release version="0.1.2" date="2024-05-13">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.1.2</url>
<description>

View File

@ -1,5 +1,5 @@
project('Alpaca',
version: '0.1.2',
version: '0.2.0',
meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ],
)

View File

@ -33,8 +33,9 @@ class AlpacaApplication(Adw.Application):
super().__init__(application_id='com.jeffser.Alpaca',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>q'])
self.create_action('clear', lambda *_: AlpacaWindow.clear_conversation_dialog(self.props.active_window), ['<primary>e'])
self.create_action('reconnect', lambda *_: AlpacaWindow.show_connection_dialog(self.props.active_window), ['<primary>r'])
self.create_action('about', self.on_about_action)
self.create_action('preferences', self.on_preferences_action)
def do_activate(self):
win = self.props.active_window
@ -47,16 +48,13 @@ class AlpacaApplication(Adw.Application):
application_name='Alpaca',
application_icon='com.jeffser.Alpaca',
developer_name='Jeffry Samuel Eduarte Rojas',
version='0.1.2',
version='0.2.0',
developers=['Jeffser https://jeffser.com'],
designers=['Jeffser https://jeffser.com'],
copyright='© 2024 Jeffser',
issue_url='https://github.com/Jeffser/Alpaca/issues')
about.present()
def on_preferences_action(self, widget, _):
print('app.preferences action activated')
def create_action(self, name, callback, shortcuts=None):
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)

View File

@ -20,7 +20,7 @@
import gi
gi.require_version("Soup", "3.0")
from gi.repository import Adw, Gtk, GLib
import json, requests, threading
import json, requests, threading, os
from datetime import datetime
from .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake
from .available_models import available_models
@ -32,12 +32,20 @@ class AlpacaWindow(Adw.ApplicationWindow):
#Variables
ollama_url = None
local_models = []
messages_history = []
#In the future I will at multiple chats, for now I'll save it like this so that past chats don't break in the future
current_chat_id="0"
chats = {"chats": {"0": {"messages": []}}}
#Elements
bot_message : Gtk.TextBuffer = None
connection_dialog = Gtk.Template.Child()
connection_carousel = Gtk.Template.Child()
connection_previous_button = Gtk.Template.Child()
connection_next_button = Gtk.Template.Child()
connection_url_entry = Gtk.Template.Child()
overlay = Gtk.Template.Child()
chat_container = Gtk.Template.Child()
chat_window = Gtk.Template.Child()
message_entry = Gtk.Template.Child()
send_button = Gtk.Template.Child()
model_drop_down = Gtk.Template.Child()
@ -58,7 +66,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
)
self.overlay.add_toast(toast)
def show_message(self, msg:str, bot:bool):
def show_message(self, msg:str, bot:bool, footer:str=None):
message_text = Gtk.TextView(
editable=False,
focusable=False,
@ -71,6 +79,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
)
message_buffer = message_text.get_buffer()
message_buffer.insert(message_buffer.get_end_iter(), msg)
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer))
message_box = Adw.Bin(
child=message_text,
css_classes=["card" if bot else None]
@ -88,87 +98,79 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.local_models.append(model["name"])
self.model_drop_down.set_selected(0)
return
#IF IT CONTINUES THEN THERE WAS EN ERROR
else:
self.show_toast(response['text'])
self.show_connection_dialog(True)
def verify_connection(self):
response = simple_get(self.ollama_url)
if response['status'] == 'ok':
if "Ollama is running" in response['text']:
with open("server.conf", "w+") as f: f.write(self.ollama_url)
self.message_entry.grab_focus_without_selecting()
self.update_list_local_models()
return True
else:
response = {"status": "error", "text": f"Unexpected response from {self.ollama_url} : {response['text']}"}
self.show_toast(response['text'])
self.show_connection_dialog()
return False
def dialog_response(self, dialog, task):
self.ollama_url = dialog.get_extra_child().get_text()
if dialog.choose_finish(task) == "login":
response = simple_get(self.ollama_url)
if response['status'] == 'ok':
if "Ollama is running" in response['text']:
self.message_entry.grab_focus_without_selecting()
self.update_list_local_models()
return
else:
response = {"status": "error", "text": f"Unexpected response from {self.ollama_url} : {response['text']}"}
#IF IT CONTINUES THEN THERE WAS EN ERROR
self.show_toast(response['text'])
self.show_connection_dialog()
self.verify_connection()
else:
self.destroy()
def show_connection_dialog(self):
dialog = Adw.AlertDialog(
heading="Login",
body="Please enter the Ollama instance URL",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("login", "Login")
dialog.set_response_appearance("login", Adw.ResponseAppearance.SUGGESTED)
entry = Gtk.Entry(text="http://localhost:11434") #FOR TESTING PURPOSES
dialog.set_extra_child(entry)
dialog.choose(parent = self, cancellable = None, callback = self.dialog_response)
def update_bot_message(self, data):
if data['done']:
try:
api_datetime = data['created_at']
api_datetime = api_datetime[:-4] + api_datetime[-1]
formated_datetime = datetime.strptime(api_datetime, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y/%m/%d %H:%M")
text = f"\n\n<small>{data['model']}\t|\t{formated_datetime}</small>"
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
except Exception as e: print(e)
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
text = f"\n\n<small>{data['model']}\t|\t{formated_datetime}</small>"
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
vadjustment = self.chat_window.get_vadjustment()
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
self.save_history()
self.bot_message = None
else:
if self.bot_message is None:
GLib.idle_add(self.show_message, data['message']['content'], True)
self.messages_history.append({
self.chats["chats"][self.current_chat_id]["messages"].append({
"role": "assistant",
"model": data['model'],
"date": datetime.now().strftime("%Y/%m/%d %H:%M"),
"content": data['message']['content']
})
else:
GLib.idle_add(self.bot_message.insert_at_cursor, data['message']['content'], len(data['message']['content']))
self.messages_history[-1]['content'] += data['message']['content']
#else: GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.chats["chats"][self.current_chat_id]["messages"][-1]['content'] += data['message']['content']
def send_message(self):
current_model = self.model_drop_down.get_selected_item()
if current_model is None:
GLib.idle_add(self.show_toast, "Please pull a model")
return
self.messages_history.append({
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
self.chats["chats"][self.current_chat_id]["messages"].append({
"role": "user",
"model": "User",
"date": formated_datetime,
"content": self.message_entry.get_text()
})
data = {
"model": current_model.get_string(),
"messages": self.messages_history
"messages": self.chats["chats"][self.current_chat_id]["messages"]
}
GLib.idle_add(self.message_entry.set_sensitive, False)
GLib.idle_add(self.send_button.set_sensitive, False)
GLib.idle_add(self.show_message, self.message_entry.get_text(), False)
GLib.idle_add(self.show_message, self.message_entry.get_text(), False, f"\n\n<small>{formated_datetime}</small>")
self.save_history()
GLib.idle_add(self.message_entry.get_buffer().set_text, "", 0)
response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps(data), callback=self.update_bot_message)
GLib.idle_add(self.send_button.set_sensitive, True)
GLib.idle_add(self.message_entry.set_sensitive, True)
if response['status'] == 'error':
self.show_toast(f"{response['text']}")
self.show_connection_dialog()
self.show_connection_dialog(True)
def send_button_activate(self, button):
if not self.message_entry.get_text(): return
@ -193,7 +195,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
else:
self.show_toast(response['text'])
self.manage_models_dialog.close()
self.show_connection_dialog()
self.show_connection_dialog(True)
def pull_model_update(self, data):
try:
@ -220,7 +222,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
else:
GLib.idle_add(self.show_toast, response['text'])
GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.show_connection_dialog)
GLib.idle_add(self.show_connection_dialog, True)
def pull_model_start(self, dialog, task, model_name, button):
@ -230,7 +232,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
def model_action_button_activate(self, button, model_name):
action = list(set(button.get_css_classes()) & set(["delete", "pull"]))[0]
print(f"action: {action}")
dialog = Adw.AlertDialog(
heading=f"{action.capitalize()} Model",
body=f"Are you sure you want to {action} '{model_name}'?",
@ -267,6 +268,75 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.manage_models_dialog.present(self)
self.update_list_available_models()
def connection_carousel_page_changed(self, carousel, index):
if index == 0: self.connection_previous_button.set_sensitive(False)
else: self.connection_previous_button.set_sensitive(True)
if index == carousel.get_n_pages()-1: self.connection_next_button.set_label("Connect")
else: self.connection_next_button.set_label("Next")
def connection_previous_button_activate(self, button):
self.connection_carousel.scroll_to(self.connection_carousel.get_nth_page(self.connection_carousel.get_position()-1), True)
def connection_next_button_activate(self, button):
if button.get_label() == "Next": self.connection_carousel.scroll_to(self.connection_carousel.get_nth_page(self.connection_carousel.get_position()+1), True)
else:
self.ollama_url = self.connection_url_entry.get_text()
if self.verify_connection():
self.connection_dialog.force_close()
else:
show_connection_dialog(True)
def show_connection_dialog(self, error:bool=False):
self.connection_carousel.scroll_to(self.connection_carousel.get_nth_page(self.connection_carousel.get_n_pages()-1),False)
if error: self.connection_url_entry.set_css_classes(["error"])
else: self.connection_url_entry.set_css_classes([])
self.connection_dialog.present(self)
def clear_conversation(self):
for widget in list(self.chat_container): self.chat_container.remove(widget)
self.chats["chats"][self.current_chat_id]["messages"] = []
def clear_conversation_dialog_response(self, dialog, task):
if dialog.choose_finish(task) == "empty":
self.clear_conversation()
self.save_history()
def clear_conversation_dialog(self):
if self.bot_message is not None:
self.show_toast("Conversation cannot be cleared while receiving a message")
return
dialog = Adw.AlertDialog(
heading=f"Clear Conversation",
body=f"Are you sure you want to clear the conversation?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("empty", "Empty")
dialog.set_response_appearance("empty", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose(
parent = self,
cancellable = None,
callback = self.clear_conversation_dialog_response
)
def save_history(self):
with open("chats.json", "w+") as f:
json.dump(self.chats, f, indent=4)
def load_history(self):
if os.path.exists("chats.json"):
self.clear_conversation()
try:
with open("chats.json", "r") as f:
self.chats = json.load(f)
except Exception as e:
self.chats = {"chats": {"0": {"messages": []}}}
for message in self.chats['chats'][self.current_chat_id]['messages']:
if message['role'] == 'user':
self.show_message(message['content'], False, f"\n\n<small>{message['date']}</small>")
else:
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>")
self.bot_message = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -274,8 +344,18 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.send_button.connect("clicked", self.send_button_activate)
self.set_default_widget(self.send_button)
self.message_entry.set_activates_default(self.send_button)
self.message_entry.set_text("Hi") #FOR TESTING PURPOSES
self.show_connection_dialog()
self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed)
self.connection_previous_button.connect("clicked", self.connection_previous_button_activate)
self.connection_next_button.connect("clicked", self.connection_next_button_activate)
self.connection_url_entry.connect("changed", lambda entry: entry.set_css_classes([]))
self.connection_dialog.connect("close-attempt", lambda dialog: self.destroy())
self.load_history()
if os.path.exists("server.conf"):
with open("server.conf", "r") as f:
self.ollama_url = f.read()
if self.verify_connection() is False: self.show_connection_dialog()
else: self.connection_dialog.present(self)

View File

@ -36,7 +36,6 @@
</object>
</child>
</object>
</property>
<child type="end">
<object class="GtkMenuButton">
@ -185,16 +184,122 @@
</object>
</child>
</object>
<object class="AdwDialog" id="connection_dialog">
<property name="can-close">false</property>
<property name="width-request">450</property>
<property name="height-request">450</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
</object>
</child>
<child type="bottom">
<object class="GtkActionBar">
<property name="hexpand">true</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-bottom">5</property>
<child type="start">
<object class="GtkButton" id="connection_previous_button">
<property name="tooltip-text" translatable="yes">Previous</property>
<property name="label">Previous</property>
<property name="sensitive">false</property>
<style>
<class name="raised"/>
</style>
</object>
</child>
<child type="center">
<object class="AdwCarouselIndicatorDots">
<property name="carousel">connection_carousel</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="connection_next_button">
<property name="tooltip-text" translatable="yes">Next</property>
<property name="label">Next</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwCarousel" id="connection_carousel">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="allow-long-swipes">true</property>
<property name="allow-scroll-wheel">true</property>
<property name="spacing">12</property>
<child>
<object class="AdwStatusPage">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="icon-name">com.jeffser.Alpaca</property>
<property name="title" translatable="yes">Welcome to Alpaca</property>
<property name="description" translatable="yes">To get started, please ensure you have an Ollama instance set up. You can either run Ollama locally on your machine or connect to a remote instance.</property>
<child>
<object class="GtkLinkButton">
<property name="label" translatable="true">Ollama Website</property>
<property name="uri">https://ollama.com/</property>
<property name="margin-top">12</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwStatusPage">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="icon-name">dialog-warning-symbolic</property>
<property name="title" translatable="yes">Disclaimer</property>
<property name="description" translatable="yes">Alpaca and its developers are not liable for any damages to devices or software resulting from the execution of code generated by an AI model. Please exercise caution and review the code carefully before running it.</property>
</object>
</child>
<child>
<object class="AdwStatusPage">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="icon-name">preferences-other-symbolic</property>
<property name="title" translatable="yes">Setup</property>
<property name="description" translatable="yes">If you are running an Ollama instance locally and haven't modified the default ports, you can use the default URL. Otherwise, please enter the URL of your Ollama instance.</property>
<child>
<object class="GtkEntry" id="connection_url_entry">
<property name="text">http://localhost:11434</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</template>
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">app.preferences</attribute>
<attribute name="label" translatable="yes">_Clear Conversation</attribute>
<attribute name="action">app.clear</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
<attribute name="label" translatable="yes">_Change Server</attribute>
<attribute name="action">app.reconnect</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_About Alpaca</attribute>