uograded to new format

This commit is contained in:
Saifeddine ALOUI 2025-02-08 23:59:53 +01:00
parent e93c293d83
commit d7a5f8b8b9
13 changed files with 582 additions and 100 deletions

View File

@ -43,7 +43,7 @@ class APScript:
def exception(self, ex, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def warning(self, warning: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def json(self, title: str, json_infos: dict, callback: Callable[([str, int, dict, list], bool)] = None, indent = 4) -> Any
def ui(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_html(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def ui_in_iframe(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def code(self, code: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def add_chunk_to_message_content(self, full_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any

View File

@ -17,7 +17,7 @@ class AIPersonality:
def notify(self, content, notification_type: NotificationType = NotificationType.NOTIF_SUCCESS, duration: int = 4, client_id = None, display_type: NotificationDisplayType = NotificationDisplayType.TOAST, verbose = True) -> Any
def new_message(self, message_text: str, message_type: MSG_TYPE = MSG_OPERATION_TYPE.MSG_OPERATION_TYPE_SET_CONTENT, metadata = [], callback: Callable[([str, int, dict, list, Any], bool)] = None) -> Any
def set_message_content(self, full_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def ui(self, ui_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_html(self, ui_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_content_invisible_to_ai(self, full_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_content_invisible_to_user(self, full_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def build_prompt(self, prompt_parts: List[str], sacrifice_id: int = -1, context_size: int = None, minimum_spare_context_size: int = None) -> Any
@ -209,7 +209,7 @@ class APScript:
def exception(self, ex, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def warning(self, warning: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def json(self, title: str, json_infos: dict, callback: Callable[([str, int, dict, list], bool)] = None, indent = 4) -> Any
def ui(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_html(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def ui_in_iframe(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def code(self, code: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def add_chunk_to_message_content(self, full_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
@ -397,7 +397,7 @@ def set_message_content(self, full_text: str, callback: Callable[([str, MSG_TYPE
### ui
```python
def ui(self, ui_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_html(self, ui_text: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
```
### full_invisible_to_ai
@ -1369,7 +1369,7 @@ def json(self, title: str, json_infos: dict, callback: Callable[([str, int, dict
### ui
```python
def ui(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
def set_message_html(self, html_ui: str, callback: Callable[([str, MSG_TYPE, dict, list], bool)] = None) -> Any
```
### ui_in_iframe

245
docs/function_calls/doc.md Normal file
View File

@ -0,0 +1,245 @@
# LoLLMS Function Calls Documentation
## Overview
The LoLLMS Function Calls system allows Large Language Models (LLMs) to interact with external functions and tools. This system enables the AI to perform tasks beyond text generation, such as retrieving real-time information, performing calculations, or interacting with external APIs.
## Key Concepts
1. **Function Zoo**: A directory containing all available functions
2. **Mounted Functions**: Functions currently enabled for use by the LLM
3. **Function Call Format**: Special JSON format for invoking functions
4. **Processing Types**:
- Direct Execution (needs_processing=False)
- AI Interpretation (needs_processing=True)
## System Architecture
```mermaid
graph TD
A[User Prompt] --> B[Prompt Enhancement]
B --> C[LLM Processing]
C --> D{Function Call Detected?}
D -->|Yes| E[Function Execution]
D -->|No| F[Direct Response]
E --> G{needs_processing?}
G -->|Yes| H[AI Interpretation]
G -->|No| I[Direct Output]
H --> J[Final Response]
I --> J
F --> J
```
## Function Call Lifecycle
1. **Prompt Enhancement**: System adds function descriptions to user prompt
2. **LLM Processing**: AI generates response, potentially including function calls
3. **Function Detection**: System extracts and validates function calls
4. **Execution**: Selected functions are executed with provided parameters
5. **Result Processing**: Output is either shown directly or processed by AI
6. **Final Response**: Combined output is presented to user
## Creating New Functions
### Step 1: Create Function Directory
1. Navigate to functions zoo:
```bash
cd {lollms_functions_zoo_path}
```
2. Create new directory:
```bash
mkdir my_function
cd my_function
```
### Step 2: Create Configuration File (config.yaml)
```yaml
name: my_function
description: Brief description of what the function does
parameters:
param1:
type: string
description: Description of parameter
param2:
type: number
description: Another parameter
returns:
result1:
type: string
description: Description of return value
examples:
- "Example usage 1"
- "Example usage 2"
needs_processing: true/false
author: Your Name
version: 1.0.0
```
### Step 3: Implement Function Logic (function.py)
```python
class MyFunction:
def __init__(self, lollmsElfServer):
self.lollmsElfServer = lollmsElfServer
def run(self, **kwargs):
"""
Main function logic
Returns: Dictionary containing results
"""
# Access parameters
param1 = kwargs.get('param1')
param2 = kwargs.get('param2')
# Your logic here
result = perform_operation(param1, param2)
return {
"result1": result,
"status": "success"
}
```
### Step 4: Mount the Function
1. Add to mounted functions list in configuration
2. Or use API endpoint:
```bash
POST /mount_function_call
{
"client_id": "your_client_id",
"function_name": "my_function"
}
```
## Function Call Format
### AI-Generated Format
```xml
<lollms_function_call>
{
"function_name": "function_name",
"parameters": {
"param1": "value1",
"param2": "value2"
},
"needs_processing": true/false
}
</lollms_function_call>
```
### Response Format
```json
{
"function_name": "function_name",
"parameters": {
"param1": "value1",
"param2": "value2"
},
"results": {
"result1": "value1",
"result2": "value2"
},
"status": "success/error",
"message": "Additional information"
}
```
## API Endpoints
1. **List Available Functions**
```bash
GET /list_function_calls
```
2. **List Mounted Functions**
```bash
GET /list_mounted_function_calls
```
3. **Mount Function**
```bash
POST /mount_function_call
{
"client_id": "your_client_id",
"function_name": "function_name"
}
```
4. **Unmount Function**
```bash
POST /unmount_function_call
{
"client_id": "your_client_id",
"function_name": "function_name"
}
```
## Best Practices
1. **Error Handling**: Implement robust error handling in your function
2. **Parameter Validation**: Validate all inputs before processing
3. **Security**: Sanitize all inputs and outputs
4. **Documentation**: Provide clear examples and descriptions
5. **Versioning**: Maintain version numbers for compatibility
6. **Performance**: Optimize for quick execution
7. **Idempotency**: Make functions repeatable without side effects
## Example: Weather Function
### config.yaml
```yaml
name: get_weather
description: Get current weather information
parameters:
location:
type: string
description: City name or coordinates
unit:
type: string
enum: [celsius, fahrenheit]
default: celsius
returns:
temperature:
type: number
condition:
type: string
examples:
- "What's the weather in Paris?"
- "How's the weather today?"
needs_processing: true
author: Weather Inc.
version: 1.2.0
```
### function.py
```python
import requests
class WeatherFunction:
def __init__(self, lollmsElfServer):
self.lollmsElfServer = lollmsElfServer
self.api_key = "your_api_key"
def run(self, **kwargs):
location = kwargs.get('location', 'Paris')
unit = kwargs.get('unit', 'celsius')
try:
response = requests.get(
f"https://api.weatherapi.com/v1/current.json?key={self.api_key}&q={location}"
)
data = response.json()
return {
"temperature": data['current']['temp_c'] if unit == 'celsius' else data['current']['temp_f'],
"condition": data['current']['condition']['text'],
"location": location,
"unit": unit,
"status": "success"
}
except Exception as e:
return {
"status": "error",
"message": str(e)
}
```
This documentation provides a comprehensive guide to understanding, creating, and managing function calls in the LoLLMS system. Follow these guidelines to extend the capabilities of your LLM with custom functions.

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 2ee602ea842486d1621926b0438597a95bb1670e
Subproject commit 6e39499b5942cd39058515d363eace6316b6a178

View File

@ -1836,7 +1836,7 @@ Don't forget encapsulate the code inside a markdown code tag. This is mandatory.
</div>
"""
sources_text += "</div>"
self.personality.ui(sources_text)
self.personality.set_message_html(sources_text)
if len(context_details["skills"]) > 0:
sources_text += '<div class="text-gray-400 mr-10px flex items-center gap-2"><i class="fas fa-brain"></i>Memories:</div>'
@ -1857,7 +1857,7 @@ Don't forget encapsulate the code inside a markdown code tag. This is mandatory.
</div>
"""
sources_text += "</div>"
self.personality.ui(sources_text)
self.personality.set_message_html(sources_text)
# Send final message
if (
@ -1923,7 +1923,7 @@ Don't forget encapsulate the code inside a markdown code tag. This is mandatory.
}
</style>
"""
self.personality.ui(sources_text)
self.personality.set_message_html(sources_text)
except Exception as ex:
trace_exception(ex)

File diff suppressed because one or more lines are too long

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 @@
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LoLLMS WebUI</title>
<script type="module" crossorigin src="/assets/index-TS_PX_VH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-lNdQ3FaH.css">
<script type="module" crossorigin src="/assets/index-B60Hye2w.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C1n6u_eE.css">
</head>
<body>
<div id="app"></div>

View File

@ -4376,7 +4376,156 @@
</div>
</div>
<!-- FUNCTION CALLS ZOO -->
<div
class="flex flex-col mb-2 rounded-lg panels-color hover:bg-bg-light-tone-panel hover:dark:bg-bg-dark-tone-panel duration-150 shadow-lg">
<div class="flex flex-row p-3 items-center">
<button @click.stop="fzc_collapsed = !fzc_collapsed"
class="text-2xl hover:text-primary p-2 -m-2 text-left w-full flex items-center">
<div v-show="fzc_collapsed"><i data-feather='chevron-right'></i></div>
<div v-show="!fzc_collapsed"><i data-feather='chevron-down'></i></div>
<p class="text-lg font-semibold cursor-pointer select-none mr-2">
Function Calls Zoo</p>
<div v-if="configFile.mounted_functions" class="mr-2">|</div>
<!-- LIST OF MOUNTED FUNCTIONS -->
<div class="mr-2 font-bold font-large text-lg line-clamp-1">
{{ active_function }}
</div>
<div v-if="configFile.mounted_functions" class="mr-2">|</div>
<div v-if="configFile.mounted_functions"
class="text-base font-semibold cursor-pointer select-none items-center flex flex-row">
<!-- LIST -->
<div class="flex -space-x-4 items-center" v-if="mountedFuncArr.length > 0">
<!-- ITEM -->
<div class="relative hover:-translate-y-2 duration-300 hover:z-10 shrink-0"
v-for="(item, index) in mountedFuncArr" :key="index + '-' + item.name" ref="mountedFunctions">
<div class="group items-center flex flex-row">
<button @click.stop="onFunctionSelected(item)">
<img :src="bUrl + item.icon" @error="functionImgPlaceholder"
class="w-8 h-8 rounded-full object-fill text-red-700 border-2 active:scale-90 group-hover:border-secondary"
:class="configFile.active_function_id == configFile.mounted_functions.indexOf(item.full_path) ? 'border-secondary' : 'border-transparent z-0'"
:title="item.name">
</button>
<button @click.stop="unmountFunction(item)">
<span
class="hidden group-hover:block -top-2 -right-1 absolute active:scale-90 bg-bg-light dark:bg-bg-dark rounded-full border-2 border-transparent"
title="Unmount function">
<!-- UNMOUNT BUTTON -->
<svg aria-hidden="true" class="w-4 h-4 text-red-600 hover:text-red-500"
fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
<button @click.stop="unmountAllFunctions()"
class="bg-bg-light hover:border-green-200 ml-5 dark:bg-bg-dark rounded-full border-2 border-transparent"
title="Unmount All">
<!-- UNMOUNT BUTTON -->
<svg aria-hidden="true" class="w-4 h-4 text-red-600 hover:text-red-500" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</button>
</button>
</div>
<div :class="{ 'hidden': fzc_collapsed }" class="flex flex-col mb-2 px-3 pb-0">
<!-- SEARCH BAR -->
<div class="mx-2 mb-4">
<label for="function-search"
class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<div v-if="searchFunctionInProgress">
<!-- SPINNER -->
<div role="status">
<svg aria-hidden="true"
class="inline w-4 h-4 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor" />
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill" />
</svg>
<span class="sr-only">Loading...</span>
</div>
</div>
<div v-if="!searchFunctionInProgress">
<!-- SEARCH -->
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<input type="search" id="function-search"
class="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 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"
placeholder="Search function..." required v-model="searchFunction"
@keyup.stop="searchFunction_func">
<button v-if="searchFunction" @click.stop="searchFunction = ''" type="button"
class="text-white absolute right-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
Clear search</button>
</div>
</div>
<div class="mx-2 mb-4" v-if="!searchFunction">
<label for="funcCat" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Function Categories: ({{ funcCatgArr.length }})
</label>
<select id="funcCat" @change="update_function_category($event.target.value, refresh)"
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 funcCatgArr" :key="index"
:selected="item == this.configFile.function_category">{{ item }}</option>
</select>
</div>
<div>
<div v-if="functionsFiltered.length > 0" class="mb-2">
<label for="function" class="block ml-2 mb-2 text-sm font-medium text-gray-900 dark:text-white">
{{ searchFunction ? 'Search results' : 'Functions' }}: ({{
functionsFiltered.length
}})
</label>
<div class="overflow-y-auto no-scrollbar p-2 pb-0 grid lg:grid-cols-3 md:grid-cols-2 gap-4"
:class="fzl_collapsed ? '' : 'max-h-96'">
<TransitionGroup name="bounce">
<function-entry ref="functionsZoo" v-for="(func, index) in functionsFiltered"
:key="'index-' + index + '-' + func.name" :function="func"
:on-mount="mountFunction"
:on-unmount="unmountFunction"
:on-remount="remountFunction"
:on-edit="editFunction"
:on-copy-to-custom="copyToCustom"
:on-reinstall="onFunctionReinstall"
:on-settings="onSettingsFunction"
:on-toggle-favorite="toggleFavorite" />
</TransitionGroup>
</div>
</div>
</div>
<!-- EXPAND / COLLAPSE BUTTON -->
<button v-if="fzl_collapsed"
class="text-2xl hover:text-secondary duration-75 flex justify-center hover:bg-bg-light-tone hover:dark:bg-bg-dark-tone rounded-lg"
title="Collapse" type="button" @click="fzl_collapsed = !fzl_collapsed">
<i data-feather="chevron-up"></i>
</button>
<button v-else
class="text-2xl hover:text-secondary duration-75 flex justify-center hover:bg-bg-light-tone hover:dark:bg-bg-dark-tone rounded-lg"
title="Expand" type="button" @click="fzl_collapsed = !fzl_collapsed">
<i data-feather="chevron-down"></i>
</button>
</div>
</div>
<!-- MODEL CONFIGURATION -->
<div
class="flex flex-col mb-2 p-3 rounded-lg panels-color hover:bg-bg-light-tone-panel hover:dark:bg-bg-dark-tone-panel duration-150 shadow-lg">
@ -4750,6 +4899,14 @@ export default {
'accept': 'application/json',
'Content-Type': 'application/json'
},
fzc_collapsed: false, // Collapse state for the function calls zoo section
fzl_collapsed: false, // Collapse state for the function list
mountedFuncArr: [], // List of mounted functions
searchFunction: '', // Search query for functions
searchFunctionInProgress: false, // Loading state for search
funcCatgArr: [], // List of function categories
functionsFiltered: [], // Filtered list of functions
showThinkingPresets: false,
showAddThinkingPreset: false,
thinkingPresets: [],
@ -4890,6 +5047,86 @@ export default {
//await socket.on('install_progress', this.progressListener);
},
methods: {
// Toggle favorite function
toggleFavorite(funcUid) {
const index = this.favorites.indexOf(funcUid);
if (index === -1) {
this.favorites.push(funcUid);
} else {
this.favorites.splice(index, 1);
}
this.saveFavoritesToLocalStorage();
},
// Mount a function
async mountFunction(func) {
try {
const response = await axios.post('/mount_function', {
client_id: this.$store.state.client_id,
function_name: func.name,
});
if (response.data.status) {
this.showMessage('Function mounted successfully', true);
this.$store.dispatch('refreshMountedFunctions');
} else {
this.showMessage('Failed to mount function', false);
}
} catch (error) {
this.showMessage('Error mounting function', false);
console.error(error);
}
},
// Unmount a function
async unmountFunction(func) {
try {
const response = await axios.post('/unmount_function', {
client_id: this.$store.state.client_id,
function_name: func.name,
});
if (response.data.status) {
this.showMessage('Function unmounted successfully', true);
this.$store.dispatch('refreshMountedFunctions');
} else {
this.showMessage('Failed to unmount function', false);
}
} catch (error) {
this.showMessage('Error unmounting function', false);
console.error(error);
}
},
// Unmount all functions
async unmountAllFunctions() {
try {
const response = await axios.post('/unmount_all_functions', {
client_id: this.$store.state.client_id,
});
if (response.data.status) {
this.showMessage('All functions unmounted successfully', true);
this.$store.dispatch('refreshMountedFunctions');
} else {
this.showMessage('Failed to unmount all functions', false);
}
} catch (error) {
this.showMessage('Error unmounting all functions', false);
console.error(error);
}
},
// Update function category
update_function_category(category, refresh) {
this.configFile.function_category = category;
if (refresh) {
this.refreshFunctionsZoo();
}
},
// Refresh functions zoo
refreshFunctionsZoo() {
this.$store.dispatch('refreshFunctionsZoo');
},
toggleLightragServerStatus(index) {
if (this.expandedStatusIndex === index) {
this.expandedStatusIndex = null

@ -1 +1 @@
Subproject commit 47c89e8cedeef1ec7758f3fe7516f05277b314a2
Subproject commit 6b4927071772973c7a4edd293e753baeea2895d8

@ -1 +1 @@
Subproject commit 7e690e4437fe0df628fb8565fe19a177ea59c3da
Subproject commit 940c926803c444a1b609d7242c90447cd87a3dad