mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
feat(telegram-ollama-bot): add Telegram bot for local LLM inference via Ollama
- NestJS-based Telegram bot with nestjs-telegraf - Ollama service for API communication with Gemma 3 4B - Commands: /start, /help, /models, /model, /mode, /clear, /status - Multiple modes: default, classify, summarize, translate, code - Chat history with context (last 10 messages) - User access control via TELEGRAM_ALLOWED_USERS - Health endpoint for monitoring - Updated MAC_MINI_SERVER.md with Ollama documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2975e5d2a1
commit
3f64c7422f
15 changed files with 1061 additions and 121 deletions
|
|
@ -16,12 +16,12 @@ Cloudflare Tunnel (cloudflared)
|
|||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Mac Mini (mana-server) │
|
||||
│ Mac Mini M4 (mana-server) │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ PostgreSQL │ │ Redis │ │
|
||||
│ │ (Docker) │ │ (Docker) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────┐ │
|
||||
│ │ PostgreSQL │ │ Redis │ │ Ollama │ │
|
||||
│ │ (Docker) │ │ (Docker) │ │ (nativ) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Docker Container │ │
|
||||
|
|
@ -36,10 +36,18 @@ Cloudflare Tunnel (cloudflared)
|
|||
│ │ ├── clock-backend (Port 3017) │ │
|
||||
│ │ └── clock-web (Port 5187) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ ▲ │
|
||||
│ │ host.docker.internal:11434 │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Ollama (Port 11434) - Gemma 3 4B │ │
|
||||
│ │ ~53 t/s Generation | Metal GPU Acceleration │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ LaunchAgents (Autostart) │ │
|
||||
│ │ ├── cloudflared (Tunnel) │ │
|
||||
│ │ ├── ollama (LLM Service) │ │
|
||||
│ │ ├── docker-startup (Container beim Boot) │ │
|
||||
│ │ └── health-check (alle 5 Minuten) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
|
|
@ -383,6 +391,116 @@ docker image prune -a
|
|||
| `stop.sh` | Stoppt alle Container |
|
||||
| `deploy.sh` | Pullt neue Images und startet neu |
|
||||
|
||||
## Ollama (Lokale KI)
|
||||
|
||||
Ollama läuft nativ auf dem Mac Mini für lokale LLM-Inferenz (Klassifizierung, Text-Analyse, etc.).
|
||||
|
||||
### Hardware
|
||||
|
||||
- **Chip:** Apple M4 (10 Cores)
|
||||
- **RAM:** 16 GB Unified Memory
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Bereits installiert via Homebrew
|
||||
/opt/homebrew/bin/brew install ollama
|
||||
/opt/homebrew/bin/brew services start ollama
|
||||
```
|
||||
|
||||
### Konfiguration
|
||||
|
||||
**LaunchAgent:** `~/Library/LaunchAgents/homebrew.mxcl.ollama.plist`
|
||||
|
||||
Optimierungen bereits aktiviert:
|
||||
- `OLLAMA_FLASH_ATTENTION=1` - Schnellere Attention-Berechnung
|
||||
- `OLLAMA_KV_CACHE_TYPE=q8_0` - Effizienterer KV-Cache
|
||||
|
||||
### Verfügbare Modelle
|
||||
|
||||
| Modell | Größe | Zweck |
|
||||
|--------|-------|-------|
|
||||
| gemma3:4b | 3.3 GB | Klassifizierung, kurze Antworten |
|
||||
|
||||
```bash
|
||||
# Modelle auflisten
|
||||
/opt/homebrew/bin/ollama list
|
||||
|
||||
# Neues Modell herunterladen
|
||||
/opt/homebrew/bin/ollama pull gemma3:12b
|
||||
```
|
||||
|
||||
### Performance (gemessen)
|
||||
|
||||
| Metrik | Wert |
|
||||
|--------|------|
|
||||
| Text Generation | ~53 tokens/sec |
|
||||
| Prompt Processing | ~260 tokens/sec |
|
||||
| Latenz (kurze Anfrage) | ~0.4 sec |
|
||||
|
||||
### API-Zugriff
|
||||
|
||||
**Lokaler Endpunkt:** `http://localhost:11434`
|
||||
|
||||
```bash
|
||||
# Generate API
|
||||
curl http://localhost:11434/api/generate -d '{
|
||||
"model": "gemma3:4b",
|
||||
"prompt": "Klassifiziere: Newsletter oder Spam?",
|
||||
"stream": false
|
||||
}'
|
||||
|
||||
# OpenAI-kompatible API
|
||||
curl http://localhost:11434/v1/chat/completions -d '{
|
||||
"model": "gemma3:4b",
|
||||
"messages": [{"role": "user", "content": "Hallo"}]
|
||||
}'
|
||||
```
|
||||
|
||||
### Zugriff aus Docker-Containern
|
||||
|
||||
Docker-Container können Ollama über `host.docker.internal` erreichen:
|
||||
|
||||
```bash
|
||||
# Aus einem Container heraus
|
||||
curl http://host.docker.internal:11434/api/generate -d '...'
|
||||
```
|
||||
|
||||
Oder in Docker Compose Environment-Variablen:
|
||||
```yaml
|
||||
environment:
|
||||
OLLAMA_URL: http://host.docker.internal:11434
|
||||
```
|
||||
|
||||
### Ollama Management
|
||||
|
||||
```bash
|
||||
# Service Status
|
||||
/opt/homebrew/bin/brew services info ollama
|
||||
|
||||
# Service neustarten
|
||||
/opt/homebrew/bin/brew services restart ollama
|
||||
|
||||
# Logs prüfen
|
||||
tail -f /opt/homebrew/var/log/ollama.log
|
||||
|
||||
# Modell entfernen
|
||||
/opt/homebrew/bin/ollama rm gemma3:4b
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
```bash
|
||||
# Prüfen ob Ollama läuft
|
||||
curl http://localhost:11434/api/version
|
||||
|
||||
# GPU-Nutzung prüfen (sollte Metal verwenden)
|
||||
/opt/homebrew/bin/ollama ps
|
||||
|
||||
# Bei Problemen: Service neustarten
|
||||
/opt/homebrew/bin/brew services restart ollama
|
||||
```
|
||||
|
||||
## Chronologie der Einrichtung
|
||||
|
||||
1. **Docker Setup** - PostgreSQL, Redis, App-Container
|
||||
|
|
@ -392,3 +510,4 @@ docker image prune -a
|
|||
5. **Health Checks** - Automatische Überwachung
|
||||
6. **Telegram Notifications** - Alerts bei Fehlern
|
||||
7. **Email Notifications** - Redundante Benachrichtigung
|
||||
8. **Ollama** - Lokale LLM-Inferenz (Gemma 3 4B)
|
||||
|
|
|
|||
289
pnpm-lock.yaml
generated
289
pnpm-lock.yaml
generated
|
|
@ -4438,7 +4438,7 @@ importers:
|
|||
version: 1.57.0
|
||||
jest:
|
||||
specifier: ^29.0.0
|
||||
version: 29.7.0(@types/node@24.10.1)
|
||||
version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||
|
|
@ -4613,6 +4613,49 @@ importers:
|
|||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
services/telegram-ollama-bot:
|
||||
dependencies:
|
||||
'@nestjs/common':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/config':
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
|
||||
'@nestjs/core':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
|
||||
nestjs-telegraf:
|
||||
specifier: ^2.8.0
|
||||
version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3)(typescript@5.9.3)
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
rxjs:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.2
|
||||
telegraf:
|
||||
specifier: ^4.16.3
|
||||
version: 4.16.3
|
||||
devDependencies:
|
||||
'@nestjs/cli':
|
||||
specifier: ^10.4.9
|
||||
version: 10.4.9
|
||||
'@nestjs/schematics':
|
||||
specifier: ^10.2.3
|
||||
version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
|
||||
'@types/node':
|
||||
specifier: ^22.10.5
|
||||
version: 22.19.1
|
||||
rimraf:
|
||||
specifier: ^6.0.1
|
||||
version: 6.1.2
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
services/telegram-stats-bot:
|
||||
dependencies:
|
||||
'@nestjs/common':
|
||||
|
|
@ -23314,7 +23357,7 @@ snapshots:
|
|||
wrap-ansi: 7.0.0
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
expo-router: 6.0.15(ohit2up6tuxb3x34brxduivol4)
|
||||
expo-router: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4)
|
||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
|
|
@ -24583,7 +24626,7 @@ snapshots:
|
|||
jest-util: 30.2.0
|
||||
slash: 3.0.0
|
||||
|
||||
'@jest/core@29.7.0':
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
|
|
@ -24597,7 +24640,7 @@ snapshots:
|
|||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@22.19.1)
|
||||
jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
|
|
@ -24618,7 +24661,7 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))':
|
||||
'@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@jest/console': 29.7.0
|
||||
'@jest/reporters': 29.7.0
|
||||
|
|
@ -24632,7 +24675,7 @@ snapshots:
|
|||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
|
|
@ -25051,6 +25094,32 @@ snapshots:
|
|||
axios: 1.13.2
|
||||
rxjs: 7.8.2
|
||||
|
||||
'@nestjs/cli@10.4.9':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||
'@angular-devkit/schematics': 17.3.11(chokidar@3.6.0)
|
||||
'@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0)
|
||||
'@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2)
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.6.0
|
||||
cli-table3: 0.6.5
|
||||
commander: 4.1.1
|
||||
fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1)
|
||||
glob: 10.4.5
|
||||
inquirer: 8.2.6
|
||||
node-emoji: 1.11.0
|
||||
ora: 5.4.1
|
||||
tree-kill: 1.2.2
|
||||
tsconfig-paths: 4.2.0
|
||||
tsconfig-paths-webpack-plugin: 4.2.0
|
||||
typescript: 5.7.2
|
||||
webpack: 5.97.1
|
||||
webpack-node-externals: 3.0.0
|
||||
transitivePeerDependencies:
|
||||
- esbuild
|
||||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@nestjs/cli@10.4.9(esbuild@0.27.0)':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 17.3.11(chokidar@3.6.0)
|
||||
|
|
@ -28446,19 +28515,6 @@ snapshots:
|
|||
pretty-format: 27.5.1
|
||||
optional: true
|
||||
|
||||
'@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
jest-matcher-utils: 30.2.0
|
||||
picocolors: 1.1.1
|
||||
pretty-format: 30.2.0
|
||||
react: 19.1.0
|
||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
react-test-renderer: 19.1.0(react@19.1.0)
|
||||
redent: 3.0.0
|
||||
optionalDependencies:
|
||||
jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))
|
||||
optional: true
|
||||
|
||||
'@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
jest-matcher-utils: 30.2.0
|
||||
|
|
@ -31593,13 +31649,13 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
create-jest@29.7.0(@types/node@24.10.1):
|
||||
create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@24.10.1)
|
||||
jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -34411,53 +34467,6 @@ snapshots:
|
|||
- supports-color
|
||||
optional: true
|
||||
|
||||
expo-router@6.0.15(ohit2up6tuxb3x34brxduivol4):
|
||||
dependencies:
|
||||
'@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@expo/schema-utils': 0.1.7
|
||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0)
|
||||
'@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
client-only: 0.0.1
|
||||
debug: 4.4.3
|
||||
escape-string-regexp: 4.0.0
|
||||
expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))
|
||||
expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
expo-server: 1.0.4
|
||||
fast-deep-equal: 3.1.3
|
||||
invariant: 2.2.4
|
||||
nanoid: 3.3.11
|
||||
query-string: 7.1.3
|
||||
react: 19.1.0
|
||||
react-fast-compare: 3.2.2
|
||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)
|
||||
react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
semver: 7.6.3
|
||||
server-only: 0.0.1
|
||||
sf-symbols-typescript: 2.1.0
|
||||
shallowequal: 1.1.0
|
||||
use-latest-callback: 0.2.6(react@19.1.0)
|
||||
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
'@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12))
|
||||
transitivePeerDependencies:
|
||||
- '@react-native-masked-view/masked-view'
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
expo-router@6.0.15(qjp3usx4acoq47dkosl6pmu254):
|
||||
dependencies:
|
||||
'@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)
|
||||
|
|
@ -35403,6 +35412,23 @@ snapshots:
|
|||
typescript: 5.7.2
|
||||
webpack: 5.97.1(esbuild@0.27.0)
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.6.0
|
||||
cosmiconfig: 8.3.6(typescript@5.7.2)
|
||||
deepmerge: 4.3.1
|
||||
fs-extra: 10.1.0
|
||||
memfs: 3.5.3
|
||||
minimatch: 3.1.2
|
||||
node-abort-controller: 3.1.1
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.7.3
|
||||
tapable: 2.3.0
|
||||
typescript: 5.7.2
|
||||
webpack: 5.97.1
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
|
|
@ -36600,16 +36626,16 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-cli@29.7.0(@types/node@24.10.1):
|
||||
jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@24.10.1)
|
||||
create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@24.10.1)
|
||||
jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.2
|
||||
|
|
@ -36697,36 +36723,6 @@ snapshots:
|
|||
- ts-node
|
||||
optional: true
|
||||
|
||||
jest-config@29.7.0(@types/node@22.19.1):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.5)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
|
|
@ -36758,7 +36754,38 @@ snapshots:
|
|||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@24.10.1):
|
||||
jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.28.5)
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
deepmerge: 4.3.1
|
||||
glob: 7.2.3
|
||||
graceful-fs: 4.2.11
|
||||
jest-circus: 29.7.0
|
||||
jest-environment-node: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
jest-regex-util: 29.6.3
|
||||
jest-resolve: 29.7.0
|
||||
jest-runner: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
micromatch: 4.0.8
|
||||
parse-json: 5.2.0
|
||||
pretty-format: 29.7.0
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.1
|
||||
ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.28.5
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
|
|
@ -36784,6 +36811,7 @@ snapshots:
|
|||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 24.10.1
|
||||
ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
|
@ -37370,12 +37398,12 @@ snapshots:
|
|||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest@29.7.0(@types/node@24.10.1):
|
||||
jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0(@types/node@24.10.1)
|
||||
jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
|
|
@ -41338,16 +41366,6 @@ snapshots:
|
|||
webpack-sources: 3.3.3
|
||||
optional: true
|
||||
|
||||
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.19.12)):
|
||||
dependencies:
|
||||
acorn-loose: 8.5.2
|
||||
neo-async: 2.6.2
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
webpack: 5.100.2(esbuild@0.19.12)
|
||||
webpack-sources: 3.3.3
|
||||
optional: true
|
||||
|
||||
react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)):
|
||||
dependencies:
|
||||
acorn-loose: 8.5.2
|
||||
|
|
@ -42668,6 +42686,15 @@ snapshots:
|
|||
optionalDependencies:
|
||||
esbuild: 0.27.0
|
||||
|
||||
terser-webpack-plugin@5.3.14(webpack@5.97.1):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 6.0.2
|
||||
terser: 5.44.1
|
||||
webpack: 5.97.1
|
||||
|
||||
terser@5.44.1:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
|
|
@ -43901,6 +43928,36 @@ snapshots:
|
|||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.97.1:
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.8
|
||||
'@webassemblyjs/ast': 1.14.1
|
||||
'@webassemblyjs/wasm-edit': 1.14.1
|
||||
'@webassemblyjs/wasm-parser': 1.14.1
|
||||
acorn: 8.15.0
|
||||
browserslist: 4.28.0
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.18.3
|
||||
es-module-lexer: 1.7.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.11
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
loader-runner: 4.3.1
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.3.0
|
||||
tapable: 2.3.0
|
||||
terser-webpack-plugin: 5.3.14(webpack@5.97.1)
|
||||
watchpack: 2.4.4
|
||||
webpack-sources: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
|
||||
webpack@5.97.1(esbuild@0.27.0):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
|
|
|
|||
130
services/telegram-ollama-bot/CLAUDE.md
Normal file
130
services/telegram-ollama-bot/CLAUDE.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Telegram Ollama Bot
|
||||
|
||||
Telegram Bot für lokale LLM-Inferenz via Ollama auf dem Mac Mini Server.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: NestJS 10
|
||||
- **Telegram**: nestjs-telegraf + Telegraf
|
||||
- **LLM**: Ollama API (Gemma 3 4B)
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
pnpm start:dev # Start with hot reload
|
||||
|
||||
# Build
|
||||
pnpm build # Production build
|
||||
|
||||
# Type check
|
||||
pnpm type-check # Check TypeScript types
|
||||
```
|
||||
|
||||
## Telegram Commands
|
||||
|
||||
| Command | Beschreibung |
|
||||
|---------|--------------|
|
||||
| `/start` | Hilfe anzeigen |
|
||||
| `/help` | Hilfe anzeigen |
|
||||
| `/models` | Verfügbare Modelle auflisten |
|
||||
| `/model [name]` | Modell wechseln |
|
||||
| `/mode [modus]` | System-Prompt ändern |
|
||||
| `/clear` | Chat-Verlauf löschen |
|
||||
| `/status` | Ollama-Status prüfen |
|
||||
|
||||
## Modi
|
||||
|
||||
| Modus | Beschreibung |
|
||||
|-------|--------------|
|
||||
| `default` | Allgemeiner Assistent |
|
||||
| `classify` | Text-Klassifizierung |
|
||||
| `summarize` | Zusammenfassungen |
|
||||
| `translate` | Übersetzungen |
|
||||
| `code` | Programmier-Hilfe |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```env
|
||||
# Server
|
||||
PORT=3301
|
||||
|
||||
# Telegram
|
||||
TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather
|
||||
TELEGRAM_ALLOWED_USERS=123,456 # Optional: Nur diese User IDs erlauben
|
||||
|
||||
# Ollama
|
||||
OLLAMA_URL=http://localhost:11434 # Ollama API URL
|
||||
OLLAMA_MODEL=gemma3:4b # Standard-Modell
|
||||
OLLAMA_TIMEOUT=120000 # Timeout in ms
|
||||
```
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
services/telegram-ollama-bot/
|
||||
├── src/
|
||||
│ ├── main.ts # Entry point
|
||||
│ ├── app.module.ts # Root module
|
||||
│ ├── health.controller.ts # Health endpoint
|
||||
│ ├── config/
|
||||
│ │ └── configuration.ts # Config & System Prompts
|
||||
│ ├── bot/
|
||||
│ │ ├── bot.module.ts
|
||||
│ │ └── bot.update.ts # Command handlers
|
||||
│ └── ollama/
|
||||
│ ├── ollama.module.ts
|
||||
│ └── ollama.service.ts # Ollama API client
|
||||
└── Dockerfile
|
||||
```
|
||||
|
||||
## Deployment auf Mac Mini
|
||||
|
||||
### Option 1: Docker
|
||||
|
||||
```yaml
|
||||
# In docker-compose.macmini.yml
|
||||
telegram-ollama-bot:
|
||||
image: ghcr.io/memo-2023/telegram-ollama-bot:latest
|
||||
container_name: manacore-telegram-ollama-bot
|
||||
restart: always
|
||||
environment:
|
||||
PORT: 3301
|
||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN}
|
||||
OLLAMA_URL: http://host.docker.internal:11434
|
||||
OLLAMA_MODEL: gemma3:4b
|
||||
ports:
|
||||
- "3301:3301"
|
||||
```
|
||||
|
||||
### Option 2: Nativ (empfohlen für beste Ollama-Performance)
|
||||
|
||||
```bash
|
||||
# Auf dem Mac Mini
|
||||
cd ~/projects/manacore-monorepo/services/telegram-ollama-bot
|
||||
pnpm install
|
||||
pnpm build
|
||||
TELEGRAM_BOT_TOKEN=xxx OLLAMA_URL=http://localhost:11434 pnpm start:prod
|
||||
```
|
||||
|
||||
## Neuen Bot erstellen
|
||||
|
||||
1. Öffne @BotFather in Telegram
|
||||
2. Sende `/newbot`
|
||||
3. Wähle einen Namen (z.B. "ManaCore Ollama")
|
||||
4. Wähle einen Username (z.B. "manacore_ollama_bot")
|
||||
5. Kopiere den Token
|
||||
|
||||
## Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3301/health
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Chat-Verlauf**: Behält die letzten 10 Nachrichten für Kontext
|
||||
- **Mehrere Modi**: Verschiedene System-Prompts für unterschiedliche Aufgaben
|
||||
- **Modell-Wechsel**: Dynamisch zwischen installierten Modellen wechseln
|
||||
- **User-Beschränkung**: Optional nur bestimmte Telegram-User erlauben
|
||||
- **Lange Antworten**: Automatisches Splitting bei >4000 Zeichen
|
||||
44
services/telegram-ollama-bot/Dockerfile
Normal file
44
services/telegram-ollama-bot/Dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile || pnpm install
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN pnpm build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN pnpm install --prod --frozen-lockfile || pnpm install --prod
|
||||
|
||||
# Copy built application
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Set environment
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3301
|
||||
|
||||
EXPOSE 3301
|
||||
|
||||
CMD ["node", "dist/main.js"]
|
||||
8
services/telegram-ollama-bot/nest-cli.json
Normal file
8
services/telegram-ollama-bot/nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
35
services/telegram-ollama-bot/package.json
Normal file
35
services/telegram-ollama-bot/package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@manacore/telegram-ollama-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "Telegram bot for local LLM inference via Ollama",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"nestjs-telegraf": "^2.8.0",
|
||||
"telegraf": "^4.16.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@types/node": "^22.10.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
27
services/telegram-ollama-bot/src/app.module.ts
Normal file
27
services/telegram-ollama-bot/src/app.module.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { TelegrafModule } from 'nestjs-telegraf';
|
||||
import configuration from './config/configuration';
|
||||
import { BotModule } from './bot/bot.module';
|
||||
import { OllamaModule } from './ollama/ollama.module';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
}),
|
||||
TelegrafModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
token: configService.get<string>('telegram.token') || '',
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
BotModule,
|
||||
OllamaModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class AppModule {}
|
||||
9
services/telegram-ollama-bot/src/bot/bot.module.ts
Normal file
9
services/telegram-ollama-bot/src/bot/bot.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { BotUpdate } from './bot.update';
|
||||
import { OllamaModule } from '../ollama/ollama.module';
|
||||
|
||||
@Module({
|
||||
imports: [OllamaModule],
|
||||
providers: [BotUpdate],
|
||||
})
|
||||
export class BotModule {}
|
||||
278
services/telegram-ollama-bot/src/bot/bot.update.ts
Normal file
278
services/telegram-ollama-bot/src/bot/bot.update.ts
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { Update, Ctx, Start, Help, Command, On, Message } from 'nestjs-telegraf';
|
||||
import { Context } from 'telegraf';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OllamaService } from '../ollama/ollama.service';
|
||||
import { SYSTEM_PROMPTS } from '../config/configuration';
|
||||
|
||||
interface UserSession {
|
||||
systemPrompt: string;
|
||||
model: string;
|
||||
history: { role: 'user' | 'assistant'; content: string }[];
|
||||
}
|
||||
|
||||
@Update()
|
||||
export class BotUpdate {
|
||||
private readonly logger = new Logger(BotUpdate.name);
|
||||
private readonly allowedUsers: number[];
|
||||
private sessions: Map<number, UserSession> = new Map();
|
||||
|
||||
constructor(
|
||||
private readonly ollamaService: OllamaService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.allowedUsers = this.configService.get<number[]>('telegram.allowedUsers') || [];
|
||||
}
|
||||
|
||||
private isAllowed(userId: number): boolean {
|
||||
// If no users configured, allow all
|
||||
if (this.allowedUsers.length === 0) return true;
|
||||
return this.allowedUsers.includes(userId);
|
||||
}
|
||||
|
||||
private getSession(userId: number): UserSession {
|
||||
if (!this.sessions.has(userId)) {
|
||||
this.sessions.set(userId, {
|
||||
systemPrompt: SYSTEM_PROMPTS.default,
|
||||
model: this.ollamaService.getDefaultModel(),
|
||||
history: [],
|
||||
});
|
||||
}
|
||||
return this.sessions.get(userId)!;
|
||||
}
|
||||
|
||||
private formatHelp(): string {
|
||||
return `<b>Ollama Bot - Lokale KI</b>
|
||||
|
||||
<b>Commands:</b>
|
||||
/start - Diese Hilfe anzeigen
|
||||
/help - Diese Hilfe anzeigen
|
||||
/models - Verfügbare Modelle anzeigen
|
||||
/model [name] - Modell wechseln
|
||||
/mode [modus] - System-Prompt ändern
|
||||
/clear - Chat-Verlauf löschen
|
||||
/status - Ollama Status prüfen
|
||||
|
||||
<b>Modi:</b>
|
||||
• <code>default</code> - Allgemeiner Assistent
|
||||
• <code>classify</code> - Text-Klassifizierung
|
||||
• <code>summarize</code> - Zusammenfassungen
|
||||
• <code>translate</code> - Übersetzungen
|
||||
• <code>code</code> - Programmier-Hilfe
|
||||
|
||||
<b>Verwendung:</b>
|
||||
Schreibe einfach eine Nachricht und ich antworte!
|
||||
|
||||
<b>Aktuelles Modell:</b> <code>${this.ollamaService.getDefaultModel()}</code>`;
|
||||
}
|
||||
|
||||
@Start()
|
||||
async start(@Ctx() ctx: Context) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`/start from user ${userId}`);
|
||||
await ctx.replyWithHTML(this.formatHelp());
|
||||
}
|
||||
|
||||
@Help()
|
||||
async help(@Ctx() ctx: Context) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.replyWithHTML(this.formatHelp());
|
||||
}
|
||||
|
||||
@Command('models')
|
||||
async models(@Ctx() ctx: Context) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`/models from user ${userId}`);
|
||||
|
||||
const models = await this.ollamaService.listModels();
|
||||
if (models.length === 0) {
|
||||
await ctx.reply('Keine Modelle gefunden. Ist Ollama gestartet?');
|
||||
return;
|
||||
}
|
||||
|
||||
const session = this.getSession(userId);
|
||||
const modelList = models
|
||||
.map((m) => {
|
||||
const sizeMB = (m.size / 1024 / 1024).toFixed(0);
|
||||
const active = m.name === session.model ? ' ✓' : '';
|
||||
return `• <code>${m.name}</code> (${sizeMB} MB)${active}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
await ctx.replyWithHTML(
|
||||
`<b>Verfügbare Modelle:</b>\n\n${modelList}\n\nWechseln mit: /model [name]`
|
||||
);
|
||||
}
|
||||
|
||||
@Command('model')
|
||||
async setModel(@Ctx() ctx: Context, @Message('text') text: string) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const modelName = text.replace('/model', '').trim();
|
||||
if (!modelName) {
|
||||
const session = this.getSession(userId);
|
||||
await ctx.reply(`Aktuelles Modell: ${session.model}\n\nVerwendung: /model gemma3:4b`);
|
||||
return;
|
||||
}
|
||||
|
||||
const models = await this.ollamaService.listModels();
|
||||
const exists = models.some((m) => m.name === modelName);
|
||||
|
||||
if (!exists) {
|
||||
await ctx.reply(
|
||||
`Modell "${modelName}" nicht gefunden. Verfügbar: ${models.map((m) => m.name).join(', ')}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = this.getSession(userId);
|
||||
session.model = modelName;
|
||||
session.history = []; // Clear history on model change
|
||||
|
||||
this.logger.log(`User ${userId} switched to model ${modelName}`);
|
||||
await ctx.reply(`Modell gewechselt zu: ${modelName}`);
|
||||
}
|
||||
|
||||
@Command('mode')
|
||||
async setMode(@Ctx() ctx: Context, @Message('text') text: string) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const mode = text.replace('/mode', '').trim().toLowerCase();
|
||||
const availableModes = Object.keys(SYSTEM_PROMPTS);
|
||||
|
||||
if (!mode) {
|
||||
const session = this.getSession(userId);
|
||||
const currentMode =
|
||||
Object.entries(SYSTEM_PROMPTS).find(([_, v]) => v === session.systemPrompt)?.[0] ||
|
||||
'custom';
|
||||
await ctx.reply(`Aktueller Modus: ${currentMode}\n\nVerfügbar: ${availableModes.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SYSTEM_PROMPTS[mode]) {
|
||||
await ctx.reply(`Unbekannter Modus: ${mode}\n\nVerfügbar: ${availableModes.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const session = this.getSession(userId);
|
||||
session.systemPrompt = SYSTEM_PROMPTS[mode];
|
||||
session.history = []; // Clear history on mode change
|
||||
|
||||
this.logger.log(`User ${userId} switched to mode ${mode}`);
|
||||
await ctx.reply(`Modus gewechselt zu: ${mode}`);
|
||||
}
|
||||
|
||||
@Command('clear')
|
||||
async clear(@Ctx() ctx: Context) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const session = this.getSession(userId);
|
||||
session.history = [];
|
||||
|
||||
this.logger.log(`User ${userId} cleared history`);
|
||||
await ctx.reply('Chat-Verlauf gelöscht.');
|
||||
}
|
||||
|
||||
@Command('status')
|
||||
async status(@Ctx() ctx: Context) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const connected = await this.ollamaService.checkConnection();
|
||||
const models = await this.ollamaService.listModels();
|
||||
const session = this.getSession(userId);
|
||||
|
||||
const statusText = `<b>Ollama Status</b>
|
||||
|
||||
<b>Verbindung:</b> ${connected ? '✅ Online' : '❌ Offline'}
|
||||
<b>Modelle:</b> ${models.length}
|
||||
<b>Dein Modell:</b> <code>${session.model}</code>
|
||||
<b>Chat-Verlauf:</b> ${session.history.length} Nachrichten`;
|
||||
|
||||
await ctx.replyWithHTML(statusText);
|
||||
}
|
||||
|
||||
@On('text')
|
||||
async onMessage(@Ctx() ctx: Context, @Message('text') text: string) {
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId || !this.isAllowed(userId)) {
|
||||
await ctx.reply('Zugriff verweigert.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore commands
|
||||
if (text.startsWith('/')) return;
|
||||
|
||||
this.logger.log(`Message from user ${userId}: ${text.substring(0, 50)}...`);
|
||||
|
||||
const session = this.getSession(userId);
|
||||
|
||||
// Show typing indicator
|
||||
await ctx.sendChatAction('typing');
|
||||
|
||||
try {
|
||||
// Add user message to history
|
||||
session.history.push({ role: 'user', content: text });
|
||||
|
||||
// Keep only last 10 messages to avoid context overflow
|
||||
if (session.history.length > 10) {
|
||||
session.history = session.history.slice(-10);
|
||||
}
|
||||
|
||||
// Build messages with system prompt
|
||||
const messages: { role: 'user' | 'assistant' | 'system'; content: string }[] = [
|
||||
{ role: 'system', content: session.systemPrompt },
|
||||
...session.history,
|
||||
];
|
||||
|
||||
const response = await this.ollamaService.chat(messages, session.model);
|
||||
|
||||
// Add assistant response to history
|
||||
session.history.push({ role: 'assistant', content: response });
|
||||
|
||||
// Split long messages (Telegram limit is 4096 chars)
|
||||
if (response.length > 4000) {
|
||||
const chunks = response.match(/.{1,4000}/gs) || [];
|
||||
for (const chunk of chunks) {
|
||||
await ctx.reply(chunk);
|
||||
}
|
||||
} else {
|
||||
await ctx.reply(response);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing message:`, error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await ctx.reply(`Fehler: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
services/telegram-ollama-bot/src/config/configuration.ts
Normal file
25
services/telegram-ollama-bot/src/config/configuration.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export default () => ({
|
||||
port: parseInt(process.env.PORT || '3301', 10),
|
||||
telegram: {
|
||||
token: process.env.TELEGRAM_BOT_TOKEN,
|
||||
allowedUsers:
|
||||
process.env.TELEGRAM_ALLOWED_USERS?.split(',').map((id) => parseInt(id, 10)) || [],
|
||||
},
|
||||
ollama: {
|
||||
url: process.env.OLLAMA_URL || 'http://localhost:11434',
|
||||
model: process.env.OLLAMA_MODEL || 'gemma3:4b',
|
||||
timeout: parseInt(process.env.OLLAMA_TIMEOUT || '120000', 10),
|
||||
},
|
||||
});
|
||||
|
||||
export const SYSTEM_PROMPTS: Record<string, string> = {
|
||||
default:
|
||||
'Du bist ein hilfreicher Assistent. Antworte präzise und auf Deutsch, wenn der User Deutsch schreibt.',
|
||||
classify:
|
||||
'Du bist ein Klassifikations-Experte. Analysiere den gegebenen Text und ordne ihn einer passenden Kategorie zu. Antworte kurz und präzise.',
|
||||
summarize:
|
||||
'Du bist ein Zusammenfassungs-Experte. Fasse den gegebenen Text kurz und prägnant zusammen. Behalte die wichtigsten Informationen bei.',
|
||||
translate:
|
||||
'Du bist ein Übersetzer. Übersetze den Text in die gewünschte Sprache. Behalte den Ton und Stil bei.',
|
||||
code: 'Du bist ein Programmier-Assistent. Hilf bei Code-Fragen, erkläre Konzepte und schlage Verbesserungen vor.',
|
||||
};
|
||||
21
services/telegram-ollama-bot/src/health.controller.ts
Normal file
21
services/telegram-ollama-bot/src/health.controller.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { OllamaService } from './ollama/ollama.service';
|
||||
|
||||
@Controller()
|
||||
export class HealthController {
|
||||
constructor(private readonly ollamaService: OllamaService) {}
|
||||
|
||||
@Get('health')
|
||||
async health() {
|
||||
const ollamaConnected = await this.ollamaService.checkConnection();
|
||||
|
||||
return {
|
||||
status: ollamaConnected ? 'ok' : 'degraded',
|
||||
timestamp: new Date().toISOString(),
|
||||
ollama: {
|
||||
connected: ollamaConnected,
|
||||
model: this.ollamaService.getDefaultModel(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
19
services/telegram-ollama-bot/src/main.ts
Normal file
19
services/telegram-ollama-bot/src/main.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const configService = app.get(ConfigService);
|
||||
const port = configService.get<number>('port') || 3301;
|
||||
|
||||
await app.listen(port);
|
||||
logger.log(`Telegram Ollama Bot running on port ${port}`);
|
||||
logger.log(`Ollama URL: ${configService.get<string>('ollama.url')}`);
|
||||
logger.log(`Default model: ${configService.get<string>('ollama.model')}`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
8
services/telegram-ollama-bot/src/ollama/ollama.module.ts
Normal file
8
services/telegram-ollama-bot/src/ollama/ollama.module.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { OllamaService } from './ollama.service';
|
||||
|
||||
@Module({
|
||||
providers: [OllamaService],
|
||||
exports: [OllamaService],
|
||||
})
|
||||
export class OllamaModule {}
|
||||
138
services/telegram-ollama-bot/src/ollama/ollama.service.ts
Normal file
138
services/telegram-ollama-bot/src/ollama/ollama.service.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface OllamaGenerateResponse {
|
||||
model: string;
|
||||
response: string;
|
||||
done: boolean;
|
||||
total_duration?: number;
|
||||
eval_count?: number;
|
||||
eval_duration?: number;
|
||||
}
|
||||
|
||||
interface OllamaModel {
|
||||
name: string;
|
||||
size: number;
|
||||
modified_at: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OllamaService implements OnModuleInit {
|
||||
private readonly logger = new Logger(OllamaService.name);
|
||||
private readonly baseUrl: string;
|
||||
private readonly defaultModel: string;
|
||||
private readonly timeout: number;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.get<string>('ollama.url') || 'http://localhost:11434';
|
||||
this.defaultModel = this.configService.get<string>('ollama.model') || 'gemma3:4b';
|
||||
this.timeout = this.configService.get<number>('ollama.timeout') || 120000;
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.checkConnection();
|
||||
}
|
||||
|
||||
async checkConnection(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/version`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
const data = await response.json();
|
||||
this.logger.log(`Ollama connected: v${data.version}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to connect to Ollama at ${this.baseUrl}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async listModels(): Promise<OllamaModel[]> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
const data = await response.json();
|
||||
return data.models || [];
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to list models:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async generate(prompt: string, systemPrompt?: string, model?: string): Promise<string> {
|
||||
const selectedModel = model || this.defaultModel;
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
model: selectedModel,
|
||||
prompt,
|
||||
stream: false,
|
||||
};
|
||||
|
||||
if (systemPrompt) {
|
||||
body.system = systemPrompt;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: OllamaGenerateResponse = await response.json();
|
||||
|
||||
// Log performance metrics
|
||||
if (data.eval_count && data.eval_duration) {
|
||||
const tokensPerSec = (data.eval_count / data.eval_duration) * 1e9;
|
||||
this.logger.debug(`Generated ${data.eval_count} tokens at ${tokensPerSec.toFixed(1)} t/s`);
|
||||
}
|
||||
|
||||
return data.response;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
throw new Error('Ollama Timeout - Antwort dauerte zu lange');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async chat(
|
||||
messages: { role: 'user' | 'assistant' | 'system'; content: string }[],
|
||||
model?: string
|
||||
): Promise<string> {
|
||||
const selectedModel = model || this.defaultModel;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: selectedModel,
|
||||
messages,
|
||||
stream: false,
|
||||
}),
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.message?.content || '';
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'TimeoutError') {
|
||||
throw new Error('Ollama Timeout - Antwort dauerte zu lange');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultModel(): string {
|
||||
return this.defaultModel;
|
||||
}
|
||||
}
|
||||
22
services/telegram-ollama-bot/tsconfig.json
Normal file
22
services/telegram-ollama-bot/tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue