Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions infra/docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ services:
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--providers.docker.network=proxy"
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
ports: !override
- "80:80"
Expand All @@ -36,7 +38,7 @@ services:
- "traefik.http.routers.py-genai-helper.entrypoints=web"
- "traefik.http.routers.py-genai-helper.rule=PathPrefix(`/api/v1/helper`)"
- "traefik.http.middlewares.helper-stripprefix.stripprefix.prefixes=/api/v1/helper"
- "traefik.http.routers.py-genai-helper.middlewares=helper-stripprefix"
- "traefik.http.routers.py-genai-helper.middlewares=helper-stripprefix,forward-auth@file"
- "traefik.http.services.py-genai-helper.loadbalancer.server.port=5000"

organization-service:
Expand All @@ -47,7 +49,7 @@ services:
- "traefik.http.routers.organization-service.entrypoints=web"
- "traefik.http.routers.organization-service.rule=PathPrefix(`/api/v1/organization`)"
- "traefik.http.middlewares.organization-stripprefix.stripprefix.prefixes=/api/v1/organization"
- "traefik.http.routers.organization-service.middlewares=organization-stripprefix"
- "traefik.http.routers.organization-service.middlewares=organization-stripprefix,forward-auth@file"
- "traefik.http.services.organization-service.loadbalancer.server.port=8080"

member-service:
Expand All @@ -58,7 +60,7 @@ services:
- "traefik.http.routers.member-service.entrypoints=web"
- "traefik.http.routers.member-service.rule=PathPrefix(`/api/v1/members`)"
- "traefik.http.middlewares.member-stripprefix.stripprefix.prefixes=/api/v1/members"
- "traefik.http.routers.member-service.middlewares=member-stripprefix"
- "traefik.http.routers.member-service.middlewares=member-stripprefix,forward-auth@file"
- "traefik.http.services.member-service.loadbalancer.server.port=8080"

event-service:
Expand All @@ -69,7 +71,7 @@ services:
- "traefik.http.routers.event-service.entrypoints=web"
- "traefik.http.routers.event-service.rule=PathPrefix(`/api/v1/events`)"
- "traefik.http.middlewares.event-stripprefix.stripprefix.prefixes=/api/v1/events"
- "traefik.http.routers.event-service.middlewares=event-stripprefix"
- "traefik.http.routers.event-service.middlewares=event-stripprefix,forward-auth@file"
- "traefik.http.services.event-service.loadbalancer.server.port=8080"

feedback-service:
Expand All @@ -80,7 +82,7 @@ services:
- "traefik.http.routers.feedback-service.entrypoints=web"
- "traefik.http.routers.feedback-service.rule=PathPrefix(`/api/v1/feedback`)"
- "traefik.http.middlewares.feedback-stripprefix.stripprefix.prefixes=/api/v1/feedback"
- "traefik.http.routers.feedback-service.middlewares=feedback-stripprefix"
- "traefik.http.routers.feedback-service.middlewares=feedback-stripprefix,forward-auth@file"
- "traefik.http.services.feedback-service.loadbalancer.server.port=8080"

finance-service:
Expand All @@ -91,7 +93,7 @@ services:
- "traefik.http.routers.finance-service.entrypoints=web"
- "traefik.http.routers.finance-service.rule=PathPrefix(`/api/v1/finance`)"
- "traefik.http.middlewares.finance-stripprefix.stripprefix.prefixes=/api/v1/finance"
- "traefik.http.routers.finance-service.middlewares=finance-stripprefix"
- "traefik.http.routers.finance-service.middlewares=finance-stripprefix,forward-auth@file"
- "traefik.http.services.finance-service.loadbalancer.server.port=8080"

letter-service:
Expand All @@ -102,7 +104,7 @@ services:
- "traefik.http.routers.letter-service.entrypoints=web"
- "traefik.http.routers.letter-service.rule=PathPrefix(`/api/v1/letters`)"
- "traefik.http.middlewares.letter-stripprefix.stripprefix.prefixes=/api/v1/letters"
- "traefik.http.routers.letter-service.middlewares=letter-stripprefix"
- "traefik.http.routers.letter-service.middlewares=letter-stripprefix,forward-auth@file"
- "traefik.http.services.letter-service.loadbalancer.server.port=8080"

