first working view of extensions

This commit is contained in:
Saifeddine ALOUI 2023-10-06 01:23:47 +02:00
parent 0191e92f4a
commit 3157922fbf
9 changed files with 469 additions and 85 deletions

View File

@ -13,6 +13,7 @@ from api.db import DiscussionsDB, Discussion
from pathlib import Path
from lollms.config import InstallOption
from lollms.types import MSG_TYPE, SENDER_TYPES
from lollms.extension import LOLLMSExtension
from lollms.personality import AIPersonality, PersonalityBuilder
from lollms.binding import LOLLMSConfig, BindingBuilder, LLMBinding, ModelBuilder
from lollms.paths import LollmsPaths
@ -959,6 +960,78 @@ class LoLLMsAPPI(LollmsApplication):
self.config["active_personality_id"]=0
return mounted_personalities
def rebuild_extensions(self, reload_all=False):
if reload_all:
self.mounted_extensions=[]
loaded = self.mounted_extensions
loaded_names = [f"{p.category}/{p.extension_folder_name}" for p in loaded]
mounted_extensions=[]
ASCIIColors.success(f" ╔══════════════════════════════════════════════════╗ ")
ASCIIColors.success(f" ║ Building mounted Extensions ║ ")
ASCIIColors.success(f" ╚══════════════════════════════════════════════════╝ ")
to_remove=[]
for i,extension in enumerate(self.config['extensions']):
ASCIIColors.yellow(f" {extension}")
if extension in loaded_names:
mounted_extensions.append(loaded[loaded_names.index(extension)])
else:
personality_path = self.lollms_paths.personalities_zoo_path/f"{extension}"
try:
extension = LOLLMSExtension()
mounted_extensions.append(extension)
except Exception as ex:
ASCIIColors.error(f"Personality file not found or is corrupted ({personality_path}).\nReturned the following exception:{ex}\nPlease verify that the personality you have selected exists or select another personality. Some updates may lead to change in personality name or category, so check the personality selection in settings to be sure.")
ASCIIColors.info("Trying to force reinstall")
if self.config["debug"]:
print(ex)
try:
personality = AIPersonality(
personality_path,
self.lollms_paths,
self.config,
self.model,
app = self,
run_scripts=True,
selected_language=personality.split(":")[1] if ":" in personality else None,
installation_option=InstallOption.FORCE_INSTALL)
mounted_extensions.append(personality)
except Exception as ex:
ASCIIColors.error(f"Couldn't load personality at {personality_path}")
trace_exception(ex)
ASCIIColors.info(f"Unmounting personality")
to_remove.append(i)
personality = AIPersonality(None,
self.lollms_paths,
self.config,
self.model,
run_scripts=True,
installation_option=InstallOption.FORCE_INSTALL)
mounted_extensions.append(personality)
ASCIIColors.info("Reverted to default personality")
if self.config["active_personality_id"]>=0 and self.config["active_personality_id"]<len(self.config["personalities"]):
ASCIIColors.success(f'selected model : {self.config["personalities"][self.config["active_personality_id"]]}')
else:
ASCIIColors.warning('An error was encountered while trying to mount personality')
ASCIIColors.success(f" ╔══════════════════════════════════════════════════╗ ")
ASCIIColors.success(f" ║ Done ║ ")
ASCIIColors.success(f" ╚══════════════════════════════════════════════════╝ ")
# Sort the indices in descending order to ensure correct removal
to_remove.sort(reverse=True)
# Remove elements from the list based on the indices
for index in to_remove:
if 0 <= index < len(mounted_extensions):
mounted_extensions.pop(index)
self.config["personalities"].pop(index)
ASCIIColors.info(f"removed personality {personality_path}")
if self.config["active_personality_id"]>=len(self.config["personalities"]):
self.config["active_personality_id"]=0
return mounted_extensions
# ================================== LOLLMSApp
#properties

141
app.py
View File

