Skip to content

Commit 8753aa3

Browse files
jangel97claude
andauthored
feat: add AI assistant chatbot (#18)
* feat: add AI assistant chatbot module and microservice Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: add chatbot image build, deploy workflow and manifests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7594a0a commit 8753aa3

37 files changed

Lines changed: 3279 additions & 1 deletion
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
name: Build & Push Chatbot Image
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
paths:
8+
- 'services/chatbot/**'
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: build-chatbot-${{ github.ref_name }}
15+
cancel-in-progress: true
16+
17+
env:
18+
CHATBOT_IMAGE: quay.io/org-pulse/org-pulse-chatbot
19+
20+
jobs:
21+
build-chatbot:
22+
name: Build Chatbot
23+
runs-on: ubuntu-latest
24+
env:
25+
BUILD_SHA: ${{ github.sha }}
26+
steps:
27+
- uses: actions/checkout@v7
28+
29+
- name: Get version from package.json
30+
id: version
31+
run: echo "version=v$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
32+
33+
- name: Build chatbot image
34+
run: |
35+
docker build \
36+
-f services/chatbot/Containerfile \
37+
--build-arg GIT_SHA=${BUILD_SHA} \
38+
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
39+
-t ${{ env.CHATBOT_IMAGE }}:${BUILD_SHA} \
40+
services/chatbot
41+
42+
- name: Smoke test chatbot
43+
run: |
44+
docker run -d --name chatbot-smoke -p 8002:8002 \
45+
-e CHATBOT_LLM_BASE_URL=http://localhost:9999/v1 \
46+
-e CHATBOT_LLM_API_KEY=test \
47+
${{ env.CHATBOT_IMAGE }}:${BUILD_SHA}
48+
echo "Waiting for chatbot to start..."
49+
sleep 5
50+
if curl -sf --max-time 5 http://localhost:8002/health > /dev/null 2>&1; then
51+
echo "Chatbot smoke test passed"
52+
else
53+
echo "Chatbot smoke test failed"
54+
docker logs chatbot-smoke
55+
docker stop chatbot-smoke
56+
exit 1
57+
fi
58+
docker stop chatbot-smoke
59+
60+
- name: Login to Quay.io
61+
if: github.ref == 'refs/heads/main'
62+
uses: docker/login-action@v4
63+
with:
64+
registry: quay.io
65+
username: ${{ secrets.QUAY_USERNAME }}
66+
password: ${{ secrets.QUAY_PASSWORD }}
67+
68+
- name: Push chatbot image
69+
if: github.ref == 'refs/heads/main'
70+
env:
71+
VERSION: ${{ steps.version.outputs.version }}
72+
run: |
73+
docker tag ${{ env.CHATBOT_IMAGE }}:${BUILD_SHA} ${{ env.CHATBOT_IMAGE }}:latest
74+
docker push ${{ env.CHATBOT_IMAGE }}:${BUILD_SHA}
75+
docker push ${{ env.CHATBOT_IMAGE }}:latest
76+
if [ -n "${VERSION}" ]; then
77+
docker tag ${{ env.CHATBOT_IMAGE }}:${BUILD_SHA} ${{ env.CHATBOT_IMAGE }}:${VERSION}
78+
docker push ${{ env.CHATBOT_IMAGE }}:${VERSION}
79+
fi
80+
81+
update-deploy-tag:
82+
name: Update Deploy Image Tag
83+
needs: [build-chatbot]
84+
if: github.ref == 'refs/heads/main'
85+
runs-on: ubuntu-latest
86+
permissions:
87+
contents: write
88+
env:
89+
BUILD_SHA: ${{ github.sha }}
90+
steps:
91+
- name: Generate GitHub App token
92+
id: app-token
93+
uses: actions/create-github-app-token@v3
94+
with:
95+
app-id: ${{ secrets.APP_ID }}
96+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
97+
98+
- uses: actions/checkout@v7
99+
with:
100+
ref: main
101+
token: ${{ steps.app-token.outputs.token }}
102+
103+
- name: Install kustomize
104+
uses: imranismail/setup-kustomize@v3
105+
with:
106+
kustomize-version: '5.8.1'
107+
108+
- name: Update chatbot image tag
109+
working-directory: deploy/openshift/base
110+
run: kustomize edit set image ${{ env.CHATBOT_IMAGE }}:${BUILD_SHA}
111+
112+
- name: Validate kustomize build
113+
run: kustomize build deploy/openshift/base > /dev/null
114+
115+
- name: Push deploy commit
116+
run: |
117+
git config user.name "github-actions[bot]"
118+
git config user.email "github-actions[bot]@users.noreply.github.com"
119+
git add deploy/openshift/base/kustomization.yaml
120+
git diff --cached --quiet && { echo "No changes to commit"; exit 0; }
121+
git commit -m "deploy: update chatbot image (${BUILD_SHA}) [skip ci]"
122+
git push origin main

CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ server/
8282
modules/
8383
team-tracker/ # Core module (roster, metrics, contributions, sprints)
8484
85+
services/
86+
chatbot/ # AI assistant microservice (Python/FastAPI, deployed separately)
87+
8588
deploy/
8689
core.backend.Dockerfile # Core backend image
8790
core.frontend.Dockerfile # Core frontend image (complete)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ make test-module MODULE=people-teams # Run integration tests
100100
- **Auth**: OpenShift OAuth proxy (production), no auth (local dev)
101101
- **Storage**: Local filesystem (`./data/`), PVC in OpenShift
102102
- **Hosting**: OpenShift with ArgoCD
103+
- **AI Assistant**: Python/FastAPI microservice (`services/chatbot/`), deployed as a separate pod
103104
- **Testing**: Vitest (unit), Playwright (smoke & integration)
104105

105106
## Deployment

deploy/OPENSHIFT.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ oc create secret generic google-sa-key \
5959
-n team-tracker \
6060
--from-file=google-sa-key.json=./secrets/google-sa-key.json
6161

62+
# Optional: Chatbot AI assistant secrets (for LLM inference + content gate)
63+
# The chatbot pod starts without these (all refs are optional), but cannot
64+
# process chat requests until the LLM credentials are set.
65+
oc create secret generic chatbot-secrets \
66+
-n team-tracker \
67+
--from-literal=CHATBOT_LLM_BASE_URL=<llm-endpoint-url> \
68+
--from-literal=CHATBOT_LLM_API_KEY=<llm-api-key> \
69+
--from-literal=CHATBOT_GATE_BASE_URL=<gate-endpoint-url> \
70+
--from-literal=CHATBOT_GATE_API_KEY=<gate-api-key> \
71+
--from-literal=CHATBOT_EMBEDDING_BASE_URL=<embedding-endpoint-url> \
72+
--from-literal=CHATBOT_EMBEDDING_API_KEY=<embedding-api-key>
73+
6274
# Optional: SmartSheet API token (for releases module -- release discovery)
6375
# Generate a token at: My Account > Personal Settings > API Access > Generate New Access Token
6476
oc patch secret team-tracker-secrets \
@@ -205,6 +217,9 @@ oc logs deployment/frontend -n team-tracker -c nginx -f
205217

206218
# OAuth proxy
207219
oc logs deployment/frontend -n team-tracker -c oauth-proxy -f
220+
221+
# Chatbot (structured JSON logs)
222+
oc logs deployment/chatbot -n team-tracker -f
208223
```
209224

210225
## Troubleshooting
@@ -263,8 +278,18 @@ oc rollout restart deployment/backend -n team-tracker
263278
│ ┌──────────────────────────────────────────┐ │
264279
│ │ Express :3001 │ │
265280
│ │ Jira API, GitHub API, Roster Sync, LDAP │ │
281+
│ │ Proxies /api/modules/chatbot → chatbot │ │
266282
│ └──────────────────────────────────────────┘ │
267283
│ Volume: /app/data (PVC: team-tracker-data) │
284+
└──────────────────┬──────────────────────────────┘
285+
│ /api/modules/chatbot
286+
┌──────────────────▼──────────────────────────────┐
287+
│ Chatbot Pod │
288+
│ ┌──────────────────────────────────────────┐ │
289+
│ │ FastAPI :8002 │ │
290+
│ │ AI assistant (LLM inference + tools) │ │
291+
│ │ Calls back to backend :3001 internally │ │
292+
│ └──────────────────────────────────────────┘ │
268293
└─────────────────────────────────────────────────┘
269294
```
270295

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: chatbot
5+
labels:
6+
app: team-tracker
7+
component: chatbot
8+
spec:
9+
replicas: 1
10+
strategy:
11+
type: Recreate
12+
selector:
13+
matchLabels:
14+
app: team-tracker
15+
component: chatbot
16+
template:
17+
metadata:
18+
labels:
19+
app: team-tracker
20+
component: chatbot
21+
spec:
22+
securityContext:
23+
runAsNonRoot: true
24+
containers:
25+
- name: chatbot
26+
securityContext:
27+
allowPrivilegeEscalation: false
28+
readOnlyRootFilesystem: true
29+
image: quay.io/org-pulse/org-pulse-chatbot:latest
30+
ports:
31+
- containerPort: 8002
32+
protocol: TCP
33+
envFrom:
34+
- configMapRef:
35+
name: chatbot-config
36+
env:
37+
- name: CHATBOT_LLM_BASE_URL
38+
valueFrom:
39+
secretKeyRef:
40+
name: chatbot-secrets
41+
key: CHATBOT_LLM_BASE_URL
42+
optional: true
43+
- name: CHATBOT_LLM_API_KEY
44+
valueFrom:
45+
secretKeyRef:
46+
name: chatbot-secrets
47+
key: CHATBOT_LLM_API_KEY
48+
optional: true
49+
- name: CHATBOT_GATE_BASE_URL
50+
valueFrom:
51+
secretKeyRef:
52+
name: chatbot-secrets
53+
key: CHATBOT_GATE_BASE_URL
54+
optional: true
55+
- name: CHATBOT_GATE_API_KEY
56+
valueFrom:
57+
secretKeyRef:
58+
name: chatbot-secrets
59+
key: CHATBOT_GATE_API_KEY
60+
optional: true
61+
- name: CHATBOT_EMBEDDING_BASE_URL
62+
valueFrom:
63+
secretKeyRef:
64+
name: chatbot-secrets
65+
key: CHATBOT_EMBEDDING_BASE_URL
66+
optional: true
67+
- name: CHATBOT_EMBEDDING_API_KEY
68+
valueFrom:
69+
secretKeyRef:
70+
name: chatbot-secrets
71+
key: CHATBOT_EMBEDDING_API_KEY
72+
optional: true
73+
readinessProbe:
74+
httpGet:
75+
path: /health
76+
port: 8002
77+
initialDelaySeconds: 10
78+
periodSeconds: 10
79+
livenessProbe:
80+
httpGet:
81+
path: /health
82+
port: 8002
83+
initialDelaySeconds: 30
84+
periodSeconds: 30
85+
volumeMounts:
86+
- name: tmp
87+
mountPath: /tmp
88+
resources:
89+
requests:
90+
memory: "256Mi"
91+
cpu: "100m"
92+
limits:
93+
memory: "1Gi"
94+
cpu: "500m"
95+
volumes:
96+
- name: tmp
97+
emptyDir: {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: chatbot
5+
labels:
6+
app: team-tracker
7+
component: chatbot
8+
spec:
9+
selector:
10+
app: team-tracker
11+
component: chatbot
12+
ports:
13+
- port: 8002
14+
targetPort: 8002
15+
protocol: TCP

deploy/openshift/base/kustomization.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ resources:
1111
- api-route.yaml
1212
- serviceaccount.yaml
1313
- cronjob-sync-refresh.yaml
14+
- chatbot-deployment.yaml
15+
- chatbot-service.yaml
1416

1517
configMapGenerator:
1618
- literals:
@@ -19,10 +21,15 @@ configMapGenerator:
1921
- JIRA_HOST=https://redhat.atlassian.net
2022
- ADMIN_EMAILS=
2123
- CRON_ADMIN_EMAIL=
24+
- CHATBOT_SERVICE_URL=http://chatbot:8002
2225
name: team-tracker-config
2326
- files:
2427
- app.conf.template=nginx-app.conf.template
2528
name: nginx-config-template
29+
- literals:
30+
- CHATBOT_PORT=8002
31+
- ORG_PULSE_API_URL=http://backend:3001
32+
name: chatbot-config
2633

2734
images:
2835
- name: quay.io/org-pulse/org-pulse-core-backend

docs/MODULES.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,22 @@ export const routes = {
116116

117117
Use `defineAsyncComponent` for lazy loading.
118118

119+
### Floating Widgets
120+
121+
A module can export a `floatingWidget` component that renders as a floating overlay in the app shell, independent of sidebar navigation. This is useful for always-available features like chat assistants.
122+
123+
```javascript
124+
import { defineAsyncComponent } from 'vue'
125+
126+
export const routes = {
127+
'chat': defineAsyncComponent(() => import('./views/ChatView.vue')),
128+
}
129+
130+
export const floatingWidget = defineAsyncComponent(() => import('./views/ChatView.vue'))
131+
```
132+
133+
When `floatingWidget` is exported, the shell renders it as a persistent floating component. The module can omit `navItems` from `module.json` if it doesn't need sidebar navigation — the floating widget will still load when the module is enabled.
134+
119135
## Navigation API
120136

121137
The shell provides a `moduleNav` object via Vue's `provide/inject`:

0 commit comments

Comments
 (0)