diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4aed35b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +data/arangodb/ +data/mariadb/ diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..bd171e5 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +CMD ["python", "app.py"] diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000..24e3008 --- /dev/null +++ b/api/app.py @@ -0,0 +1,70 @@ +from flask import Flask, request, jsonify +import mysql.connector +import requests +import os +from arango import ArangoClient + +app = Flask(__name__) + +# MariaDB Verbindung +def get_db(): + return mysql.connector.connect( + host=os.environ['DB_HOST'], + user=os.environ['DB_USER'], + password=os.environ['DB_PASSWORD'], + database=os.environ['DB_NAME'] + ) + +# ArangoDB Verbindung +def get_arango(): + client = ArangoClient(hosts=os.environ['ARANGO_URL']) + sys_db = client.db('_system', username=os.environ['ARANGO_USER'], password=os.environ['ARANGO_PASSWORD']) + if not sys_db.has_database('llm_facts'): + sys_db.create_database('llm_facts') + db = client.db('llm_facts', username=os.environ['ARANGO_USER'], password=os.environ['ARANGO_PASSWORD']) + if not db.has_collection('facts'): + db.create_collection('facts') + return db.collection('facts') + +# Skill-Trigger +def extract_fact(text): + triggers = ["merke dir", "notiere", "speichere"] + for t in triggers: + if text.lower().startswith(t): + fact = text[len(t):].strip() + return fact + return None + +@app.route("/chat", methods=["POST"]) +def chat(): + data = request.json + project = data.get("project") + message = data.get("message") + + # Lernbefehl + fact = extract_fact(message) + if fact: + arango = get_arango() + arango.insert({ + "_raw": message, + "_source": project, + "content": fact, + "tags": ["skill:merke"], + "relations": [] + }) + return jsonify({"reply": f"Fakt gespeichert: {fact}"}) + + # Chat speichern in MariaDB + db = get_db() + cursor = db.cursor() + cursor.execute("INSERT INTO chats (project, user_input) VALUES (%s,%s)", (project, message)) + db.commit() + cursor.close() + db.close() + + # Anfrage an LLM + resp = requests.post(f"{os.environ['LM_API_URL']}/generate", json={"prompt": message}) + return jsonify({"reply": resp.json()}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/api/requirements.txt b/api/requirements.txt new file mode 100644 index 0000000..ccd4a16 --- /dev/null +++ b/api/requirements.txt @@ -0,0 +1,4 @@ +flask +mysql-connector-python +requests +python-arango diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c905c84 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,96 @@ +services: + myki-mariadb: + image: mariadb:11 + container_name: myki-mariadb + restart: always + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: llm_projects + MYSQL_USER: llmuser + MYSQL_PASSWORD: llmpassword + ports: + - "3306:3306" + volumes: + - ./data/mariadb:/var/lib/mysql + - ./initdb:/docker-entrypoint-initdb.d # SQL-Dateien werden hier automatisch beim ersten Start ausgeführt + + myki-phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: myki-phpmyadmin + restart: always + environment: + PMA_HOST: mariadb + PMA_USER: llmuser + PMA_PASSWORD: llmpassword + ports: + - "8081:80" + depends_on: + - myki-mariadb + + myki-arangodb: + image: arangodb:3.12 + container_name: myki-arangodb + restart: always + environment: + ARANGO_ROOT_PASSWORD: rootpassword + ports: + - "8529:8529" + volumes: + - ./data/arangodb:/var/lib/arangodb3 + - ./initdb-arango:/docker-entrypoint-initdb.d # Initialisierung + +# myki-lmstudio: +# image: ghcr.io/nomic-ai/ministral-3-14b-instruct-2512 +# container_name: myki-lmstudio +# environment: +# LMSTUDIO_API: "1" +# ports: +# - "8080:8080" +# volumes: +# - ./lmstudio/models:/models + + myki-api: + build: ./api + container_name: myki-api + depends_on: + - myki-mariadb + - myki-arangodb + #- myki-lmstudio + environment: + DB_HOST: mariadb + DB_USER: llmuser + DB_PASSWORD: llmpassword + DB_NAME: llm_projects + #LM_API_URL: http://lmstudio:8080 + LM_API_URL: http://host.docker.internal:1234 + ARANGO_URL: http://arangodb:8529 + ARANGO_USER: root + ARANGO_PASSWORD: rootpassword + ports: + - "5000:5000" + + myki-worker: + build: ./worker + container_name: myki-worker + depends_on: + - myki-api + - myki-mariadb + - myki-arangodb + #- lmstudio + environment: + DB_HOST: mariadb + DB_USER: llmuser + DB_PASSWORD: llmpassword + DB_NAME: llm_projects + LM_API_URL: http://lmstudio:8080 + ARANGO_URL: http://arangodb:8529 + ARANGO_USER: root + ARANGO_PASSWORD: rootpassword + + web: + build: ./web + container_name: llm_web + depends_on: + - myki-api + ports: + - "3001:3000" diff --git a/initdb/init.sql b/initdb/init.sql new file mode 100644 index 0000000..883ce0d --- /dev/null +++ b/initdb/init.sql @@ -0,0 +1,15 @@ +CREATE TABLE chats ( + id INT AUTO_INCREMENT PRIMARY KEY, + project VARCHAR(255), + user_input TEXT, + llm_output TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE feedback ( + id INT AUTO_INCREMENT PRIMARY KEY, + chat_id INT, + rating ENUM('positive','negative'), + comment TEXT, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 0000000..6e8acef --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,2 @@ +FROM nginx:alpine +COPY . /usr/share/nginx/html diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..1840a44 --- /dev/null +++ b/web/index.html @@ -0,0 +1,17 @@ + + +
+ +Du: ${message}
LLM: ${data.reply}
`; +} diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..c859149 --- /dev/null +++ b/web/style.css @@ -0,0 +1,4 @@ +#chat-container { width: 500px; margin: auto; } +#messages { border: 1px solid #ccc; height: 400px; overflow-y: scroll; padding: 5px; margin-bottom: 5px; } +input { width: 80%; margin-bottom: 5px; } +button { width: 18%; } diff --git a/worker/Dockerfile b/worker/Dockerfile new file mode 100644 index 0000000..7bf1623 --- /dev/null +++ b/worker/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY worker.py . +CMD ["python", "worker.py"] diff --git a/worker/requirements.txt b/worker/requirements.txt new file mode 100644 index 0000000..fa0b1ea --- /dev/null +++ b/worker/requirements.txt @@ -0,0 +1,4 @@ +mysql-connector-python +requests +python-arango +schedule diff --git a/worker/worker.py b/worker/worker.py new file mode 100644 index 0000000..1bc27e0 --- /dev/null +++ b/worker/worker.py @@ -0,0 +1,58 @@ +import os +import mysql.connector +import requests +from arango import ArangoClient +import schedule +import time + +# MariaDB +def get_db(): + return mysql.connector.connect( + host=os.environ['DB_HOST'], + user=os.environ['DB_USER'], + password=os.environ['DB_PASSWORD'], + database=os.environ['DB_NAME'] + ) + +# ArangoDB +def get_arango(): + client = ArangoClient(hosts=os.environ['ARANGO_URL']) + db = client.db('llm_facts', username=os.environ['ARANGO_USER'], password=os.environ['ARANGO_PASSWORD']) + return db.collection('facts') + +# Analysiere neue Chats und überführe sie in ArangoDB +def analyze_chats(): + db = get_db() + cursor = db.cursor(dictionary=True) + cursor.execute("SELECT * FROM chats WHERE llm_output IS NULL") + chats = cursor.fetchall() + + arango = get_arango() + + for chat in chats: + # Anfrage an LLM zur Analyse / Fakt-Extraktion + resp = requests.post(f"{os.environ['LM_API_URL']}/generate", json={"prompt": f"Extrahiere Fakten und Kontext aus diesem Text:\n{chat['user_input']}"}) + fact = resp.json() + + # Speichern in ArangoDB + arango.insert({ + "_raw": chat['user_input'], + "_source": chat['project'], + "content": fact, + "tags": ["auto"], + "relations": [] + }) + + # Update llm_output in MariaDB + cursor.execute("UPDATE chats SET llm_output=%s WHERE id=%s", (fact, chat['id'])) + + db.commit() + cursor.close() + db.close() + +# Alle 30 Sekunden ausführen +schedule.every(30).seconds.do(analyze_chats) + +while True: + schedule.run_pending() + time.sleep(1)