@ -263,6 +263,10 @@ class LoLLMsWebUI(LoLLMsAPPI):
self.add_endpoint("/mount_personality", "mount_personality", self.p_mount_personality, methods=["POST"])
self.add_endpoint("/remount_personality", "remount_personality", self.p_remount_personality, methods=["POST"])
self.add_endpoint("/mount_extension", "mount_extension", self.p_mount_extension, methods=["POST"])
self.add_endpoint("/remount_extension", "remount_extension", self.p_remount_extension, methods=["POST"])
self.add_endpoint("/unmount_personality", "unmount_personality", self.p_unmount_personality, methods=["POST"])
self.add_endpoint("/select_personality", "select_personality", self.p_select_personality, methods=["POST"])
@ -1025,7 +1029,7 @@ class LoLLMsWebUI(LoLLMsAPPI):
if category_folder.is_dir() and not category_folder.stem.startswith('.'):
extensions[category_folder.name] = []
for extensions_folder in category_folder.iterdir():
pers = extensions_folder.stem
ext = extensions_folder.stem
if extensions_folder.is_dir() and not extensions_folder.stem.startswith('.'):
extension_info = {"folder":extensions_folder.stem}
config_path = extensions_folder / 'card.yaml'
@ -1042,7 +1046,7 @@ class LoLLMsWebUI(LoLLMsAPPI):
extension_info['help'] = config_data.get('help', '')
real_assets_path = extensions_folder/ 'assets'
assets_path = Path("extensions") / cat / pers / 'assets'
assets_path = Path("extensions") / cat / ext / 'assets'
gif_logo_path = assets_path / 'logo.gif'
webp_logo_path = assets_path / 'logo.webp'
png_logo_path = assets_path / 'logo.png'
@ -1129,18 +1133,6 @@ class LoLLMsWebUI(LoLLMsAPPI):
extensions_categories = [f.stem for f in extensions_categories_dir.iterdir() if f.is_dir() and not f.name.startswith(".")]
return jsonify(extensions_categories)
def list_extensions(self):
category = request.args.get('category')
if not category:
return jsonify([])
try:
extensions_dir = self.lollms_paths.extensions_zoo_path/f'{category}' # replace with the actual path to the models folder
extensions = [f.stem for f in extensions_dir.iterdir() if f.is_dir() and not f.name.startswith(".")]
except Exception as ex:
extensions=[]
ASCIIColors.error(f"No extensions found. Using default one {ex}")
return jsonify(extensions)
def list_discussions(self):
discussions = self.db.get_discussions()
@ -1601,6 +1593,8 @@ class LoLLMsWebUI(LoLLMsAPPI):
return jsonify({"status":False, 'error':str(ex)})
def p_mount_personality(self):
print("- Mounting personality")
try:
@ -1798,6 +1792,125 @@ class LoLLMsWebUI(LoLLMsAPPI):
return jsonify({'status':False})
def p_mount_extension(self):
print("- Mounting extension")
try:
data = request.get_json()
# Further processing of the data
except Exception as e:
print(f"Error occurred while parsing JSON: {e}")
return
category = data['category']
name = data['folder']
language = data.get('language', None)
package_path = f"{category}/{name}"
package_full_path = self.lollms_paths.extensions_zoo_path/package_path
config_file = package_full_path / "config.yaml"
if config_file.exists():
self.config["extensions"].append(package_path)
self.mounted_extensions = self.rebuild_extensions()
ASCIIColors.success("ok")
if self.config.auto_save:
ASCIIColors.info("Saving configuration")
self.config.save_config()
ASCIIColors.error("Mounted successfully")
return jsonify({"status": True,
"extensions":self.config["extensions"],
})
else:
pth = str(config_file).replace('\\','/')
ASCIIColors.error(f"nok : Personality not found @ {pth}")
ASCIIColors.yellow(f"Available personalities: {[p.name for p in self.mounted_personalities]}")
return jsonify({"status": False, "error":f"Personality not found @ {pth}"})
def p_remount_extension(self):
print("- Remounting extension")
try:
data = request.get_json()
# Further processing of the data
except Exception as e:
print(f"Error occurred while parsing JSON: {e}")
return
category = data['category']
name = data['folder']
package_path = f"{category}/{name}"
package_full_path = self.lollms_paths.extensions_zoo_path/package_path
config_file = package_full_path / "config.yaml"
if config_file.exists():
ASCIIColors.info(f"Unmounting personality {package_path}")
index = self.config["extensions"].index(f"{category}/{name}")
self.config["extensions"].remove(f"{category}/{name}")
if len(self.config["extensions"])>0:
self.mounted_personalities = self.rebuild_extensions()
else:
self.personalities = ["generic/lollms"]
self.mounted_personalities = self.rebuild_extensions()
ASCIIColors.info(f"Mounting personality {package_path}")
self.config["personalities"].append(package_path)
self.mounted_personalities = self.rebuild_extensions()
ASCIIColors.success("ok")
if self.config["active_personality_id"]<0:
return jsonify({"status": False,
"personalities":self.config["personalities"],
"active_personality_id":self.config["active_personality_id"]
})
else:
return jsonify({"status": True,
"personalities":self.config["personalities"],
"active_personality_id":self.config["active_personality_id"]
})
else:
pth = str(config_file).replace('\\','/')
ASCIIColors.error(f"nok : Personality not found @ {pth}")
ASCIIColors.yellow(f"Available personalities: {[p.name for p in self.mounted_personalities]}")
return jsonify({"status": False, "error":f"Personality not found @ {pth}"})
def p_unmount_extension(self):
print("- Unmounting extension ...")
try:
data = request.get_json()
# Further processing of the data
except Exception as e:
print(f"Error occurred while parsing JSON: {e}")
return
category = data['category']
name = data['folder']
language = data.get('language',None)
try:
personality_id = f"{category}/{name}" if language is None else f"{category}/{name}:{language}"
index = self.config["personalities"].index(personality_id)
self.config["extensions"].remove(personality_id)
self.mounted_extensions = self.rebuild_extensions()
ASCIIColors.success("ok")
if self.config.auto_save:
ASCIIColors.info("Saving configuration")
self.config.save_config()
return jsonify({
"status": True,
"extensions":self.config["extensions"]
})
except:
if language:
ASCIIColors.error(f"nok : Personality not found @ {category}/{name}:{language}")
else:
ASCIIColors.error(f"nok : Personality not found @ {category}/{name}")
ASCIIColors.yellow(f"Available personalities: {[p.name for p in self.mounted_personalities]}")
return jsonify({"status": False, "error":"Couldn't unmount personality"})
def set_active_binding_settings(self):
print("- Setting binding settings")
try:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
web/dist/index.html vendored
View File

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LoLLMS WebUI - Welcome</title>
<script type="module" crossorigin src="/assets/index-1bc15114.js"></script>
<link rel="stylesheet" href="/assets/index-5306ea0b.css">
<script type="module" crossorigin src="/assets/index-5dd957eb.js"></script>
<link rel="stylesheet" href="/assets/index-1e156293.css">
</head>
<body>
<div id="app"></div>

View File

@ -3,16 +3,16 @@
class=" min-w-96 items-start p-4 hover:bg-primary-light rounded-lg mb-2 shadow-lg border-2 cursor-pointer select-none"
tabindex="-1"
:class="selected_computed ? 'border-2 border-primary-light' : 'border-transparent', isMounted ? 'bg-blue-200 dark:bg-blue-700':''"
:title="!personality.installed ? 'Not installed' : ''">
:title="!extension.installed ? 'Not installed' : ''">
<div :class="!personality.installed ? 'opacity-50' : ''">
<div :class="!extension.installed ? 'border-red-500' : ''">
<div class="flex flex-row items-center flex-shrink-0 gap-3">
<img @click="toggleSelected" ref="imgElement" :src="getImgUrl()" @error="defaultImg($event)"
class="w-10 h-10 rounded-full object-fill text-red-700 cursor-pointer">
<!-- :class="personality.installed ? 'grayscale-0':'grayscale'" -->
<!-- :class="extension.installed ? 'grayscale-0':'grayscale'" -->
<h3 @click="toggleSelected" class="font-bold font-large text-lg line-clamp-3 cursor-pointer">
{{ personality.name }}
{{ extension.name }}
</h3>
</div>
<div class="">
@ -22,16 +22,16 @@
<i data-feather="user" class="w-5 m-1"></i>
<b>Author:&nbsp;</b>
{{ personality.author }}
{{ extension.author }}
</div>
<div v-if="personality.languages && select_language" class="flex items-center">
<div v-if="extension.languages && select_language" class="flex items-center">
<i data-feather="globe" class="w-5 m-1"></i>
<b>Languages:&nbsp;</b>
<select id="languages" v-model ="personality.language"
<select id="languages" v-model ="extension.language"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option v-for="(item, index) in personality.languages" :key="index"
:selected="item == personality.languages[0]">{{
<option v-for="(item, index) in extension.languages" :key="index"
:selected="item == extension.languages[0]">{{
item
}}
@ -39,16 +39,16 @@
</select>
</div>
<div v-if="personality.language" class="flex items-center">
<div v-if="extension.language" class="flex items-center">
<i data-feather="globe" class="w-5 m-1"></i>
<b>Language:&nbsp;</b>
{{ personality.language }}
{{ extension.language }}
</div>
<div class="flex items-center">
<i data-feather="bookmark" class="w-5 m-1"></i>
<b>Category:&nbsp;</b>
{{ personality.category }}
{{ extension.category }}
</div>
</div>
@ -56,7 +56,7 @@
<i data-feather="info" class="w-5 m-1"></i>
<b>Description:&nbsp;</b><br>
</div>
<p class="mx-1 opacity-80 h-20 overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary" :title="personality.description">{{ personality.description }}</p>
<p class="mx-1 opacity-80 h-20 overflow-y-auto scrollbar-thin scrollbar-track-bg-light-tone scrollbar-thumb-bg-light-tone-panel hover:scrollbar-thumb-primary dark:scrollbar-track-bg-dark-tone dark:scrollbar-thumb-bg-dark-tone-panel dark:hover:scrollbar-thumb-primary active:scrollbar-thumb-secondary" :title="extension.description">{{ extension.description }}</p>
</div>
<div class="rounded bg-blue-300">
<button v-if="isMounted" type="button" title="Select"
@ -94,7 +94,7 @@ import InteractiveMenu from "@/components/InteractiveMenu.vue"
const bUrl = import.meta.env.VITE_LOLLMS_API_BASEURL
export default {
props: {
personality: {},
extension: {},
select_language: Boolean,
selected: Boolean,
full_path: String,
@ -112,7 +112,7 @@ export default {
data() {
return {
isMounted: false,
name: this.personality.name,
name: this.extension.name,
};
},
computed:{
@ -124,7 +124,7 @@ export default {
if(this.isMounted){
main_menu.push({name:"remount", icon: "feather:refresh-ccw", is_file:false, value:this.reMount})
}
if(this.selected && this.personality.has_scripts){
if(this.selected && this.extension.has_scripts){
main_menu.push({name:"settings", icon: "feather:settings", is_file:false, value:this.toggleSettings})
}
return main_menu
@ -135,7 +135,7 @@ export default {
},
mounted() {
this.isMounted = this.personality.isMounted
this.isMounted = this.extension.isMounted
nextTick(() => {
feather.replace()
@ -145,7 +145,7 @@ export default {
},
methods: {
getImgUrl() {
return bUrl + this.personality.avatar
return bUrl + this.extension.avatar
},
defaultImg(event) {
event.target.src = botImgPlaceholder

View File

@ -5,7 +5,7 @@
:class="selected_computed ? 'border-2 border-primary-light' : 'border-transparent', isMounted ? 'bg-blue-200 dark:bg-blue-700':''"
:title="!personality.installed ? 'Not installed' : ''">
<div :class="!personality.installed ? 'opacity-50' : ''">
<div :class="!personality.installed ? 'border-red-500' : ''">
<div class="flex flex-row items-center flex-shrink-0 gap-3">
<img @click="toggleSelected" ref="imgElement" :src="getImgUrl()" @error="defaultImg($event)"

View File

@ -154,6 +154,13 @@ export const store = createStore({
//let personality_path_infos = await this.api_get_req("get_current_personality_path_infos")
configFile.personality_category = personality_path_infos[0]
configFile.personality_folder = personality_path_infos[1]
if (configFile.extensions.length>0){
configFile.extension_category = configFile.extensions[-1]
}
else{
configFile.extension_category = "ai_sensors"
}
console.log("Recovered config")
console.log(configFile)
console.log("Committing config");
@ -275,7 +282,7 @@ export const store = createStore({
let extensions = []
let catdictionary = await api_get_req("list_extensions")
const catkeys = Object.keys(catdictionary); // returns categories
console.log("Extensions recovered:"+this.state.config.extensions)
console.log("Extensions recovered:"+catdictionary)
for (let j = 0; j < catkeys.length; j++) {
const catkey = catkeys[j];

View File

@ -1711,17 +1711,17 @@
<div class="overflow-y-auto no-scrollbar p-2 pb-0 grid lg:grid-cols-3 md:grid-cols-2 gap-4"
:class="pzl_collapsed ? '' : 'max-h-96'">
<TransitionGroup name="bounce">
<ExtensionEntry ref="extensionsZoo" v-for="(pers, index) in extensionsFiltererd"
:key="'index-' + index + '-' + pers.name" :personality="pers"
<ExtensionEntry ref="extensionsZoo" v-for="(ext, index) in extensionsFiltererd"
:key="'index-' + index + '-' + ext.name" :extension="ext"
:select_language="true"
:full_path="pers.full_path"
:selected="configFile.active_personality_id == configFile.personalities.findIndex(item => item === pers.full_path || item === pers.full_path+':'+pers.language)"
:on-selected="onPersonalitySelected"
:on-mount="mountPersonality"
:on-un-mount="unmountPersonality"
:on-remount="remountPersonality"
:on-reinstall="onPersonalityReinstall"
:on-settings="onSettingsPersonality" />
:full_path="ext.full_path"
:selected="configFile.active_personality_id == configFile.personalities.findIndex(item => item === ext.full_path)"
:on-selected="onExtensionSelected"
:on-mount="mountExtension"
:on-un-mount="unmountExtension"
:on-remount="remountExtension"
:on-reinstall="onExtensionReinstall"
:on-settings="onSettingsExtension" />
</TransitionGroup>
</div>
</div>
@ -2352,13 +2352,19 @@ export default {
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.configFile.personality_category)
// this.personalitiesFiltered.sort()
//mountedPersArr
console.log("Extensions zoo")
console.log(this.$store.state.extensionsZoo)
this.modelsFiltered = this.models
this.extension_category = this.configFile.extension_category
this.extensionsFiltererd = this.$store.state.extensionsZoo.filter((item) => item.category === this.configFile.extension_category )
//this.bindings = await this.api_get_req("list_bindings")
// this.bindingsArr.sort((a, b) => a.name.localeCompare(b.name))
this.isLoading = false
this.isMounted = true
this.extension_category = this.configFile.extension_category
},
@ -2504,6 +2510,7 @@ export default {
this.$store.dispatch('refreshDiskUsage');
this.$store.dispatch('refreshRamUsage');
},
async onPersonalitySelected(pers) {
console.log('on pers', pers)
// eslint-disable-next-line no-unused-vars
@ -2542,6 +2549,52 @@ export default {
}
nextTick(() => {
feather.replace()
})
}
},
async onExtensionSelected(ext) {
console.log('on ext', ext)
// eslint-disable-next-line no-unused-vars
if (this.isLoading) {
this.$refs.toast.showToast("Loading... please wait", 4, false)
}
this.isLoading = true
console.log('extension', ext)
if (ext) {
if (ext.selected) {
this.$refs.toast.showToast("Extension already selected", 4, true)
this.isLoading = false
return
}
//this.settingsChanged = true
if (ext.isMounted && this.configFile.extensions.includes(ext.full_path)) {
const res = await this.select_extension(ext)
console.log('ext is mounted', res)
if (res && res.status && res.active_personality_id > -1) {
this.$refs.toast.showToast("Selected personality:\n" + ext.name, 4, true)
} else {
this.$refs.toast.showToast("Error on select personality:\n" + ext.name, 4, false)
}
this.isLoading = false
} else {
console.log('mounting ext')
this.mountPersonality(ext)
}
nextTick(() => {
feather.replace()
@ -3052,8 +3105,18 @@ export default {
this.persCatgArr = cats
this.personalitiesFiltered = this.personalities.filter((item) => item.category === this.personality_category)
this.personalitiesFiltered.sort()
})
this.api_get_req("list_extensions_categories").then((cats)=>{
console.log("cats",cats)
this.persCatgArr = cats
this.extensionsFiltererd = this.$store.state.extensionsZoo.filter((item) => item.category === this.extension_category)
this.extensionsFiltererd.sort()
})
});
@ -3477,10 +3540,6 @@ export default {
return
}
},
async unmount_personality(pers) {
if (!pers) { return { 'status': false, 'error': 'no personality - unmount_personality' } }
@ -3532,6 +3591,50 @@ export default {
return
}
},
async mount_extension(ext) {
if (!ext) { return { 'status': false, 'error': 'no extension - mount_extension' } }
try {
const obj = {
category: ext.category,
folder: ext.folder,
}
const res = await axios.post('/mount_extension', obj);
if (res) {
return res.data
}
} catch (error) {
console.log(error.message, 'mount_extension - settings')
return
}
},
async unmount_extension(ext) {
if (!ext) { return { 'status': false, 'error': 'no extension - unmount_extension' } }
const obj = {
language: ext.language,
category: ext.category,
folder: ext.folder
}
try {
const res = await axios.post('/unmount_extension', obj);
if (res) {
return res.data
}
} catch (error) {
console.log(error.message, 'unmount_extension - settings')
return
}
},
async mountPersonality(pers) {
this.isLoading = true
@ -3618,6 +3721,87 @@ export default {
await this.unmountPersonality(pers);
await this.mountPersonality(pers);
},
async mountExtension(ext) {
this.isLoading = true
console.log('mount ext', ext)
if (!ext) { return }
if (this.configFile.personalities.includes(ext.extension.full_path)) {
this.isLoading = false
this.$refs.toast.showToast("Extension already mounted", 4, false)
return
}
const res = await this.mount_extension(ext.extension)
console.log('mount_extension res', res)
if (res && res.status && res.extensions.includes(ext.extension.full_path)) {
this.configFile.extensions = res.extensions
this.$refs.toast.showToast("Extension mounted", 4, true)
ext.isMounted = true
const res2 = await this.select_extensions(ext.extensions)
if (res2.status) {
this.$refs.toast.showToast("Selected extension:\n" + ext.extension.name, 4, true)
}
this.$store.dispatch('refreshMountedExtensions');
} else {
ext.isMounted = false
this.$refs.toast.showToast("Could not mount extension\nError: " + res.error + "\nResponse:\n" + res, 4, false)
}
this.isLoading = false
},
async unmountExtension(ext) {
this.isLoading = true
if (!ext) { return }
const res = await this.unmount_personality(ext.personality || ext)
if (res.status) {
this.configFile.personalities = res.personalities
this.$refs.toast.showToast("Extension unmounted", 4, true)
const persId = this.personalities.findIndex(item => item.full_path == ext.full_path)
const persFilteredId = this.personalitiesFiltered.findIndex(item => item.full_path == ext.full_path)
const persIdZoo = this.$refs.personalitiesZoo.findIndex(item => item.full_path == ext.full_path)
console.log('ppp', this.personalities[persId])
this.personalities[persId].isMounted = false
if (persFilteredId > -1) {
this.personalitiesFiltered[persFilteredId].isMounted = false
}
if (persIdZoo > -1) {
this.$refs.personalitiesZoo[persIdZoo].isMounted = false
}
//ext.isMounted = false
this.$store.dispatch('refreshMountedPersonalities');
// Select some other personality
const lastPers = this.mountedPersArr[this.mountedPersArr.length - 1]
console.log(lastPers, this.mountedPersArr.length)
} else {
this.$refs.toast.showToast("Could not unmount extension\nError: " + res.error, 4, false)
}
this.isLoading = false
},
async remountExtension(ext){
await this.unmountExtension(ext);
await this.mountExtension(ext);
},
onPersonalityReinstall(persItem){
console.log('on reinstall ', persItem)
this.isLoading = true