diff --git a/.github/workflows/studios-CICD-pipeline.yaml b/.github/workflows/studios-CICD-pipeline.yaml new file mode 100644 index 00000000000..6795998a8d5 --- /dev/null +++ b/.github/workflows/studios-CICD-pipeline.yaml @@ -0,0 +1,45 @@ +name: Studios Agent Hub Build and Deploy +on: + push: + branches: + - main + - AH-125_workflows + workflow_dispatch: + +concurrency: + group: studios-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + frontend: + uses: ./.github/workflows/studios-frontend.yaml + backend: + uses: ./.github/workflows/studios-backend.yaml + + build: + needs: [frontend, backend] + uses: ./.github/workflows/studios-build.yml + with: + productEcrRegistry: 590183674594.dkr.ecr.eu-west-1.amazonaws.com + awsRole: arn:aws:iam::590183674594:role/dev-ai-agent-hub-service-gha + serviceName: studios-agent-hub + publish: ${{ github.ref == 'refs/heads/main' }} + runsOn: 'dev-studios-agent-hub' + stg_deploy: + needs: build + uses: ./.github/workflows/studios-deploy.yml + with: + imageTag: ${{ needs.build.outputs.image_tag || 'latest' }} + ecosystem: dev + gitRef: ${{ github.ref }} + secrets: inherit + + prd_deploy: + needs: [build, stg_deploy] + uses: ./.github/workflows/studios-deploy.yml + with: + imageTag: ${{ needs.build.outputs.image_tag || 'latest' }} + ecosystem: prd + prdPromoteImage: true + gitRef: ${{ github.ref }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/studios-auto-tag-release.yml b/.github/workflows/studios-auto-tag-release.yml new file mode 100644 index 00000000000..e100d4b31f9 --- /dev/null +++ b/.github/workflows/studios-auto-tag-release.yml @@ -0,0 +1,45 @@ +name: Auto Tag Merged Sync PR + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + tag-release: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check Merge Commit & Create Tag + run: | + # Get the last commit message on main + COMMIT_MSG=$(git log -1 --pretty=%B) + + # Regex check if the commit came from a sync/vX.Y.Z branch merge + if [[ "$COMMIT_MSG" =~ sync/(v[0-9]+\.[0-9]+\.[0-9]+) ]]; then + UPSTREAM_VERSION="${BASH_REMATCH[1]}" + NEW_TAG="itv-$UPSTREAM_VERSION" + + echo "Detected merge for version $UPSTREAM_VERSION." + + # Check if tag already exists + if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then + echo "Tag $NEW_TAG already exists. Skipping." + else + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + echo "Creating tag $NEW_TAG..." + git tag "$NEW_TAG" + git push origin "$NEW_TAG" + fi + else + echo "Commit is not a sync branch merge. Skipping tag creation." + fi \ No newline at end of file diff --git a/.github/workflows/studios-backend.yaml b/.github/workflows/studios-backend.yaml new file mode 100644 index 00000000000..a8f60d9f5d8 --- /dev/null +++ b/.github/workflows/studios-backend.yaml @@ -0,0 +1,38 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Backend CI — Python formatting checks via Ruff +# Runs on pushes and PRs to main/dev when backend files change +# ───────────────────────────────────────────────────────────────────────────── +name: Python CI + +on: + workflow_call: + +concurrency: + group: backend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Ruff format check across supported Python versions ─────────────────── + format-check: + name: Ruff Format (${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install formatter + run: pip install "ruff>=0.15.5" + + - name: Verify formatting + run: ruff format --check . --exclude .venv --exclude venv + + - name: Detect logic errors + run: ruff check --select=F --ignore=F401,F403,F405,F541,F811,F841 --output-format=github . diff --git a/.github/workflows/studios-build.yml b/.github/workflows/studios-build.yml new file mode 100644 index 00000000000..fbfae439f6b --- /dev/null +++ b/.github/workflows/studios-build.yml @@ -0,0 +1,108 @@ +name: Studios Agent Hub Build +on: + workflow_call: + inputs: + productEcrRegistry: + description: 'ECR Registry' + required: true + type: string + awsRole: + description: 'AWS IAM Role' + required: false + type: string + serviceName: + description: 'Name of service' + required: true + type: string + publish: + description: 'Publish to ECR' + required: false + type: boolean + default: false + runsOn: + description: 'Defaults to ubuntu-latest, for self hosted runners use cd-runner-$serviceName-$tShirtSize' + required: false + type: string + default: 'ubuntu-latest' + outputs: + image_tag: + description: 'Tag of the built image' + value: ${{ jobs.build.outputs.image_tag }} + + secrets: + AWS_ACCESS_KEY_ID: + description: 'AWS Access Key' + required: false + AWS_SECRET_ACCESS_KEY: + description: 'AWS Secret Key' + required: false + +env: + DOCKER_API_VERSION: "1.43" +jobs: + build: + timeout-minutes: 60 + permissions: + id-token: write + contents: write + runs-on: ${{ inputs.runsOn }} + outputs: + image_tag: ${{ steps.set-tag.outputs.image_tag }} + steps: + - uses: actions/checkout@v2 + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-region: eu-west-1 + role-to-assume: ${{ inputs.awsRole }} + role-duration-seconds: 1200 + + - name: Log in to the root ECR + uses: docker/login-action@v2 + with: + registry: 655028521085.dkr.ecr.eu-west-1.amazonaws.com + + - name: Extract metadata (tags, labels) for service image + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ inputs.productEcrRegistry }}/${{ inputs.serviceName }} + tags: | + type=ref,event=branch,prefix=gha-,suffix=-{{sha}}-${{ github.run_number }} + type=ref,event=pr,prefix=gha-pr-,suffix=-{{sha}}-${{ github.run_number }} + + - name: Log in to the ECR + if: ${{ inputs.publish }} + uses: docker/login-action@v2 + with: + registry: ${{ inputs.productEcrRegistry }} + + - name: Set up docker context + run: docker context create builders + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + version: latest + endpoint: builders + + - name: Build and push service image + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ inputs.publish }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # build-args: | + # ARTIFACTORY_USERNAME=${{ secrets.ARTIFACTORY_USERNAME }} + # ARTIFACTORY_PASSWORD=${{ secrets.ARTIFACTORY_PASSWORD }} + + - name: Tag built version + id: set-tag + if: ${{ inputs.publish }} + env: + TAG_VALUE: ${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} + run: | + git tag $TAG_VALUE $GITHUB_SHA + git push origin $TAG_VALUE + echo "image_tag=$TAG_VALUE" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/workflows/studios-deploy.yml b/.github/workflows/studios-deploy.yml new file mode 100644 index 00000000000..852beac70ce --- /dev/null +++ b/.github/workflows/studios-deploy.yml @@ -0,0 +1,42 @@ +name: Agent Hub Deploy + +on: + workflow_call: + inputs: + imageTag: + description: 'Docker image tag to deploy' + required: true + type: string + ecosystem: + description: 'Target APLO ecosystem (dev for stg, prd for production)' + required: true + type: string + prdPromoteImage: + description: 'Enable production image promotion' + required: false + type: boolean + default: false + gitRef: + description: 'Git reference override for APLO' + required: false + type: string + default: 'refs/heads/main' + secrets: + INTERNAL_REPO_RO_PAT: + required: true + +jobs: + deploy: + uses: ITV/cp-gha-workflows/.github/workflows/aplo.yml@378a119ba22f72f30b95ed76485504bbca6a189c + with: + ECOSYSTEM: ${{ inputs.ecosystem }} + PRODUCT: ca + SERVICE: studios-airtable-catalogue + GIT_BRANCH: main + GIT_REF_OVERRIDE: ${{ inputs.gitRef }} + IMAGE_TAG: ${{ inputs.imageTag }} + BUILD_ENABLED: false + PRD_PROMOTE_IMAGE: ${{ inputs.prdPromoteImage }} + DEPLOY_ENABLED: false + secrets: + INTERNAL_REPO_RO_PAT: ${{ secrets.INTERNAL_REPO_RO_PAT }} \ No newline at end of file diff --git a/.github/workflows/studios-deploy_old.yml b/.github/workflows/studios-deploy_old.yml new file mode 100644 index 00000000000..780305f8034 --- /dev/null +++ b/.github/workflows/studios-deploy_old.yml @@ -0,0 +1,110 @@ +name: Agent Hub Deploy Old + +on: + workflow_call: + inputs: + imageTag: + description: 'Docker image tag to deploy' + required: true + type: string + ecosystem: + description: 'Target APLO ecosystem (dev for stg, prd for production)' + required: true + type: string + prdPromoteImage: + description: 'Enable production image promotion' + required: false + type: boolean + default: false + gitRef: + description: 'Git reference override for APLO' + required: false + type: string + default: 'refs/heads/main' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Log in to the root ECR + uses: docker/login-action@v2 + with: + registry: 655028521085.dkr.ecr.eu-west-1.amazonaws.com + + - name: Log in to the ECR + if: ${{ inputs.publish }} + uses: docker/login-action@v2 + with: + registry: ${{ inputs.productEcrRegistry }} + + - name: Check prd testing image exists + if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && !inputs.BUILD_IN_PRD + run: | + TEST_TAG="${{ env.IMAGE_TO_DEPLOY }}-testing-image" + if ! PRD_TEST_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then + echo "Unable to describe PRD ECR repository for $TEST_TAG" >&2 + exit 1 + fi + PRD_TEST_TAGS=$(echo "$PRD_TEST_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) + + if echo "$PRD_TEST_TAGS" | grep -Fx "$TEST_TAG" >/dev/null; then + echo "TESTING_IMAGE_EXISTS=true" >> "$GITHUB_ENV" + echo "Detected testing image tag $TEST_TAG in PRD ECR" + else + echo "TESTING_IMAGE_EXISTS=" >> "$GITHUB_ENV" + echo "Testing image tag $TEST_TAG not found in PRD ECR; will attempt to copy from DEV if promotion is enabled" + fi + + - name: Copy testing image to prd + if: inputs.ECOSYSTEM == 'prd' && needs.generate-values.outputs.ENABLE_TEST_IMAGE == 'true' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD + run: | + if [[ '${{ env.TESTING_IMAGE_EXISTS }}' == '' ]]; then + if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image; then + docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image + docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}-testing-image + else + if [[ '${{ env.IS_ROLLBACK }}' == 'true' ]]; then + echo "⚠️ Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image not found in DEV ECR during rollback; deployment will continue but canary tests may be skipped or fail" + else + echo "Testing image ${{ env.IMAGE_TO_DEPLOY }}-testing-image missing in DEV ECR; rerun build or ensure testing images are available" >&2 + exit 1 + fi + fi + else + echo "Testing image with tag ${{ env.IMAGE_TO_DEPLOY }}-testing-image already exists in the repository ${{ inputs.SERVICE }}" + exit 0 + fi + + - name: Check prd image exists + if: inputs.ECOSYSTEM == 'prd' && !inputs.BUILD_IN_PRD + run: | + IMAGE_TAG="${{ env.IMAGE_TO_DEPLOY }}" + if ! PRD_DESCRIBE=$(aws ecr describe-images --repository-name "${{ inputs.SERVICE }}" --region "${{ env.AWS_REGION }}" --output json); then + echo "Unable to describe PRD ECR repository for $IMAGE_TAG" >&2 + exit 1 + fi + PRD_TAGS=$(echo "$PRD_DESCRIBE" | jq -r '.imageDetails[].imageTags[]?' || true) + + if echo "$PRD_TAGS" | grep -Fx "$IMAGE_TAG" >/dev/null; then + echo "IMAGE_EXISTS=true" >> "$GITHUB_ENV" + echo "Detected image tag $IMAGE_TAG in PRD ECR" + else + echo "IMAGE_EXISTS=" >> "$GITHUB_ENV" + echo "Image tag $IMAGE_TAG not found in PRD ECR; attempting copy from DEV" + fi + + - name: Copy image to prd + if: inputs.ECOSYSTEM == 'prd' && (inputs.GIT_REF_OVERRIDE == '' || inputs.PRD_PROMOTE_IMAGE) && !inputs.BUILD_IN_PRD + run: | + if [[ '${{ env.IMAGE_EXISTS }}' == '' ]]; then + if docker pull ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }}; then + docker tag ${{ needs.generate-values.outputs.DEV_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} + docker push ${{ needs.generate-values.outputs.PRD_REGISTRY }}/${{ inputs.SERVICE }}:${{ env.IMAGE_TO_DEPLOY }} + else + echo "Failed to pull image ${{ env.IMAGE_TO_DEPLOY }} from DEV ECR; confirm the tag exists or rebuild the image before retrying." + exit 1 + fi + else + echo "Image with tag ${{ env.IMAGE_TO_DEPLOY }} already exists in the repository ${{ inputs.SERVICE }}" + exit 0 + fi diff --git a/.github/workflows/studios-frontend.yaml b/.github/workflows/studios-frontend.yaml new file mode 100644 index 00000000000..a3f08074070 --- /dev/null +++ b/.github/workflows/studios-frontend.yaml @@ -0,0 +1,55 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Frontend CI — Lint, format check, build, and unit tests +# Runs on pushes and PRs to main/dev, skipping backend-only changes +# ───────────────────────────────────────────────────────────────────────────── +name: Frontend Build + +on: + workflow_call: + +concurrency: + group: frontend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Format, i18n, and production build ──────────────────────────────────── + format-and-build: + name: Format + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + run: npm install --force + + - name: Verify code formatting + run: npm run format + + - name: Verify i18n strings + run: npm run i18n:parse + + - name: Ensure working tree is clean + run: git diff --exit-code + + # ── Vitest unit tests ──────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies (frozen lockfile) + run: npm ci --force + + - name: Execute test suite + run: npm run test:frontend diff --git a/.github/workflows/studios-upstream-sync-pr.yml b/.github/workflows/studios-upstream-sync-pr.yml new file mode 100644 index 00000000000..dbdbbe1d826 --- /dev/null +++ b/.github/workflows/studios-upstream-sync-pr.yml @@ -0,0 +1,89 @@ +name: Auto Sync Upstream Releases + +on: + schedule: + - cron: '0 6 * * *' # Runs daily at 06:00 UTC + workflow_dispatch: # Allows manual trigger anytime from Actions tab + +permissions: + contents: write + pull-requests: write + +jobs: + sync-upstream: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch Upstream Tags & Create Sync PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + UPSTREAM_URL: "https://github.com/open-webui/open-webui.git" + run: | + # 1. Setup upstream remote to fetch ONLY tags + git remote add upstream "$UPSTREAM_URL" + git config remote.upstream.fetch "+refs/tags/*:refs/tags/*" + git fetch upstream + + # 2. Get latest upstream tag matching version pattern (e.g., v1.2.0) + LATEST_TAG=$(git tag -l "v*" | sort -V | tail -n 1) + + if [ -z "$LATEST_TAG" ]; then + echo "No matching upstream tags found." + exit 0 + fi + + echo "Latest upstream tag: $LATEST_TAG" + + MY_TAG="itv-$LATEST_TAG" + BRANCH_NAME="sync/$LATEST_TAG" + + # 3. Skip if we already deployed this tag or if a sync branch/PR exists + if git rev-parse "$MY_TAG" >/dev/null 2>&1; then + echo "Tag $MY_TAG already exists in fork. Skipping." + exit 0 + fi + + if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then + echo "Branch $BRANCH_NAME already exists on remote. Skipping." + exit 0 + fi + + # 4. Create sync branch off main and merge tag + git checkout -b "$BRANCH_NAME" main + # Merge without committing immediately + git merge "$LATEST_TAG" --no-commit --no-ff || true + + # DO NOT SYNC github workflow files - the defualt GITHUB_TOKEN doesn't have permission to create PRs for workflows, so we ignore them + # Reset staged workflow changes to match fork's HEAD + git reset HEAD -- .github/workflows 2>/dev/null || true + + # Restore workflow files if they exist in fork, or remove if they don't + if git cat-file -e "HEAD:.github/workflows" 2>/dev/null; then + git checkout HEAD -- .github/workflows + else + rm -rf .github/workflows + fi + + # Clean any untracked workflow files introduced by upstream + git clean -fd .github/workflows 2>/dev/null || true + + # Complete the merge commit and push + git commit -m "sync: merge upstream $LATEST_TAG (preserved fork workflows)" + git push origin "$BRANCH_NAME" + + # 5. Open Pull Request via GitHub CLI + gh pr create \ + --repo "$GITHUB_REPOSITORY" \ + --title "sync: upstream release $LATEST_TAG" \ + --body "Automated PR bringing upstream release \`$LATEST_TAG\` into \`main\`." \ + --head "$BRANCH_NAME" \ + --base "main" \ No newline at end of file diff --git a/branding/custom.css b/branding/custom.css new file mode 100644 index 00000000000..6dc13fe0ceb --- /dev/null +++ b/branding/custom.css @@ -0,0 +1 @@ +/* :root {} */ diff --git a/docs/adr/repo-structure.md b/docs/adr/repo-structure.md new file mode 100644 index 00000000000..f55ef2cffd5 --- /dev/null +++ b/docs/adr/repo-structure.md @@ -0,0 +1,147 @@ +# ITV AI Agent Hub - OpenWebUI (OWUI) repo, structure, and updating strategy + +## Architecture decisions and recommendations + +## 1. Purpose and scope + +This document captures the key decisions for ITV Studios Agent Hub, specifically around its primary chosen interface platform, Open WebUI (OWUI). It follows on from a successful MVP programme also built using OWUI, and covers the approach for the production build: how we adopt and maintain OWUI, how we govern model access, and how our own AI agents are exposed to users. + +Decisions here reflect the engineering team’s current thinking and should be revisited as the platform matures and OWUI itself evolves. + +## 2. Licensing + +OWUI moved from a permissive BSD-3-Clause licence to a custom 'Open WebUI Licence' at version 0.6.6 around April 2025. This licence retains permissive use and modification rights, but adds a branding protection clause: + +> “deployments with more than 50 aggregate end users in any rolling 30-day period may not remove or alter the 'OWUI' name, logo, or UI marks unless they hold an Enterprise licence or specific written permission.” + +### Decision + +ITV’s plan is to purchase an OWUI Enterprise licence. This removes the branding restriction entirely and allows for full white-labelling. Enterprise licensing also offers: + +- SLA-backed commercial support +- Access to Long-Term Support (LTS) release versions +- A supported path for full theming and custom branding without legal ambiguity + +This decision removes licensing as a constraint on the technical approach to how we customise and brand the OWUI interface described in the rest of this document. + +## 3. Repository forking and upstream sync strategy + +The core OWUI product is a living codebase; an ongoing concern in active development and maintenance. This means we can benefit from OWUI’s engineering team producing new features, pushing bug fixes, and closing security vulnerabilities. Whilst this is an absolute win for the business, it creates a challenge for the Agent Hub Delivery Team in bringing these changes into our product from the upstream codebase. + +There are two approaches to take: + +1. We pull a fork of the original OWUI project and track it as an upstream remote and merge in new tagged releases as they become available. +2. We create a brand new repository based on the latest version of OWUI and manually merge in updates and releases as we diverge away from the original codebase. + +### Decision + +The recommendation – based on several discussions with businesses in similar positions, documentation and agentic-supported research – is that ITV Studios Agent Hub should is **built as a maintained fork** of `open-webui/open-webui`, rather than a from-scratch build, in order to benefit from OWUI's ongoing feature development, security fixes, and community ecosystem with minimal ongoing engineering cost. + +In practice this means: + +- Fork `open-webui/open-webui` into ITV's own GitHub organisation. +- Add the original repository as a git remote (upstream) and track tagged releases only — never the moving `main` or `dev` branches. +- Pull in new upstream releases via a short-lived `sync/vX.Y.Z` branch, + - Apply changes via merging (**not rebasing**) into main. + - Review changes via standard PR process. + - Once deployed, tag with appropriate release version, e.g. `itv-vX.Y.Z`. +- Keep ITV-specific changes confined to configuration, branding assets, and deployment tooling — avoid modifying OWUI's own source files wherever possible, to keep merge conflicts rare and upstream syncs low-risk. + +### Upstream open-source contributions + +A path exists to contribute fixes back upstream to the main OWUI project where useful. Any contribution merged after v0.6.5 requires agreeing to OWUI's Contributor Licence Agreement (CLA), and the core team heavily reviews and reworks community PRs, so this is best reserved for genuinely general-purpose fixes rather than ITV-specific work. + +## 4. Customisation approach + +OWUI is layered in a way that supports the Agent Hub Delivery Team’s goal of staying easy to update and keep in sync with upstream releases. + +The proposed approach below favours the lowest-friction customisation layer available for each need: + +| Layer | Use for | +| --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| **CSS theming**
(custom.css, injected via our own Docker image layer) | Branding, colours, fonts, ITV visual identity — no core source changes required | +| **Functions, Tools, Filters**
(in-process Python plugins, managed via Admin Panel) | Lightweight behavioural changes: message filters, custom UI buttons, new model source wiring | +| **Pipelines**
(standalone microservice, OpenAI-API-compatible) | Heavier processing that shouldn't run inside the OWUI process itself | +| **Direct source modification** | Avoided wherever possible — reserved for cases with no other option, since this is where upstream merge conflicts arise | + +It’s recommended that we minimise any direct customisation of OWUI’s existing codebase to reduce treacherous version updates. Modifications should be created as separate modules or Svelte components and brought into their relevant areas in as discreet a manner as possible. For example, adding custom buttons to the chat window or specialist HTML responses. + +Given our own AI agents will live in a separate Python codebase/repository, the strong preference is to keep that separation intact rather than pulling agent logic into OWUI's Pipelines framework (see section 6). + +## 5. Repository structure and branching + +At a high level, the core Agent Hub would be comprised of these three repositories, matching the natural seams in the system: + +```bash +studios-agent-hub/ # ← the main codebase, the OWUI fork +studio-agent-hub-agents/ # ← FastAPI + Bedrock agents (e.g. Sales Agent) +``` + +**\*note**: repository names are placeholders at this stage and this outline don’t account for separate ‘infra’ style repositories to support deployment and CPV3 provisioning\* + +The instance of LiteLLM and its proxy configuration is handled in a separate infra repository. + +### Example agent-hub structure + +```bash +agent-hub/ +├── branding/ # custom.css, logo and icon assets +├── deploy/ # (if cpv3 provisioning lives here) +│ ├── helm/values-cpv3.yaml +│ └── terraform/ +├── docs/adr/ +├── Dockerfile # FROM ghcr.io/open-webui/open-webui:vX.Y.Z + COPY branding/ +└── .github/workflows/ # build/push image, upstream diff checks, etc. +``` + +The original (vendored) OWUI source itself is not shown above because it’s not copied into the repository at all — it's pulled in via the fork relationship (git history) and consumed as a base image in the Dockerfile. + +Our Agent Hub specific additions sit alongside this as configuration, assets, and deployment tooling, never as edits mixed into OWUI's own source tree. This is what keeps upstream syncs (section 3) low-conflict. + +### Example branching model + +| Example branch | Used for | +| ----------------------------------- | -------------------------------------------------------- | +| `main` | Core production/release branch; what's actually deployed | +| upstream (remote) | `open-webui/open-webui`, tracked, never pushed to | +| `sync/vX.Y.Z` | short-lived branch per upstream release pulled in | +| `feature/AH-123`, `AH456-some-name` | standard Jira-ticket branches (existing team convention) | + +## 6. Model and agent governance via LiteLLM + +Agent Hub does not connect directly to approved models, or to our own agents (e.g. Sales Agent). All model traffic is routed through a LiteLLM proxy, which acts as the single governance point for ‘what's exposed and to whom’. + +### Decision and rationale + +- Foundation models (e.g. Bedrock, OpenAI) will be registered in LiteLLM as model deployments, so they present identically to Agent Hub as OpenAI-API-compatible ‘models’. +- Each FastAPI agent (e.g. Sales Agent) wraps its logic behind its own `/v1/chat/completions-compatible` endpoint which can then be connected to OWUI directly via the Pipelines framework. This enables us to support rich HTML or complex UI components called by and from agents or tools by dealing with OWUI's internal HTML renderer. +- This approach helps to maintain a restricted governance path: + - model allow-listing, + - per-team/per-key access control, + - budgets, + - rate limiting, + - and usage/cost tracking and metrics +- This also helps to keep the OWUI fork closer to stock, since very minimal custom pipe or connector code needs to live inside it. + +Sensitive data handling (e.g. the Sales Agent's access to Rights and financial data) happens entirely inside the agent's own API service, ahead of the call to Bedrock. OWUI and LiteLLM have no awareness of the underlying data sources — they only see a model that produces relevant answers. + +### Open item + +Model-level visibility (e.g. restricting the Sales Agent to specific teams) could be enforced at two independent layers: + +1. OWUI's group-based model visibility controls. +2. LiteLLM's own key/team-based access controls. + +The specific mapping of ITV's user groups to both layers is not yet finalised and will involve some sort of Okta grouping. + +## 7. Security and operational considerations + +- Functions, Tools, and Pipes execute arbitrary Python with the same file and network permissions as the OWUI process. **Community-sourced plugins should not be imported without code review**, particularly in a walled cpv3 environment. +- Outbound calls OWUI makes by default (community marketplace, update checks, telemetry) should be audited and disabled where not needed. +- Configuration that can persist to the database via the Admin UI (e.g. OAuth settings) should be pinned to environment variables (`ENABLE_OAUTH_PERSISTENT_CONFIG=false` and equivalents) to keep configuration reproducible and version-controlled rather than living as manual UI changes. + +## 8. Open questions + +1. Preferred cadence for pulling in upstream OWUI releases (e.g. monthly review vs. on-demand). +2. Model-level visibility (see ‘Open item’ in section 7 above). +3. Naming conventions, including repo name, branching, and so on.