- Complete MODELS.md with detailed model descriptions - Interactive download script for 5+ specialized SDXL models - Updated README with model selection guide - Fix docker-compose volumes path (/root/ComfyUI) - API workflows for OpenWebUI integration Models documented: - RealVisXL (portraits) - Juggernaut XL (versatile) - DreamShaper (art/fantasy) - SDXL Lightning (fast) - Proteus (realism)
568 lines
19 KiB
Markdown
568 lines
19 KiB
Markdown
<img src="https://r2cdn.perplexity.ai/pplx-full-logo-primary-dark%402x.png" style="height:64px;margin-right:32px"/>
|
||
|
||
# CONTEXTE TECHNIQUE
|
||
|
||
Je cherche à créer un assistant IA personnel qui utilise Open WebUI comme interface, Ollama pour l'inférence LLM, et AFFiNE comme base de connaissance/mémoire.
|
||
Infrastructure actuelle :
|
||
|
||
1. NAS Synology (192.168.1.25)
|
||
Open WebUI (container Docker, port 3000)
|
||
AFFiNE (container Docker, port 3010) avec PostgreSQL + Redis
|
||
Compose AFFiNE complet fonctionnel (affine, migration, postgres, redis)
|
||
2. PC Gamer (192.168.1.11)
|
||
Ollama (port 11434) avec modèles 14B paramètres (Qwen3-VL, Ministral, etc.)
|
||
Accessible via un WOL proxy (container sur NAS, port 3011) qui réveille le PC à la demande
|
||
3. WOL Proxy fonctionnel
|
||
Python/Flask sur NAS (192.168.1.25:3011)
|
||
Réveille le PC gamer automatiquement via Wake-on-LAN
|
||
Proxifie ensuite toutes les requêtes vers Ollama de manière transparente
|
||
Open WebUI configuré avec OLLAMA_BASE_URL=http://wol-proxy:3010
|
||
CONFIGURATION AFFINE ACTUELLE
|
||
text
|
||
services:
|
||
affine:
|
||
image: ghcr.io/toeverything/affine:stable
|
||
ports: ['3010:3010']
|
||
environment:
|
||
- AFFINE_COPILOT_ENABLED=true
|
||
- AFFINE_AI_PROVIDER=openai
|
||
- AFFINE_OPENAI_BASE_URL=http://192.168.1.10:11434 \# À modifier ?
|
||
- AFFINE_OPENAI_API_KEY=ollama
|
||
depends_on: [redis, postgres, affine_migration]
|
||
|
||
# + services redis, postgres, migration (fonctionnels)
|
||
|
||
OBJECTIF
|
||
Je veux que :
|
||
Open WebUI soit l'orchestrateur central (UI unique, gestion conversation, config assistant)
|
||
Ollama sur PC gamer fasse l'inférence (via WOL proxy pour économie énergie)
|
||
AFFiNE serve de base de connaissance persistante où l'assistant peut :
|
||
Rechercher dans mes documents/notes
|
||
Créer/modifier des notes automatiquement
|
||
Récupérer du contexte pour les conversations
|
||
Architecture souhaitée :
|
||
text
|
||
Utilisateur → Open WebUI (NAS) → WOL Proxy → Ollama (PC)
|
||
↓
|
||
[Tools]
|
||
↓
|
||
AFFiNE (NAS) ← GraphQL API
|
||
|
||
CONTRAINTES \& PRÉFÉRENCES
|
||
✅ Tout en Docker/Docker Compose sur le NAS (sauf Ollama sur PC)
|
||
✅ Solution simple et maintenable (pas de dépendances npm complexes si possible)
|
||
✅ Pas de duplication d'historique (Open WebUI = source de vérité)
|
||
✅ AFFiNE doit rester utilisable indépendamment (pas couplé à l'IA)
|
||
⚠️ J'ai essayé d'installer le package npm @dawncr0w/affine-mcp-server mais il n'existe pas dans le registre npm
|
||
QUESTIONS CONCRÈTES
|
||
Quelle est l'approche la plus robuste : MCP server + proxy HTTP, ou outil Python custom dans Open WebUI qui appelle directement l'API GraphQL d'AFFiNE ?
|
||
Comment configurer proprement l'intégration pour que le modèle LLM puisse faire du function calling vers AFFiNE (recherche, création docs, etc.) ?
|
||
Dois-je modifier l'URL Ollama dans AFFiNE (AFFINE_OPENAI_BASE_URL) pour qu'elle passe aussi par le WOL proxy ? Actuellement elle pointe directement sur le PC.
|
||
Peux-tu me fournir :
|
||
Le docker-compose.yml complet modifié (si ajout de services)
|
||
Le code de l'outil/intégration (Python, MCP config, etc.)
|
||
Les étapes de configuration dans Open WebUI
|
||
VERSION LOGICIELS
|
||
Open WebUI : 0.8.2
|
||
AFFiNE : stable (ghcr.io/toeverything/affine:stable)
|
||
Ollama : 0.16.1
|
||
IMPORTANT : Je préfère une solution qui minimise le nombre de conteneurs supplémentaires et qui soit facile à débugger. Si MCP est trop complexe, une intégration directe via GraphQL + Python dans Open WebUI me convient aussi.
|
||
|
||
Basé sur votre infrastructure et vos contraintes, je recommande **une intégration Python directe via GraphQL** plutôt que MCP, car c'est plus simple, maintenable et parfaitement adapté à votre cas.[^1_1][^1_2][^1_3]
|
||
|
||
## Approche recommandée : Outil Python Custom dans Open WebUI
|
||
|
||
Open WebUI supporte nativement le function calling via des **Tools** Python qui permettent au LLM d'appeler des fonctions externes. Cette approche évite d'ajouter des conteneurs supplémentaires et reste facilement débuggable.[^1_2][^1_3][^1_1]
|
||
|
||
### Architecture finale
|
||
|
||
```
|
||
Utilisateur → Open WebUI (NAS:3000) → Ollama (via WOL Proxy)
|
||
↓
|
||
[AFFiNE Tool]
|
||
↓
|
||
AFFiNE GraphQL API (NAS:3010)
|
||
```
|
||
|
||
|
||
## Solution complète
|
||
|
||
### 1. Docker Compose modifié
|
||
|
||
```yaml
|
||
services:
|
||
# Configuration AFFiNE existante
|
||
affine:
|
||
image: ghcr.io/toeverything/affine:stable
|
||
ports: ['3010:3010']
|
||
environment:
|
||
- AFFINE_COPILOT_ENABLED=true
|
||
- AFFINE_AI_PROVIDER=openai
|
||
# Modifier pour passer par le WOL proxy
|
||
- AFFINE_OPENAI_BASE_URL=http://wol-proxy:3011/v1
|
||
- AFFINE_OPENAI_API_KEY=ollama
|
||
depends_on: [redis, postgres, affine_migration]
|
||
networks:
|
||
- affine_network
|
||
|
||
postgres:
|
||
# Votre config existante
|
||
networks:
|
||
- affine_network
|
||
|
||
redis:
|
||
# Votre config existante
|
||
networks:
|
||
- affine_network
|
||
|
||
affine_migration:
|
||
# Votre config existante
|
||
networks:
|
||
- affine_network
|
||
|
||
# Configuration Open WebUI
|
||
open-webui:
|
||
image: ghcr.io/open-webui/open-webui:latest
|
||
ports: ['3000:8080']
|
||
environment:
|
||
- OLLAMA_BASE_URL=http://wol-proxy:3011
|
||
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
|
||
volumes:
|
||
- open-webui-data:/app/backend/data
|
||
networks:
|
||
- affine_network
|
||
|
||
# WOL Proxy existant
|
||
wol-proxy:
|
||
# Votre config existante
|
||
ports: ['3011:3011']
|
||
networks:
|
||
- affine_network
|
||
|
||
networks:
|
||
affine_network:
|
||
driver: bridge
|
||
|
||
volumes:
|
||
open-webui-data:
|
||
```
|
||
|
||
|
||
### 2. Tool Python pour Open WebUI
|
||
|
||
Créez un fichier `affine_tool.py` à importer dans Open WebUI:[^1_3]
|
||
|
||
```python
|
||
"""
|
||
title: AFFiNE Knowledge Base Tool
|
||
author: Your Name
|
||
version: 1.0.0
|
||
description: Interact with AFFiNE workspace for document search, creation, and retrieval
|
||
required_open_webui_version: 0.8.0
|
||
"""
|
||
|
||
import os
|
||
import requests
|
||
from typing import Optional, Callable, Any
|
||
from pydantic import BaseModel, Field
|
||
|
||
|
||
class Tools:
|
||
class Valves(BaseModel):
|
||
AFFINE_BASE_URL: str = Field(
|
||
default="http://affine:3010",
|
||
description="AFFiNE server base URL"
|
||
)
|
||
AFFINE_WORKSPACE_ID: str = Field(
|
||
default="",
|
||
description="Default workspace ID"
|
||
)
|
||
AFFINE_API_TOKEN: str = Field(
|
||
default="",
|
||
description="AFFiNE personal access token (optional)"
|
||
)
|
||
|
||
def __init__(self):
|
||
self.valves = self.Valves()
|
||
self.graphql_endpoint = None
|
||
|
||
def _get_endpoint(self):
|
||
"""Construire l'URL GraphQL"""
|
||
if not self.graphql_endpoint:
|
||
base = self.valves.AFFINE_BASE_URL.rstrip('/')
|
||
self.graphql_endpoint = f"{base}/graphql"
|
||
return self.graphql_endpoint
|
||
|
||
def _execute_graphql(self, query: str, variables: dict = None) -> dict:
|
||
"""Exécuter une requête GraphQL"""
|
||
headers = {"Content-Type": "application/json"}
|
||
|
||
# Ajouter le token si disponible
|
||
if self.valves.AFFINE_API_TOKEN:
|
||
headers["Authorization"] = f"Bearer {self.valves.AFFINE_API_TOKEN}"
|
||
|
||
payload = {"query": query}
|
||
if variables:
|
||
payload["variables"] = variables
|
||
|
||
try:
|
||
response = requests.post(
|
||
self._get_endpoint(),
|
||
json=payload,
|
||
headers=headers,
|
||
timeout=30
|
||
)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
except Exception as e:
|
||
return {"error": str(e)}
|
||
|
||
def search_documents(
|
||
self,
|
||
search_query: str,
|
||
workspace_id: Optional[str] = None,
|
||
__event_emitter__: Callable[[dict], Any] = None
|
||
) -> str:
|
||
"""
|
||
Search for documents in AFFiNE workspace.
|
||
|
||
:param search_query: Text to search for in documents
|
||
:param workspace_id: Workspace ID (uses default if not provided)
|
||
:return: Search results with document titles and IDs
|
||
"""
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": f"Searching AFFiNE for: {search_query}", "done": False}
|
||
})
|
||
|
||
ws_id = workspace_id or self.valves.AFFINE_WORKSPACE_ID
|
||
|
||
query = """
|
||
query SearchDocs($workspaceId: String!, $query: String!) {
|
||
workspace(id: $workspaceId) {
|
||
docs {
|
||
id
|
||
title
|
||
createdAt
|
||
}
|
||
}
|
||
}
|
||
"""
|
||
|
||
result = self._execute_graphql(query, {
|
||
"workspaceId": ws_id,
|
||
"query": search_query
|
||
})
|
||
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": "Search completed", "done": True}
|
||
})
|
||
|
||
if "error" in result:
|
||
return f"Error searching documents: {result['error']}"
|
||
|
||
if "data" in result and result["data"].get("workspace"):
|
||
docs = result["data"]["workspace"]["docs"]
|
||
if docs:
|
||
response = f"Found {len(docs)} documents:\n"
|
||
for doc in docs[:10]: # Limiter à 10 résultats
|
||
response += f"- {doc['title']} (ID: {doc['id']})\n"
|
||
return response
|
||
return "No documents found matching your query."
|
||
|
||
return "Error retrieving search results."
|
||
|
||
def get_document_content(
|
||
self,
|
||
doc_id: str,
|
||
workspace_id: Optional[str] = None,
|
||
__event_emitter__: Callable[[dict], Any] = None
|
||
) -> str:
|
||
"""
|
||
Retrieve the content of a specific document.
|
||
|
||
:param doc_id: Document ID to retrieve
|
||
:param workspace_id: Workspace ID (uses default if not provided)
|
||
:return: Document content
|
||
"""
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": f"Retrieving document {doc_id}", "done": False}
|
||
})
|
||
|
||
ws_id = workspace_id or self.valves.AFFINE_WORKSPACE_ID
|
||
|
||
query = """
|
||
query GetDoc($workspaceId: String!, $docId: String!) {
|
||
workspace(id: $workspaceId) {
|
||
doc(id: $docId) {
|
||
id
|
||
title
|
||
content
|
||
updatedAt
|
||
}
|
||
}
|
||
}
|
||
"""
|
||
|
||
result = self._execute_graphql(query, {
|
||
"workspaceId": ws_id,
|
||
"docId": doc_id
|
||
})
|
||
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": "Document retrieved", "done": True}
|
||
})
|
||
|
||
if "error" in result:
|
||
return f"Error retrieving document: {result['error']}"
|
||
|
||
if "data" in result and result["data"].get("workspace", {}).get("doc"):
|
||
doc = result["data"]["workspace"]["doc"]
|
||
return f"# {doc['title']}\n\nLast updated: {doc['updatedAt']}\n\n{doc.get('content', 'No content available')}"
|
||
|
||
return "Document not found or error retrieving content."
|
||
|
||
def create_document(
|
||
self,
|
||
title: str,
|
||
content: str,
|
||
workspace_id: Optional[str] = None,
|
||
__event_emitter__: Callable[[dict], Any] = None
|
||
) -> str:
|
||
"""
|
||
Create a new document in AFFiNE workspace.
|
||
|
||
:param title: Title of the new document
|
||
:param content: Content of the document
|
||
:param workspace_id: Workspace ID (uses default if not provided)
|
||
:return: Created document ID and confirmation
|
||
"""
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": f"Creating document: {title}", "done": False}
|
||
})
|
||
|
||
ws_id = workspace_id or self.valves.AFFINE_WORKSPACE_ID
|
||
|
||
mutation = """
|
||
mutation CreateDoc($workspaceId: String!, $title: String!, $content: String!) {
|
||
createDoc(workspaceId: $workspaceId, title: $title, content: $content) {
|
||
id
|
||
title
|
||
}
|
||
}
|
||
"""
|
||
|
||
result = self._execute_graphql(mutation, {
|
||
"workspaceId": ws_id,
|
||
"title": title,
|
||
"content": content
|
||
})
|
||
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": "Document created successfully", "done": True}
|
||
})
|
||
|
||
if "error" in result:
|
||
return f"Error creating document: {result['error']}"
|
||
|
||
if "data" in result and result["data"].get("createDoc"):
|
||
doc = result["data"]["createDoc"]
|
||
return f"Successfully created document '{doc['title']}' with ID: {doc['id']}"
|
||
|
||
return "Error creating document."
|
||
|
||
def list_workspaces(
|
||
self,
|
||
__event_emitter__: Callable[[dict], Any] = None
|
||
) -> str:
|
||
"""
|
||
List all available workspaces.
|
||
|
||
:return: List of workspace names and IDs
|
||
"""
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": "Fetching workspaces", "done": False}
|
||
})
|
||
|
||
query = """
|
||
query {
|
||
workspaces {
|
||
id
|
||
name
|
||
createdAt
|
||
}
|
||
}
|
||
"""
|
||
|
||
result = self._execute_graphql(query)
|
||
|
||
if __event_emitter__:
|
||
__event_emitter__({
|
||
"type": "status",
|
||
"data": {"description": "Workspaces retrieved", "done": True}
|
||
})
|
||
|
||
if "error" in result:
|
||
return f"Error listing workspaces: {result['error']}"
|
||
|
||
if "data" in result and result["data"].get("workspaces"):
|
||
workspaces = result["data"]["workspaces"]
|
||
if workspaces:
|
||
response = f"Available workspaces ({len(workspaces)}):\n"
|
||
for ws in workspaces:
|
||
response += f"- {ws['name']} (ID: {ws['id']})\n"
|
||
return response
|
||
return "No workspaces found."
|
||
|
||
return "Error retrieving workspaces."
|
||
```
|
||
|
||
|
||
### 3. Configuration dans Open WebUI
|
||
|
||
#### Étape A : Importer le Tool
|
||
|
||
1. Connectez-vous à Open WebUI (http://192.168.1.25:3000)
|
||
2. Allez dans **Workspace → Tools**
|
||
3. Cliquez sur **+ Create Tool**
|
||
4. Collez le code Python ci-dessus
|
||
5. Cliquez sur **Save**
|
||
|
||
#### Étape B : Configurer les Valves[^1_3]
|
||
|
||
1. Cliquez sur l'icône d'engrenage du tool **AFFiNE Knowledge Base**
|
||
2. Configurez les valeurs :
|
||
- `AFFINE_BASE_URL`: `http://affine:3010` (communication inter-conteneur)
|
||
- `AFFINE_WORKSPACE_ID`: Votre ID de workspace (à récupérer)
|
||
- `AFFINE_API_TOKEN`: Laissez vide pour l'instant
|
||
|
||
#### Étape C : Récupérer le Workspace ID[^1_4][^1_5]
|
||
|
||
Depuis le NAS, exécutez :
|
||
|
||
```bash
|
||
curl -X POST http://192.168.1.25:3010/graphql \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"query": "{ workspaces { id name } }"}'
|
||
```
|
||
|
||
Copiez l'ID du workspace et mettez-le dans les Valves.
|
||
|
||
#### Étape D : Activer le Tool
|
||
|
||
1. Dans **Workspace → Models**, sélectionnez votre modèle Ollama
|
||
2. Cliquez sur l'icône crayon pour éditer
|
||
3. Scrollez jusqu'à **Functions**
|
||
4. Cochez **AFFiNE Knowledge Base Tool**
|
||
5. Cliquez sur **Save**
|
||
|
||
## Réponses aux questions
|
||
|
||
### 1. Approche la plus robuste
|
||
|
||
**Python Tool directement dans Open WebUI** est supérieure à MCP dans votre cas car:[^1_6][^1_3]
|
||
|
||
- Pas de conteneur supplémentaire
|
||
- Débugage simple (logs Python natifs)
|
||
- Configuration unifiée dans Open WebUI
|
||
- Le package npm `@dawncr0w/affine-mcp-server` existe mais nécessite Node.js + conteneur dédié[^1_7][^1_5]
|
||
|
||
|
||
### 2. Configuration function calling
|
||
|
||
Le code fourni utilise les **Sphinx-style docstrings** qu'Open WebUI convertit automatiquement en spécifications de tools pour le LLM. Les paramètres typés permettent au modèle de comprendre comment appeler les fonctions.[^1_2][^1_3]
|
||
|
||
### 3. URL Ollama dans AFFiNE
|
||
|
||
**Oui, modifiez-la** pour utiliser le WOL proxy. Changez :[^1_1]
|
||
|
||
```yaml
|
||
- AFFINE_OPENAI_BASE_URL=http://wol-proxy:3011/v1
|
||
```
|
||
|
||
**Important** : Ajoutez `/v1` car AFFiNE attend une API compatible OpenAI. Votre WOL proxy doit donc exposer `/v1/chat/completions` (pattern standard Ollama).[^1_4]
|
||
|
||
## Avantages de cette solution
|
||
|
||
✅ **Un seul conteneur supplémentaire** : Open WebUI gère tout
|
||
✅ **Debugging facile** : Logs Python visibles dans Open WebUI
|
||
✅ **Pas de duplication** : Open WebUI reste la source de vérité
|
||
✅ **AFFiNE indépendant** : Fonctionne normalement sans l'IA
|
||
✅ **Économie d'énergie** : Le WOL proxy réveille le PC uniquement quand nécessaire
|
||
|
||
## Limitations et évolutions
|
||
|
||
- Les requêtes GraphQL ci-dessus sont des **exemples** : l'API réelle d'AFFiNE peut différer[^1_8][^1_9]
|
||
- Testez les requêtes manuellement avec `curl` avant de les intégrer
|
||
- Pour l'authentification avancée, générez un Personal Access Token via l'API GraphQL d'AFFiNE[^1_10][^1_11]
|
||
- Si vous avez besoin de capacités MCP plus tard, le projet `ivanuser/open-webui-mcp-connector` existe[^1_6]
|
||
|
||
Cette architecture vous permet de commencer simplement et d'étendre progressivement les fonctionnalités selon vos besoins.[^1_1][^1_3]
|
||
<span style="display:none">[^1_12][^1_13][^1_14][^1_15][^1_16][^1_17][^1_18][^1_19][^1_20][^1_21][^1_22][^1_23][^1_24][^1_25]</span>
|
||
|
||
<div align="center">⁂</div>
|
||
|
||
[^1_1]: https://github.com/open-webui/open-webui/discussions/11820
|
||
|
||
[^1_2]: https://github.com/open-webui/open-webui/discussions/3134
|
||
|
||
[^1_3]: https://docs.openwebui.com/tutorials/plugin/functions/
|
||
|
||
[^1_4]: https://lobehub.com/mcp/dawncr0w-affine-mcp-server
|
||
|
||
[^1_5]: https://github.com/dawncr0w/affine-mcp-server
|
||
|
||
[^1_6]: https://github.com/ivanuser/open-webui-mcp-connector
|
||
|
||
[^1_7]: https://playbooks.com/mcp/dawncr0w/affine-mcp-server
|
||
|
||
[^1_8]: https://deepwiki.com/toeverything/AFFiNE/3.1-server-and-api
|
||
|
||
[^1_9]: https://opendeep.wiki/toeverything/AFFiNE/integrating-external-app
|
||
|
||
[^1_10]: https://github.com/toeverything/AFFiNE/issues/13262
|
||
|
||
[^1_11]: https://glama.ai/mcp/servers/@DAWNCR0W/affine-mcp-server/tools/affine_generate_access_token
|
||
|
||
[^1_12]: https://www.reddit.com/r/OpenWebUI/comments/1jeqj9u/is_function_calling_possible_in_open_webui/
|
||
|
||
[^1_13]: https://www.youtube.com/watch?v=04h9XlOkhCE
|
||
|
||
[^1_14]: https://www.reddit.com/r/mcp/comments/1oqj7hp/affine_mcp_server_enables_interaction_with_affine/
|
||
|
||
[^1_15]: https://github.com/open-webui/open-webui/issues/2825
|
||
|
||
[^1_16]: https://dahvc-docs.colab.duke.edu/mcp/open-web-ui-and-mcpo.html
|
||
|
||
[^1_17]: https://github.com/open-webui/open-webui/discussions/1629
|
||
|
||
[^1_18]: https://mcpmarket.com/server/affine
|
||
|
||
[^1_19]: https://rainbowbreeze.it/openwebui-mcp-http/
|
||
|
||
[^1_20]: https://www.reddit.com/r/OpenWebUI/comments/1oqh3au/open_webui_now_supports_native_sequential_tool/
|
||
|
||
[^1_21]: https://docs.affine.pro/self-host-affine/install/upgrade
|
||
|
||
[^1_22]: https://developers.cloudflare.com/analytics/graphql-api/getting-started/authentication/api-token-auth/
|
||
|
||
[^1_23]: https://developers.openai.com/api/docs/guides/function-calling/
|
||
|
||
[^1_24]: https://community.retool.com/t/graphql-authorization-with-url-token/4638
|
||
|
||
[^1_25]: https://shyft.ai/mcp/dawncr0w-affine-mcp-server-github
|
||
|