api-docs:
Expand All @@ -120,13 +122,21 @@ services:
- "traefik.enable=true"
- "traefik.http.routers.web-client.entrypoints=web"
- "traefik.http.routers.web-client.rule=PathPrefix(`/`)"
- "traefik.http.routers.web-client.middlewares=forward-auth@file"
- "traefik.http.services.web-client.loadbalancer.server.port=8080"

keycloak:
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=web"
- "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
environment:
KC_HOSTNAME: "http://localhost:8081/auth"

traefik-forward-auth:
extra_hosts:
- "localhost:host-gateway"
labels: !override
- "traefik.enable=false"
environment:
Expand Down
13 changes: 13 additions & 0 deletions infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -365,10 +365,23 @@ services:
networks:
- data

ollama:
image: ollama/ollama:latest
container_name: ollama
expose:
- 11434
volumes:
- ollama_data:/root/.ollama
- ./ollama/entrypoint_ollama.sh:/entrypoint_ollama.sh
networks:
- proxy
entrypoint: ["/usr/bin/bash", "/entrypoint_ollama.sh"]

volumes:
member_db_data:
keycloak_db_data:
letsencrypt:
ollama_data:

networks:
proxy:
Expand Down
1 change: 1 addition & 0 deletions infra/helm/team-devoops/files/realm-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"webOrigins": [
"https://team-devoops.uaenorth.cloudapp.azure.com",
"https://ge83mom-devops26.stud.k8s.aet.cit.tum.de",
"https://ge83mom-devops26.stud.k8s.aet.cit.tum.de",
"http://localhost"
]
}
Expand Down
33 changes: 33 additions & 0 deletions infra/helm/team-devoops/templates/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
{{- $host := .Values.ingress.host }}
{{- $tls := .Values.ingress.tls }}
{{- $fa := .Values.forwardAuth }}
{{- $fa := .Values.forwardAuth }}
# ---------------------------------------------------------------------------
# Stripped ingress: services whose path prefix must be removed before the
# request reaches the backend (Traefik stripPrefix parity). Uses a regex
# capture group so `/api/v1/members/foo` -> `/foo`.
# Auth-protected when forwardAuth is enabled.
# Auth-protected when forwardAuth is enabled.
# ---------------------------------------------------------------------------
apiVersion: networking.k8s.io/v1
kind: Ingress
Expand Down Expand Up @@ -53,6 +55,7 @@ spec:
# ---------------------------------------------------------------------------
# Plain ingress: services served at their path as-is (web-client, api-docs).
# Auth-protected when forwardAuth is enabled.
# Auth-protected when forwardAuth is enabled.
# ---------------------------------------------------------------------------
apiVersion: networking.k8s.io/v1
kind: Ingress
Expand All @@ -61,6 +64,7 @@ metadata:
labels:
{{- include "team-devoops.labels" (dict "name" "ingress-plain" "root" $) | nindent 4 }}
annotations:
{{- if and $tls.enabled $tls.clusterIssuer }}
{{- if and $tls.enabled $tls.clusterIssuer }}
cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }}
{{- end }}
Expand Down Expand Up @@ -100,6 +104,35 @@ spec:
# ---------------------------------------------------------------------------
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: team-devoops-open
labels:
{{- include "team-devoops.labels" (dict "name" "ingress-open" "root" $) | nindent 4 }}
{{- if and $tls.enabled $tls.clusterIssuer }}
annotations:
cert-manager.io/cluster-issuer: {{ $tls.clusterIssuer | quote }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if $tls.enabled }}
tls:
- hosts:
- {{ $host | quote }}
{{- if $tls.secretName }}
secretName: {{ $tls.secretName }}
{{- end }}
{{- end }}
rules:
- host: {{ $host | quote }}
http:
paths:
---
# ---------------------------------------------------------------------------
# Open ingress: Keycloak (auth provider) and the forward-auth OAuth callback
# must never be behind forward-auth to avoid redirect loops.
# ---------------------------------------------------------------------------
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: team-devoops-open
labels:
Expand Down
1 change: 1 addition & 0 deletions infra/keycloak/realm-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"webOrigins": [
"https://team-devoops.uaenorth.cloudapp.azure.com",
"https://ge83mom-devops26.stud.k8s.aet.cit.tum.de",
"https://ge83mom-devops26.stud.k8s.aet.cit.tum.de",
"http://localhost"
]
}
Expand Down
17 changes: 17 additions & 0 deletions infra/ollama/entrypoint_ollama.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash

# Start Ollama in the background.
/bin/ollama serve &
# Record Process ID.
pid=$!

# Pause for Ollama to start.
sleep 5

echo "Retrieve model..."
ollama pull qwen3:8b
ollama pull nomic-embed-text
echo "Done!"

# Wait for Ollama process to finish.
wait $pid
2 changes: 1 addition & 1 deletion services/py-genai-helper/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ EXPOSE 5000
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s CMD wget -qO- http://127.0.0.1:5000/health || exit 1

# Run app
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "300", "app:app"]
12 changes: 12 additions & 0 deletions services/py-genai-helper/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,15 @@ def rag_response():

response = generate_rag_response(question)
return {"response": response}, 200

@app.route("/rag-response-local", methods=["POST"])
def rag_response_local():
# Get the json of the object. force=True ignores the stated MimeType
data = request.get_json(force=True) or {}
question = data.get("question")

if not question:
return {"error": "Missing required field: 'question'"}, 400

response = generate_rag_response(question, local=True)
return {"response": response}, 200
20 changes: 14 additions & 6 deletions services/py-genai-helper/rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@
from langchain_community.document_loaders import PyPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_core.tools import create_retriever_tool
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

load_dotenv()

embeddings = OpenAIEmbeddings(model="text-embedding-3-large")

_FILE_STORAGE = Path(__file__).parent / "file-storage"


def _load_pdfs() -> FAISS | None:
def _load_pdfs(embeddings) -> FAISS | None:
pdf_files = list(_FILE_STORAGE.glob("*.pdf"))
if not pdf_files:
return None
Expand All @@ -29,10 +28,19 @@ def _load_pdfs() -> FAISS | None:
return FAISS.from_documents(docs, embedding=embeddings)


vector_store = _load_pdfs()
_local_vector_store = _load_pdfs(OllamaEmbeddings(model="nomic-embed-text", base_url="http://ollama:11434"))
_remote_vector_store = _load_pdfs(OpenAIEmbeddings(model="text-embedding-3-large"))


def get_rag_agent(local: bool):
global _local_vector_store, _remote_vector_store
if local:
vector_store = _local_vector_store
model = ChatOllama(model="qwen3:8b", base_url="http://ollama:11434", think=False)
else:
vector_store = _remote_vector_store
model = "gpt-4.1-mini"

def get_rag_agent():
if vector_store is None:
raise RuntimeError("No PDFs found in file-storage/")

Expand All @@ -45,7 +53,7 @@ def get_rag_agent():
)

rag_agent = create_agent(
model="gpt-4.1-mini",
model=model,
tools=[retriever_tool],
system_prompt=(
"You are a helpful assistant."
Expand Down
15 changes: 11 additions & 4 deletions services/py-genai-helper/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@
from rag import get_rag_agent

load_dotenv()
agent = create_agent("gpt-4.1-mini")
_agent = None


def _get_agent():
global _agent
if _agent is None:
_agent = create_agent("gpt-4.1-mini")
return _agent


def hello():
response = agent.invoke(
response = _get_agent().invoke(
{
"messages": [
SystemMessage(
Expand All @@ -22,7 +29,7 @@ def hello():
return response["messages"][-1].content


def generate_rag_response(question):
rag_agent = get_rag_agent()
def generate_rag_response(question, local=False):
rag_agent = get_rag_agent(local)
response = rag_agent.invoke({"messages": [{"role": "user", "content": question}]})
return response["messages"][-1].content
10 changes: 8 additions & 2 deletions web-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
"dependencies": {
"@fontsource/bebas-neue": "^5.2.7",
"@fontsource/poppins": "^5.2.7",
"@hookform/resolvers": "^5.4.0",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-query": "^5",
"@tanstack/react-query-devtools": "^5.101.0",
"axios": "^1.16.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -32,13 +35,16 @@
"radix-ui": "^1.4.3",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-hook-form": "^7.77.0",
"react-router-dom": "^7.15.1",
"rolldown": "^1.0.2",
"rollup": "^4.60.4",
"tslib": "^2.8.1",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0"
"tslib": "^2.8.1",
"tw-animate-css": "^1.4.0",
"zod": "^4.4.3",
"zustand": "^5.0.14"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
Loading
Loading