feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
This commit is contained in:
486
ROUTER-MULTIMODAL-SUPPORT.md
Normal file
486
ROUTER-MULTIMODAL-SUPPORT.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# 🔧 Router - Підтримка мультимодальних запитів
|
||||
|
||||
**Дата:** 2025-11-23
|
||||
**Мета:** Додати підтримку images/files у Router payload
|
||||
**Статус:** 📝 Інструкції для бекенду
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Що потрібно реалізувати
|
||||
|
||||
### Поточна структура запиту:
|
||||
|
||||
```json
|
||||
POST http://144.76.224.179:9102/route
|
||||
|
||||
{
|
||||
"agent": "daarwizz",
|
||||
"message": "текст повідомлення",
|
||||
"mode": "chat",
|
||||
"payload": {
|
||||
"context": {
|
||||
"system_prompt": "..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Нова структура з мультимодальністю:
|
||||
|
||||
```json
|
||||
POST http://144.76.224.179:9102/route
|
||||
|
||||
{
|
||||
"agent": "daarwizz",
|
||||
"message": "Проаналізуй це зображення",
|
||||
"mode": "chat",
|
||||
"payload": {
|
||||
"context": {
|
||||
"system_prompt": "...",
|
||||
"images": [
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANS..."
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"name": "document.pdf",
|
||||
"type": "application/pdf",
|
||||
"data": "data:application/pdf;base64,JVBERi0xLj..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Зміни в Router (NODE1)
|
||||
|
||||
### 1. Оновити `router-config-final.yml`
|
||||
|
||||
**Файл:** `/opt/microdao-daarion/router/router-config-final.yml`
|
||||
|
||||
**Додати підтримку мультимодальних моделей:**
|
||||
|
||||
```yaml
|
||||
llm_profiles:
|
||||
# Існуючі профілі...
|
||||
|
||||
# Мультимодальні профілі
|
||||
- name: sofia_vision
|
||||
provider: xai
|
||||
model: grok-4.1
|
||||
settings:
|
||||
temperature: 0.7
|
||||
max_tokens: 4096
|
||||
supports_vision: true # Нове поле
|
||||
description: "Sofia - Vision + Code"
|
||||
|
||||
- name: spectra_multimodal
|
||||
provider: ollama
|
||||
model: qwen3-vl:latest
|
||||
settings:
|
||||
temperature: 0.7
|
||||
max_tokens: 2048
|
||||
supports_vision: true # Нове поле
|
||||
description: "Spectra - Vision + Language"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Оновити Python Router код
|
||||
|
||||
**Файл:** `/opt/microdao-daarion/router/main.py` (або аналогічний)
|
||||
|
||||
**Додати обробку images/files:**
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
import base64
|
||||
import io
|
||||
from PIL import Image
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class ContextPayload(BaseModel):
|
||||
system_prompt: Optional[str] = None
|
||||
images: Optional[List[str]] = None # base64 encoded images
|
||||
files: Optional[List[Dict[str, str]]] = None # file metadata + base64 data
|
||||
|
||||
class RouteRequest(BaseModel):
|
||||
agent: str
|
||||
message: str
|
||||
mode: str = "chat"
|
||||
payload: Optional[Dict[str, Any]] = None
|
||||
|
||||
def process_images(images: List[str]) -> List[Image.Image]:
|
||||
"""Конвертує base64 зображення в PIL Image об'єкти"""
|
||||
processed = []
|
||||
for img_data in images:
|
||||
# Видалити data:image/...;base64, префікс
|
||||
if ',' in img_data:
|
||||
img_data = img_data.split(',')[1]
|
||||
|
||||
# Декодувати base64
|
||||
img_bytes = base64.b64decode(img_data)
|
||||
img = Image.open(io.BytesIO(img_bytes))
|
||||
processed.append(img)
|
||||
|
||||
return processed
|
||||
|
||||
def process_files(files: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
||||
"""Обробляє файли (PDF, TXT, тощо)"""
|
||||
processed = []
|
||||
for file_data in files:
|
||||
name = file_data.get('name')
|
||||
file_type = file_data.get('type')
|
||||
data = file_data.get('data')
|
||||
|
||||
# Видалити data:...;base64, префікс
|
||||
if ',' in data:
|
||||
data = data.split(',')[1]
|
||||
|
||||
# Декодувати base64
|
||||
file_bytes = base64.b64decode(data)
|
||||
|
||||
processed.append({
|
||||
'name': name,
|
||||
'type': file_type,
|
||||
'content': file_bytes,
|
||||
'size': len(file_bytes)
|
||||
})
|
||||
|
||||
return processed
|
||||
|
||||
@app.post("/route")
|
||||
async def route_message(request: RouteRequest):
|
||||
try:
|
||||
# Отримати payload
|
||||
payload = request.payload or {}
|
||||
context = payload.get('context', {})
|
||||
|
||||
# Обробити зображення (якщо є)
|
||||
images = None
|
||||
if context.get('images'):
|
||||
images = process_images(context['images'])
|
||||
print(f"✅ Processed {len(images)} images")
|
||||
|
||||
# Обробити файли (якщо є)
|
||||
files = None
|
||||
if context.get('files'):
|
||||
files = process_files(context['files'])
|
||||
print(f"✅ Processed {len(files)} files")
|
||||
|
||||
# Визначити агента
|
||||
agent_id = request.agent
|
||||
|
||||
# Перевірити чи агент підтримує vision
|
||||
agent_supports_vision = agent_id in ['sofia', 'spectra', 'daarwizz']
|
||||
|
||||
if images and not agent_supports_vision:
|
||||
return {
|
||||
"error": f"Агент {agent_id} не підтримує обробку зображень",
|
||||
"suggestion": "Спробуйте sofia або spectra для vision tasks"
|
||||
}
|
||||
|
||||
# Підготувати запит до LLM
|
||||
llm_request = {
|
||||
"model": get_model_for_agent(agent_id),
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": context.get('system_prompt', get_default_prompt(agent_id))
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": request.message
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Додати зображення до запиту (для vision моделей)
|
||||
if images and agent_supports_vision:
|
||||
# Для Ollama qwen3-vl або grok-4.1
|
||||
llm_request['images'] = [img_to_base64(img) for img in images]
|
||||
|
||||
# Додати файли як контекст
|
||||
if files:
|
||||
files_context = "\n\n".join([
|
||||
f"Файл: {f['name']} ({f['size']} bytes)"
|
||||
for f in files
|
||||
])
|
||||
llm_request['messages'][-1]['content'] += f"\n\nПрикріплені файли:\n{files_context}"
|
||||
|
||||
# Викликати LLM
|
||||
response = await call_llm(llm_request)
|
||||
|
||||
return {
|
||||
"data": {
|
||||
"text": response['text'],
|
||||
"model": response['model'],
|
||||
"tokens": response.get('tokens', 0)
|
||||
},
|
||||
"metadata": {
|
||||
"agent": agent_id,
|
||||
"has_images": bool(images),
|
||||
"has_files": bool(files)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
def img_to_base64(img: Image.Image) -> str:
|
||||
"""Конвертує PIL Image в base64"""
|
||||
buffered = io.BytesIO()
|
||||
img.save(buffered, format="PNG")
|
||||
return base64.b64encode(buffered.getvalue()).decode()
|
||||
|
||||
def get_model_for_agent(agent_id: str) -> str:
|
||||
"""Мапінг агентів до моделей"""
|
||||
models = {
|
||||
'sofia': 'grok-4.1',
|
||||
'spectra': 'qwen3-vl:latest',
|
||||
'solarius': 'deepseek-r1:70b',
|
||||
'daarwizz': 'qwen3-8b',
|
||||
}
|
||||
return models.get(agent_id, 'qwen3-8b')
|
||||
|
||||
def get_default_prompt(agent_id: str) -> str:
|
||||
"""System prompts для агентів"""
|
||||
# ... (існуючі промпти)
|
||||
pass
|
||||
|
||||
async def call_llm(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Викликає LLM (Ollama або xAI)"""
|
||||
# ... (існуюча логіка)
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестування
|
||||
|
||||
### 1. Текстовий запит (існуючий)
|
||||
|
||||
```bash
|
||||
curl -X POST http://144.76.224.179:9102/route \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"agent": "daarwizz",
|
||||
"message": "Привіт!",
|
||||
"mode": "chat"
|
||||
}'
|
||||
```
|
||||
|
||||
**Очікується:** `{ "data": { "text": "..." } }`
|
||||
|
||||
---
|
||||
|
||||
### 2. Запит з зображенням
|
||||
|
||||
```bash
|
||||
# Конвертувати зображення в base64
|
||||
BASE64_IMAGE=$(base64 -w 0 image.png)
|
||||
|
||||
curl -X POST http://144.76.224.179:9102/route \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"agent\": \"sofia\",
|
||||
\"message\": \"Що на цьому зображенні?\",
|
||||
\"mode\": \"chat\",
|
||||
\"payload\": {
|
||||
\"context\": {
|
||||
\"images\": [\"data:image/png;base64,$BASE64_IMAGE\"]
|
||||
}
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
**Очікується:** `{ "data": { "text": "На зображенні..." }, "metadata": { "has_images": true } }`
|
||||
|
||||
---
|
||||
|
||||
### 3. Запит з файлом
|
||||
|
||||
```bash
|
||||
# Конвертувати PDF в base64
|
||||
BASE64_PDF=$(base64 -w 0 document.pdf)
|
||||
|
||||
curl -X POST http://144.76.224.179:9102/route \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"agent\": \"solarius\",
|
||||
\"message\": \"Проаналізуй цей документ\",
|
||||
\"mode\": \"chat\",
|
||||
\"payload\": {
|
||||
\"context\": {
|
||||
\"files\": [
|
||||
{
|
||||
\"name\": \"document.pdf\",
|
||||
\"type\": \"application/pdf\",
|
||||
\"data\": \"data:application/pdf;base64,$BASE64_PDF\"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
**Очікується:** `{ "data": { "text": "Документ містить..." }, "metadata": { "has_files": true } }`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Підтримка моделей
|
||||
|
||||
### Vision-моделі (підтримують зображення):
|
||||
|
||||
| Агент | Модель | Provider | Vision |
|
||||
|-------|--------|----------|--------|
|
||||
| Sofia | grok-4.1 | xAI | ✅ |
|
||||
| Spectra | qwen3-vl:latest | Ollama | ✅ |
|
||||
| Daarwizz | qwen3-8b | Ollama | ❌ → можна додати |
|
||||
|
||||
### Text-моделі (не підтримують зображення):
|
||||
|
||||
| Агент | Модель | Provider | Vision |
|
||||
|-------|--------|----------|--------|
|
||||
| Solarius | deepseek-r1:70b | Ollama | ❌ |
|
||||
| Monitor | mistral-nemo:12b | Ollama | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Додаткові покращення
|
||||
|
||||
### 1. STT (Speech-to-Text) для голосових повідомлень
|
||||
|
||||
**Endpoint:** `POST /stt`
|
||||
|
||||
```python
|
||||
@app.post("/stt")
|
||||
async def speech_to_text(audio_data: str):
|
||||
"""Конвертує аудіо в текст"""
|
||||
# Використати Whisper або інший STT сервіс
|
||||
# TODO: Інтеграція з STT Service на НОДА2
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. OCR для документів
|
||||
|
||||
**Endpoint:** `POST /ocr`
|
||||
|
||||
```python
|
||||
@app.post("/ocr")
|
||||
async def extract_text_from_image(image_data: str):
|
||||
"""Витягує текст з зображення"""
|
||||
# Використати Tesseract або аналогічний
|
||||
# TODO: Інтеграція з OCR Service
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Веб-пошук
|
||||
|
||||
**Endpoint:** `POST /web-search`
|
||||
|
||||
```python
|
||||
@app.post("/web-search")
|
||||
async def web_search(query: str):
|
||||
"""Виконує веб-пошук"""
|
||||
# Використати Google Search API або аналогічний
|
||||
# TODO: Інтеграція з Web Search Service
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Чекліст реалізації
|
||||
|
||||
### Backend (Router на NODE1):
|
||||
- [ ] Оновити `router-config-final.yml` з `supports_vision: true`
|
||||
- [ ] Додати `process_images()` функцію
|
||||
- [ ] Додати `process_files()` функцію
|
||||
- [ ] Оновити `/route` endpoint для обробки images/files
|
||||
- [ ] Додати маппінг агентів до vision-моделей
|
||||
- [ ] Протестувати з Sofia (grok-4.1)
|
||||
- [ ] Протестувати з Spectra (qwen3-vl:latest)
|
||||
- [ ] Додати error handling для non-vision агентів
|
||||
- [ ] Додати логування розміру images/files
|
||||
- [ ] Оновити документацію API
|
||||
|
||||
### Frontend (вже готово):
|
||||
- [x] Enhanced Chat UI
|
||||
- [x] MultimodalInput компонент
|
||||
- [x] Image upload
|
||||
- [x] File upload
|
||||
- [x] Voice recording (Web Audio API)
|
||||
- [x] Switch toggle для Enhanced режиму
|
||||
- [x] Base64 encoding для images
|
||||
- [x] Відправка в payload.context.images
|
||||
- [x] Відправка в payload.context.files
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Деплой
|
||||
|
||||
### 1. SSH до NODE1
|
||||
|
||||
```bash
|
||||
ssh root@144.76.224.179
|
||||
```
|
||||
|
||||
### 2. Backup існуючої конфігурації
|
||||
|
||||
```bash
|
||||
cd /opt/microdao-daarion/router
|
||||
cp router-config-final.yml router-config-final.yml.backup
|
||||
cp main.py main.py.backup # якщо є
|
||||
```
|
||||
|
||||
### 3. Оновити код
|
||||
|
||||
```bash
|
||||
# Завантажити новий код з Git або вручну відредагувати
|
||||
nano router-config-final.yml
|
||||
nano main.py
|
||||
```
|
||||
|
||||
### 4. Перезапустити Router
|
||||
|
||||
```bash
|
||||
docker restart dagi-router
|
||||
# або
|
||||
docker-compose restart router
|
||||
```
|
||||
|
||||
### 5. Перевірити логи
|
||||
|
||||
```bash
|
||||
docker logs -f dagi-router
|
||||
```
|
||||
|
||||
**Шукати:**
|
||||
```
|
||||
✅ Processed 1 images
|
||||
✅ Vision model grok-4.1 selected for sofia
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 Документація
|
||||
|
||||
**Файли для оновлення:**
|
||||
- `INFRASTRUCTURE.md` - додати info про мультимодальність
|
||||
- `router-config-final.yml` - оновити конфігурацію
|
||||
- `README.md` (Router) - додати приклади з images/files
|
||||
|
||||
---
|
||||
|
||||
**СТАТУС:** 📝 Інструкції готові для бекенд команди
|
||||
**Наступний крок:** Реалізувати на NODE1 Router
|
||||
|
||||
Reference in New Issue
Block a user