Commit
·
9434d3d
0
Parent(s):
Deploy ChatGPT MCP Server
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +6 -0
- .github/FUNDING.yml +3 -0
- .github/ISSUE_TEMPLATE/bug_report.yml +37 -0
- .github/ISSUE_TEMPLATE/feature_request.yml +29 -0
- .github/renovate.json +13 -0
- .github/workflows/ci.yml +67 -0
- .github/workflows/deploy-hf-space.yml +38 -0
- .gitignore +56 -0
- .npmrc +5 -0
- .nvmrc +1 -0
- .vscode/extensions.json +8 -0
- .vscode/launch.json +15 -0
- .vscode/project.code-workspace +36 -0
- .vscode/settings.json +7 -0
- DEPLOYMENT.md +335 -0
- Dockerfile +57 -0
- README.md +55 -0
- RUN_LOCAL.sh +54 -0
- SUBMISSION.md +120 -0
- apps/eu-ai-act-agent/.gitignore +52 -0
- apps/eu-ai-act-agent/.python-version +2 -0
- apps/eu-ai-act-agent/API.md +579 -0
- apps/eu-ai-act-agent/ARCHITECTURE.md +674 -0
- apps/eu-ai-act-agent/DEPLOYMENT.md +302 -0
- apps/eu-ai-act-agent/Dockerfile +61 -0
- apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp +57 -0
- apps/eu-ai-act-agent/EXAMPLES.md +517 -0
- apps/eu-ai-act-agent/QUICKSTART.md +371 -0
- apps/eu-ai-act-agent/README.md +502 -0
- apps/eu-ai-act-agent/biome.json +30 -0
- apps/eu-ai-act-agent/package.json +47 -0
- apps/eu-ai-act-agent/pyproject.toml +24 -0
- apps/eu-ai-act-agent/requirements.txt +7 -0
- apps/eu-ai-act-agent/src/.mcp_url +1 -0
- apps/eu-ai-act-agent/src/agent/index.ts +819 -0
- apps/eu-ai-act-agent/src/agent/prompts.ts +533 -0
- apps/eu-ai-act-agent/src/chatgpt_app.py +1410 -0
- apps/eu-ai-act-agent/src/gradio_app.py +1502 -0
- apps/eu-ai-act-agent/src/server.ts +1235 -0
- apps/eu-ai-act-agent/src/types/index.ts +43 -0
- apps/eu-ai-act-agent/start.sh +127 -0
- apps/eu-ai-act-agent/tsconfig.json +22 -0
- apps/eu-ai-act-agent/tsup.config.ts +14 -0
- apps/eu-ai-act-agent/tsx +0 -0
- apps/eu-ai-act-agent/uv.lock +0 -0
- biome.json +43 -0
- modal/README.md +237 -0
- modal/deploy.sh +84 -0
- modal/gpt_oss_inference.py +362 -0
- modal/requirements.txt +4 -0
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
TAVILY_API_KEY=
|
| 2 |
+
OPENAI_API_KEY=
|
| 3 |
+
XAI_API_KEY=
|
| 4 |
+
ANTHROPIC_API_KEY=
|
| 5 |
+
GOOGLE_API_KEY=
|
| 6 |
+
AI_MODEL="gpt-5/grok-4-1/claude-4-5"
|
.github/FUNDING.yml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# These are supported funding model platforms
|
| 2 |
+
|
| 3 |
+
github: rajatsandeepsen
|
.github/ISSUE_TEMPLATE/bug_report.yml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: 🐞 Bug Report
|
| 2 |
+
description: Create a bug report to help us improve
|
| 3 |
+
title: "bug: "
|
| 4 |
+
labels: ["🐞❔ unconfirmed bug"]
|
| 5 |
+
body:
|
| 6 |
+
- type: textarea
|
| 7 |
+
attributes:
|
| 8 |
+
label: Provide environment information
|
| 9 |
+
description: |
|
| 10 |
+
Run this command in your project root and paste the results in a code block:
|
| 11 |
+
```bash
|
| 12 |
+
npx envinfo --system --binaries
|
| 13 |
+
```
|
| 14 |
+
validations:
|
| 15 |
+
required: true
|
| 16 |
+
- type: textarea
|
| 17 |
+
attributes:
|
| 18 |
+
label: Describe the bug
|
| 19 |
+
description: A clear and concise description of the bug, as well as what you expected to happen when encountering it.
|
| 20 |
+
validations:
|
| 21 |
+
required: true
|
| 22 |
+
- type: input
|
| 23 |
+
attributes:
|
| 24 |
+
label: Link to reproduction
|
| 25 |
+
description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored.
|
| 26 |
+
validations:
|
| 27 |
+
required: true
|
| 28 |
+
- type: textarea
|
| 29 |
+
attributes:
|
| 30 |
+
label: To reproduce
|
| 31 |
+
description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc.
|
| 32 |
+
validations:
|
| 33 |
+
required: true
|
| 34 |
+
- type: textarea
|
| 35 |
+
attributes:
|
| 36 |
+
label: Additional information
|
| 37 |
+
description: Add any other information related to the bug here, screenshots if applicable.
|
.github/ISSUE_TEMPLATE/feature_request.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This template is heavily inspired by the Next.js's template:
|
| 2 |
+
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
|
| 3 |
+
|
| 4 |
+
name: 🛠 Feature Request
|
| 5 |
+
description: Create a feature request for the core packages
|
| 6 |
+
title: "feat: "
|
| 7 |
+
labels: ["✨ enhancement"]
|
| 8 |
+
body:
|
| 9 |
+
- type: markdown
|
| 10 |
+
attributes:
|
| 11 |
+
value: |
|
| 12 |
+
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
|
| 13 |
+
- type: textarea
|
| 14 |
+
attributes:
|
| 15 |
+
label: Describe the feature you'd like to request
|
| 16 |
+
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
|
| 17 |
+
validations:
|
| 18 |
+
required: true
|
| 19 |
+
- type: textarea
|
| 20 |
+
attributes:
|
| 21 |
+
label: Describe the solution you'd like to see
|
| 22 |
+
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
|
| 23 |
+
validations:
|
| 24 |
+
required: true
|
| 25 |
+
- type: textarea
|
| 26 |
+
attributes:
|
| 27 |
+
label: Additional information
|
| 28 |
+
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
| 29 |
+
|
.github/renovate.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
| 3 |
+
"extends": ["config:base"],
|
| 4 |
+
"packageRules": [
|
| 5 |
+
{
|
| 6 |
+
"matchPackagePatterns": ["^@decode/"],
|
| 7 |
+
"enabled": false
|
| 8 |
+
}
|
| 9 |
+
],
|
| 10 |
+
"updateInternalDeps": true,
|
| 11 |
+
"rangeStrategy": "bump",
|
| 12 |
+
"automerge": true
|
| 13 |
+
}
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
pull_request:
|
| 5 |
+
branches: ["*"]
|
| 6 |
+
push:
|
| 7 |
+
branches: ["main"]
|
| 8 |
+
merge_group:
|
| 9 |
+
|
| 10 |
+
concurrency:
|
| 11 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 12 |
+
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
| 13 |
+
|
| 14 |
+
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
|
| 15 |
+
# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
|
| 16 |
+
env:
|
| 17 |
+
FORCE_COLOR: 3
|
| 18 |
+
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
| 19 |
+
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
| 20 |
+
|
| 21 |
+
jobs:
|
| 22 |
+
lint:
|
| 23 |
+
runs-on: ubuntu-latest
|
| 24 |
+
steps:
|
| 25 |
+
- uses: actions/checkout@v4
|
| 26 |
+
|
| 27 |
+
- name: Setup
|
| 28 |
+
uses: ./tooling/github/setup
|
| 29 |
+
|
| 30 |
+
- name: Setup Biome
|
| 31 |
+
uses: biomejs/setup-biome@v2
|
| 32 |
+
with:
|
| 33 |
+
version: latest
|
| 34 |
+
|
| 35 |
+
- name: Copy env
|
| 36 |
+
shell: bash
|
| 37 |
+
run: cp .env.example .env
|
| 38 |
+
|
| 39 |
+
- name: Lint
|
| 40 |
+
run: pnpm lint
|
| 41 |
+
|
| 42 |
+
format:
|
| 43 |
+
runs-on: ubuntu-latest
|
| 44 |
+
steps:
|
| 45 |
+
- uses: actions/checkout@v4
|
| 46 |
+
|
| 47 |
+
- name: Setup
|
| 48 |
+
uses: ./tooling/github/setup
|
| 49 |
+
|
| 50 |
+
- name: Setup Biome
|
| 51 |
+
uses: biomejs/setup-biome@v2
|
| 52 |
+
with:
|
| 53 |
+
version: latest
|
| 54 |
+
|
| 55 |
+
- name: Format
|
| 56 |
+
run: pnpm format
|
| 57 |
+
|
| 58 |
+
typecheck:
|
| 59 |
+
runs-on: ubuntu-latest
|
| 60 |
+
steps:
|
| 61 |
+
- uses: actions/checkout@v4
|
| 62 |
+
|
| 63 |
+
- name: Setup
|
| 64 |
+
uses: ./tooling/github/setup
|
| 65 |
+
|
| 66 |
+
- name: Typecheck
|
| 67 |
+
run: turbo typecheck
|
.github/workflows/deploy-hf-space.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy to Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
env:
|
| 9 |
+
HF_SPACE: MCP-1st-Birthday/eu-ai-act-compliance-agent
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
deploy:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
steps:
|
| 15 |
+
- uses: actions/checkout@v4
|
| 16 |
+
with:
|
| 17 |
+
fetch-depth: 0
|
| 18 |
+
lfs: true
|
| 19 |
+
|
| 20 |
+
- name: Push to Hugging Face Space
|
| 21 |
+
env:
|
| 22 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 23 |
+
run: |
|
| 24 |
+
# Configure git
|
| 25 |
+
git config --global user.email "[email protected]"
|
| 26 |
+
git config --global user.name "GitHub Actions"
|
| 27 |
+
|
| 28 |
+
# Copy Space files from apps/eu-ai-act-agent to root
|
| 29 |
+
cp apps/eu-ai-act-agent/README_HF.md ./README.md
|
| 30 |
+
cp apps/eu-ai-act-agent/Dockerfile ./Dockerfile
|
| 31 |
+
|
| 32 |
+
# Add HF Space remote and push
|
| 33 |
+
git remote add hf https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_SPACE} || true
|
| 34 |
+
git add -A
|
| 35 |
+
git commit -m "Deploy to HF Spaces" --allow-empty
|
| 36 |
+
git push hf main:main --force
|
| 37 |
+
|
| 38 |
+
echo "✅ Deployed to https://huggingface.co/spaces/${HF_SPACE}"
|
.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
node_modules
|
| 5 |
+
.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
coverage
|
| 10 |
+
|
| 11 |
+
# next.js
|
| 12 |
+
.next/
|
| 13 |
+
out/
|
| 14 |
+
next-env.d.ts
|
| 15 |
+
|
| 16 |
+
# nitro
|
| 17 |
+
.nitro/
|
| 18 |
+
.output/
|
| 19 |
+
|
| 20 |
+
# expo
|
| 21 |
+
.expo/
|
| 22 |
+
dist/
|
| 23 |
+
expo-env.d.ts
|
| 24 |
+
apps/expo/.gitignore
|
| 25 |
+
|
| 26 |
+
# production
|
| 27 |
+
build
|
| 28 |
+
|
| 29 |
+
# misc
|
| 30 |
+
.DS_Store
|
| 31 |
+
*.pem
|
| 32 |
+
|
| 33 |
+
# debug
|
| 34 |
+
npm-debug.log*
|
| 35 |
+
yarn-debug.log*
|
| 36 |
+
yarn-error.log*
|
| 37 |
+
.pnpm-debug.log*
|
| 38 |
+
|
| 39 |
+
# local env files
|
| 40 |
+
.env
|
| 41 |
+
.env*.local
|
| 42 |
+
|
| 43 |
+
# vercel
|
| 44 |
+
.vercel
|
| 45 |
+
|
| 46 |
+
# typescript
|
| 47 |
+
*.tsbuildinfo
|
| 48 |
+
|
| 49 |
+
# turbo
|
| 50 |
+
.turbo
|
| 51 |
+
/.history
|
| 52 |
+
|
| 53 |
+
# generated compliance documentation
|
| 54 |
+
compliance-docs/
|
| 55 |
+
/modal/__pycache__
|
| 56 |
+
modal/__pycache__/gpt_oss_inference.cpython-313.pyc
|
.npmrc
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Expo doesn't play nice with pnpm by default.
|
| 2 |
+
# The symbolic links of pnpm break the rules of Expo monorepos.
|
| 3 |
+
# @link https://docs.expo.dev/guides/monorepos/#common-issues
|
| 4 |
+
node-linker=hoisted
|
| 5 |
+
strict-peer-dependencies=false
|
.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
18.18.2
|
.vscode/extensions.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"recommendations": [
|
| 3 |
+
"bradlc.vscode-tailwindcss",
|
| 4 |
+
"expo.vscode-expo-tools",
|
| 5 |
+
"yoavbls.pretty-ts-errors",
|
| 6 |
+
"biomejs.biome"
|
| 7 |
+
]
|
| 8 |
+
}
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "0.2.0",
|
| 3 |
+
"configurations": [
|
| 4 |
+
{
|
| 5 |
+
"name": "Next.js",
|
| 6 |
+
"type": "node-terminal",
|
| 7 |
+
"request": "launch",
|
| 8 |
+
"command": "pnpm dev",
|
| 9 |
+
"cwd": "${workspaceFolder}/apps/web/",
|
| 10 |
+
"skipFiles": [
|
| 11 |
+
"<node_internals>/**"
|
| 12 |
+
]
|
| 13 |
+
}
|
| 14 |
+
]
|
| 15 |
+
}
|
.vscode/project.code-workspace
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"folders": [
|
| 3 |
+
{
|
| 4 |
+
"path": "..",
|
| 5 |
+
"name": "root"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"name": "native",
|
| 9 |
+
"path": "../apps/native/"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"name": "web",
|
| 13 |
+
"path": "../apps/web/"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"name": "auth-proxy",
|
| 17 |
+
"path": "../apps/auth-proxy/"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"name": "api",
|
| 21 |
+
"path": "../packages/api/"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"name": "db",
|
| 25 |
+
"path": "../packages/db/"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"name": "auth",
|
| 29 |
+
"path": "../packages/auth/"
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"name": "tooling",
|
| 33 |
+
"path": "../tooling/"
|
| 34 |
+
}
|
| 35 |
+
]
|
| 36 |
+
}
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"eslint.workingDirectories": [
|
| 3 |
+
{
|
| 4 |
+
"mode": "auto"
|
| 5 |
+
}
|
| 6 |
+
]
|
| 7 |
+
}
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Deployment Guide
|
| 2 |
+
|
| 3 |
+
This guide covers deploying the EU AI Act Compliance Suite for the MCP 1st Birthday Hackathon.
|
| 4 |
+
|
| 5 |
+
## 📋 Table of Contents
|
| 6 |
+
|
| 7 |
+
- [Deployment Options](#deployment-options)
|
| 8 |
+
- [Hugging Face Spaces (Recommended)](#hugging-face-spaces-recommended)
|
| 9 |
+
- [Manual Deployment](#manual-deployment)
|
| 10 |
+
- [GitHub Actions CI/CD](#github-actions-cicd)
|
| 11 |
+
- [Environment Variables](#environment-variables)
|
| 12 |
+
- [Hackathon Submission Checklist](#hackathon-submission-checklist)
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## 🎯 Deployment Options
|
| 17 |
+
|
| 18 |
+
| Option | Best For | Difficulty |
|
| 19 |
+
|--------|----------|------------|
|
| 20 |
+
| **Hugging Face Spaces** | Hackathon submission, public demos | ⭐ Easy |
|
| 21 |
+
| **Docker** | Self-hosted, production | ⭐⭐ Medium |
|
| 22 |
+
| **Local Development** | Testing, development | ⭐ Easy |
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 🤗 Hugging Face Spaces (Recommended)
|
| 27 |
+
|
| 28 |
+
The easiest way to deploy for the hackathon is using **Hugging Face Spaces**.
|
| 29 |
+
|
| 30 |
+
### Method 1: Automated Deployment (GitHub Actions)
|
| 31 |
+
|
| 32 |
+
1. **Fork this repository** to your GitHub account
|
| 33 |
+
|
| 34 |
+
2. **Add GitHub Secrets:**
|
| 35 |
+
- Go to your repo → Settings → Secrets and variables → Actions
|
| 36 |
+
- Add `HF_TOKEN`: Your Hugging Face token with write access
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
# Get your HF token from: https://huggingface.co/settings/tokens
|
| 40 |
+
# Required scopes: write access to spaces
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
3. **Join the Hackathon Organization:**
|
| 44 |
+
- Go to [MCP-1st-Birthday](https://huggingface.co/MCP-1st-Birthday)
|
| 45 |
+
- Click "Request to join this org"
|
| 46 |
+
- Wait for approval
|
| 47 |
+
|
| 48 |
+
4. **Trigger Deployment:**
|
| 49 |
+
- Push to `main` branch (auto-deploys on changes to `spaces/` directory)
|
| 50 |
+
- Or manually trigger via GitHub Actions → "Deploy to Hugging Face Spaces" → "Run workflow"
|
| 51 |
+
|
| 52 |
+
5. **Configure Space Secrets:**
|
| 53 |
+
- Go to your Space settings: `https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance/settings`
|
| 54 |
+
- Add secrets:
|
| 55 |
+
- `XAI_API_KEY` (required) - Get from [x.ai](https://x.ai/)
|
| 56 |
+
- `TAVILY_API_KEY` (optional) - Get from [tavily.com](https://app.tavily.com/)
|
| 57 |
+
|
| 58 |
+
### Method 2: Manual Upload
|
| 59 |
+
|
| 60 |
+
1. **Create a new Space:**
|
| 61 |
+
```bash
|
| 62 |
+
# Install huggingface_hub
|
| 63 |
+
pip install huggingface_hub
|
| 64 |
+
|
| 65 |
+
# Login
|
| 66 |
+
huggingface-cli login
|
| 67 |
+
|
| 68 |
+
# Create space
|
| 69 |
+
huggingface-cli repo create eu-ai-act-compliance --type space --space-sdk gradio
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
2. **Upload files:**
|
| 73 |
+
```bash
|
| 74 |
+
cd spaces/eu-ai-act-compliance
|
| 75 |
+
|
| 76 |
+
# Clone the space
|
| 77 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/eu-ai-act-compliance
|
| 78 |
+
|
| 79 |
+
# Copy files
|
| 80 |
+
cp -r . eu-ai-act-compliance/
|
| 81 |
+
|
| 82 |
+
# Push
|
| 83 |
+
cd eu-ai-act-compliance
|
| 84 |
+
git add .
|
| 85 |
+
git commit -m "Initial deployment"
|
| 86 |
+
git push
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
3. **Transfer to hackathon org** (for submission):
|
| 90 |
+
- Go to Space Settings → Transfer
|
| 91 |
+
- Transfer to `MCP-1st-Birthday` organization
|
| 92 |
+
|
| 93 |
+
### Method 3: Using the Deploy Script
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
# Run the deployment script
|
| 97 |
+
./scripts/deploy-hf.sh
|
| 98 |
+
|
| 99 |
+
# With custom org/name
|
| 100 |
+
./scripts/deploy-hf.sh --org MCP-1st-Birthday --name eu-ai-act-compliance
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 🐳 Docker Deployment
|
| 106 |
+
|
| 107 |
+
### Build and Run
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
# Build the image
|
| 111 |
+
docker build -t eu-ai-act-compliance -f Dockerfile .
|
| 112 |
+
|
| 113 |
+
# Run with environment variables
|
| 114 |
+
docker run -p 7860:7860 \
|
| 115 |
+
-e XAI_API_KEY=your-key \
|
| 116 |
+
-e TAVILY_API_KEY=your-key \
|
| 117 |
+
eu-ai-act-compliance
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
### Docker Compose
|
| 121 |
+
|
| 122 |
+
```yaml
|
| 123 |
+
version: '3.8'
|
| 124 |
+
services:
|
| 125 |
+
eu-ai-act-agent:
|
| 126 |
+
build: .
|
| 127 |
+
ports:
|
| 128 |
+
- "7860:7860"
|
| 129 |
+
environment:
|
| 130 |
+
- XAI_API_KEY=${XAI_API_KEY}
|
| 131 |
+
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
| 132 |
+
restart: unless-stopped
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 🔧 Manual Deployment
|
| 138 |
+
|
| 139 |
+
### Prerequisites
|
| 140 |
+
|
| 141 |
+
- Node.js 18+
|
| 142 |
+
- Python 3.9+
|
| 143 |
+
- pnpm 8+
|
| 144 |
+
|
| 145 |
+
### Steps
|
| 146 |
+
|
| 147 |
+
```bash
|
| 148 |
+
# 1. Clone the repository
|
| 149 |
+
git clone https://github.com/your-org/eu-ai-act-compliance.git
|
| 150 |
+
cd eu-ai-act-compliance
|
| 151 |
+
|
| 152 |
+
# 2. Install dependencies
|
| 153 |
+
pnpm install
|
| 154 |
+
|
| 155 |
+
# 3. Set up environment variables
|
| 156 |
+
cp .env.example .env
|
| 157 |
+
# Edit .env and add your API keys
|
| 158 |
+
|
| 159 |
+
# 4. Build the MCP server
|
| 160 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 161 |
+
|
| 162 |
+
# 5. Start the agent (API + Gradio)
|
| 163 |
+
cd apps/eu-ai-act-agent
|
| 164 |
+
./start.sh
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
### Production Mode
|
| 168 |
+
|
| 169 |
+
```bash
|
| 170 |
+
# Build everything
|
| 171 |
+
pnpm build
|
| 172 |
+
|
| 173 |
+
# Start in production
|
| 174 |
+
cd apps/eu-ai-act-agent
|
| 175 |
+
NODE_ENV=production node dist/server.js &
|
| 176 |
+
python src/gradio_app.py
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## 🔄 GitHub Actions CI/CD
|
| 182 |
+
|
| 183 |
+
### Workflows
|
| 184 |
+
|
| 185 |
+
| Workflow | Trigger | Purpose |
|
| 186 |
+
|----------|---------|---------|
|
| 187 |
+
| `ci.yml` | Push/PR | Lint, typecheck, build |
|
| 188 |
+
| `deploy-hf-space.yml` | Push to main + `spaces/` changes | Deploy to HF Spaces |
|
| 189 |
+
|
| 190 |
+
### Required Secrets
|
| 191 |
+
|
| 192 |
+
| Secret | Required | Description |
|
| 193 |
+
|--------|----------|-------------|
|
| 194 |
+
| `HF_TOKEN` | Yes | Hugging Face token with write access |
|
| 195 |
+
|
| 196 |
+
### Manual Deployment Trigger
|
| 197 |
+
|
| 198 |
+
1. Go to Actions → "Deploy to Hugging Face Spaces"
|
| 199 |
+
2. Click "Run workflow"
|
| 200 |
+
3. Select branch and environment
|
| 201 |
+
4. Click "Run workflow"
|
| 202 |
+
|
| 203 |
+
---
|
| 204 |
+
|
| 205 |
+
## 🔐 Environment Variables
|
| 206 |
+
|
| 207 |
+
### Required
|
| 208 |
+
|
| 209 |
+
| Variable | Description | Where to Get |
|
| 210 |
+
|----------|-------------|--------------|
|
| 211 |
+
| `XAI_API_KEY` | xAI API key for Grok model | [console.x.ai](https://console.x.ai/) |
|
| 212 |
+
|
| 213 |
+
### Optional
|
| 214 |
+
|
| 215 |
+
| Variable | Description | Where to Get |
|
| 216 |
+
|----------|-------------|--------------|
|
| 217 |
+
| `TAVILY_API_KEY` | Tavily API for web research | [app.tavily.com](https://app.tavily.com/) |
|
| 218 |
+
| `PORT` | API server port (default: 3001) | - |
|
| 219 |
+
|
| 220 |
+
### Setting Secrets in Hugging Face Spaces
|
| 221 |
+
|
| 222 |
+
1. Go to your Space: `https://huggingface.co/spaces/ORG/SPACE_NAME`
|
| 223 |
+
2. Click ⚙️ Settings
|
| 224 |
+
3. Scroll to "Repository secrets"
|
| 225 |
+
4. Add each secret:
|
| 226 |
+
- Name: `XAI_API_KEY`
|
| 227 |
+
- Value: Your API key
|
| 228 |
+
- Click "Add"
|
| 229 |
+
|
| 230 |
+
---
|
| 231 |
+
|
| 232 |
+
## ✅ Hackathon Submission Checklist
|
| 233 |
+
|
| 234 |
+
### Before Submission (Nov 30, 2025 11:59 PM UTC)
|
| 235 |
+
|
| 236 |
+
- [ ] **Join the organization**: [Request to join MCP-1st-Birthday](https://huggingface.co/MCP-1st-Birthday)
|
| 237 |
+
- [ ] **Deploy your Space**: Make sure it's running and accessible
|
| 238 |
+
- [ ] **Configure secrets**: Add `XAI_API_KEY` (and optionally `TAVILY_API_KEY`)
|
| 239 |
+
- [ ] **Test the demo**: Verify all features work
|
| 240 |
+
|
| 241 |
+
### README Requirements
|
| 242 |
+
|
| 243 |
+
Your Space README must include:
|
| 244 |
+
|
| 245 |
+
- [ ] **Hackathon tags** in frontmatter:
|
| 246 |
+
```yaml
|
| 247 |
+
tags:
|
| 248 |
+
- mcp
|
| 249 |
+
- agents
|
| 250 |
+
- track-1-mcp-servers
|
| 251 |
+
- track-2-agentic-applications
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
- [ ] **Social media link**: Share your project and include the link
|
| 255 |
+
```markdown
|
| 256 |
+
[🐦 Twitter Post](https://twitter.com/your-post-link)
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
### Track Tags
|
| 260 |
+
|
| 261 |
+
| Track | Tags |
|
| 262 |
+
|-------|------|
|
| 263 |
+
| Track 1: Building MCP | `track-1-mcp-servers`, `mcp` |
|
| 264 |
+
| Track 2: MCP in Action | `track-2-agentic-applications`, `agents` |
|
| 265 |
+
|
| 266 |
+
### Social Media Post Template
|
| 267 |
+
|
| 268 |
+
```
|
| 269 |
+
🇪🇺 Excited to share my #MCPHackathon submission!
|
| 270 |
+
|
| 271 |
+
EU AI Act Compliance Agent - AI-powered compliance assessment with MCP tools
|
| 272 |
+
|
| 273 |
+
✅ Discover organization profiles
|
| 274 |
+
✅ Classify AI systems by risk
|
| 275 |
+
✅ Generate compliance documentation
|
| 276 |
+
|
| 277 |
+
Try it: [HF Space Link]
|
| 278 |
+
|
| 279 |
+
#MCP #AIAct #Gradio @huggingface
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
## 🔍 Troubleshooting
|
| 285 |
+
|
| 286 |
+
### Space Not Building
|
| 287 |
+
|
| 288 |
+
1. Check `requirements.txt` for valid packages
|
| 289 |
+
2. Verify Python version compatibility
|
| 290 |
+
3. Check build logs in Space settings
|
| 291 |
+
|
| 292 |
+
### API Key Errors
|
| 293 |
+
|
| 294 |
+
1. Verify secrets are set in Space settings
|
| 295 |
+
2. Check secret names match exactly (case-sensitive)
|
| 296 |
+
3. Ensure API keys are valid and have required permissions
|
| 297 |
+
|
| 298 |
+
### Deployment Failing
|
| 299 |
+
|
| 300 |
+
1. Check GitHub Actions logs
|
| 301 |
+
2. Verify `HF_TOKEN` has write access
|
| 302 |
+
3. Ensure you're a member of the target organization
|
| 303 |
+
|
| 304 |
+
### Space Sleeping
|
| 305 |
+
|
| 306 |
+
Free HF Spaces sleep after inactivity. To wake:
|
| 307 |
+
1. Visit the Space URL
|
| 308 |
+
2. Wait for it to build/start
|
| 309 |
+
3. Consider upgrading for persistent uptime
|
| 310 |
+
|
| 311 |
+
---
|
| 312 |
+
|
| 313 |
+
## 📞 Support
|
| 314 |
+
|
| 315 |
+
- **Hackathon Discord**: [#agents-mcp-hackathon-winter25🏆](https://discord.gg/huggingface)
|
| 316 |
+
- **GitHub Issues**: [Create an issue](https://github.com/your-org/eu-ai-act-compliance/issues)
|
| 317 |
+
- **Email**: [email protected]
|
| 318 |
+
|
| 319 |
+
---
|
| 320 |
+
|
| 321 |
+
## 📚 Additional Resources
|
| 322 |
+
|
| 323 |
+
- [Hugging Face Spaces Documentation](https://huggingface.co/docs/hub/spaces)
|
| 324 |
+
- [Gradio Deployment Guide](https://www.gradio.app/guides/sharing-your-app)
|
| 325 |
+
- [MCP Course](https://huggingface.co/learn/mcp-course)
|
| 326 |
+
- [Hackathon Page](https://huggingface.co/MCP-1st-Birthday)
|
| 327 |
+
|
| 328 |
+
---
|
| 329 |
+
|
| 330 |
+
<div align="center">
|
| 331 |
+
|
| 332 |
+
**Good luck with your submission! 🎂**
|
| 333 |
+
|
| 334 |
+
</div>
|
| 335 |
+
|
Dockerfile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EU AI Act - ChatGPT MCP Server
|
| 2 |
+
# Standalone MCP server for ChatGPT Apps integration
|
| 3 |
+
# Deploys ONLY the MCP tools (discover_organization, discover_ai_services, assess_compliance)
|
| 4 |
+
|
| 5 |
+
FROM node:20-slim
|
| 6 |
+
|
| 7 |
+
# Install Python, pnpm, and uv
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
python3 \
|
| 10 |
+
python3-venv \
|
| 11 |
+
curl \
|
| 12 |
+
&& npm install -g pnpm \
|
| 13 |
+
&& curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
# Use existing node user (UID 1000) for HF Spaces compatibility
|
| 17 |
+
USER node
|
| 18 |
+
ENV HOME=/home/node
|
| 19 |
+
|
| 20 |
+
WORKDIR $HOME/app
|
| 21 |
+
|
| 22 |
+
# Copy entire monorepo
|
| 23 |
+
COPY --chown=node . .
|
| 24 |
+
|
| 25 |
+
# Install Node dependencies
|
| 26 |
+
RUN pnpm install --frozen-lockfile
|
| 27 |
+
|
| 28 |
+
# Build MCP server and Agent (needed for API)
|
| 29 |
+
RUN pnpm --filter @eu-ai-act/mcp-server build
|
| 30 |
+
RUN pnpm --filter @eu-ai-act/agent build
|
| 31 |
+
|
| 32 |
+
# Create Python venv and install dependencies
|
| 33 |
+
RUN uv venv $HOME/venv && \
|
| 34 |
+
. $HOME/venv/bin/activate && \
|
| 35 |
+
uv pip install --no-cache -r apps/eu-ai-act-agent/requirements.txt
|
| 36 |
+
|
| 37 |
+
# Environment
|
| 38 |
+
ENV NODE_ENV=production \
|
| 39 |
+
PORT=3001 \
|
| 40 |
+
API_URL=http://localhost:3001 \
|
| 41 |
+
PUBLIC_URL=https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space \
|
| 42 |
+
CHATGPT_APP_SERVER_NAME=0.0.0.0 \
|
| 43 |
+
CHATGPT_APP_SERVER_PORT=7860 \
|
| 44 |
+
PATH=/home/node/venv/bin:$PATH \
|
| 45 |
+
VIRTUAL_ENV=/home/node/venv \
|
| 46 |
+
MCP_SERVER_PATH=/home/node/app/packages/eu-ai-act-mcp/dist/index.js
|
| 47 |
+
|
| 48 |
+
WORKDIR $HOME/app/apps/eu-ai-act-agent
|
| 49 |
+
|
| 50 |
+
EXPOSE 7860
|
| 51 |
+
|
| 52 |
+
# Start API server + ChatGPT MCP App on port 7860
|
| 53 |
+
# MCP URL will be: PUBLIC_URL/gradio_api/mcp/
|
| 54 |
+
CMD node dist/server.js & \
|
| 55 |
+
sleep 2 && \
|
| 56 |
+
python src/chatgpt_app.py
|
| 57 |
+
|
README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: EU AI Act - ChatGPT MCP Server by legitima.ai
|
| 3 |
+
emoji: ⚖️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
tags:
|
| 9 |
+
- building-mcp-track-enterprise
|
| 10 |
+
- mcp-in-action-track-enterprise
|
| 11 |
+
short_description: MCP Server for ChatGPT Apps - EU AI Act Compliance Tools
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# 🇪🇺 EU AI Act - ChatGPT MCP Server by [legitima.ai](https://legitima.ai/mcp-hackathon) powered by [decode](https://decode.gr/en)
|
| 15 |
+
|
| 16 |
+
<div align="center">
|
| 17 |
+
<img src="https://www.legitima.ai/mcp-hackathon.png" alt="Gradio MCP Hackathon - EU AI Act Compliance" width="800"/>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
This is the **MCP Server** for integrating EU AI Act compliance tools with **ChatGPT Desktop**.
|
| 21 |
+
|
| 22 |
+
## 🔗 MCP URL
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/gradio_api/mcp/
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## 📖 How to Use in ChatGPT
|
| 29 |
+
|
| 30 |
+
1. **Enable Developer Mode** in ChatGPT: Settings → Apps & Connectors → Advanced settings
|
| 31 |
+
2. **Create a Connector** with the MCP URL above (choose "No authentication")
|
| 32 |
+
3. **Chat with ChatGPT** using `@eu-ai-act` to access the tools
|
| 33 |
+
|
| 34 |
+
## 🔧 Available MCP Tools
|
| 35 |
+
|
| 36 |
+
| Tool | Description |
|
| 37 |
+
|------|-------------|
|
| 38 |
+
| `discover_organization` | Research and profile an organization for compliance |
|
| 39 |
+
| `discover_ai_services` | Discover and classify AI systems by risk level |
|
| 40 |
+
| `assess_compliance` | Generate compliance assessment and documentation |
|
| 41 |
+
|
| 42 |
+
## 🤖 Main Agent UI
|
| 43 |
+
|
| 44 |
+
For the full interactive chat experience, visit:
|
| 45 |
+
**[EU AI Act Compliance Agent](https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance-agent)**
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
Built for the **MCP 1st Birthday Hackathon** 🎂
|
| 50 |
+
|
| 51 |
+
**🔗 Demo & Showcase:** [www.legitima.ai/mcp-hackathon](https://www.legitima.ai/mcp-hackathon)
|
| 52 |
+
**📹 Video:** [Guiddes](https://app.guidde.com/share/playlists/2wXbDrSm2YY7YnWMJbftuu?origin=wywDANMIvNhPu9kYVOXCPpdFcya2)
|
| 53 |
+
**📱 Social Media:** [LinkedIn Post 1](https://www.linkedin.com/posts/iordanis-sarafidis_mcp-1st-birthday-mcp-1st-birthday-activity-7400132272282144768-ZIir?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs)
|
| 54 |
+
|
| 55 |
+
[LinkedIn Post 2](https://www.linkedin.com/posts/billdrosatos_mcp-1st-birthday-mcp-1st-birthday-activity-7400135422502252544-C5BS?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs)
|
RUN_LOCAL.sh
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# EU AI Act MCP Server - Local Testing Script
|
| 4 |
+
# This script builds and tests the MCP server
|
| 5 |
+
|
| 6 |
+
set -e
|
| 7 |
+
|
| 8 |
+
echo "🚀 EU AI Act MCP Server - Local Testing"
|
| 9 |
+
echo "========================================"
|
| 10 |
+
echo ""
|
| 11 |
+
|
| 12 |
+
# Colors for output
|
| 13 |
+
GREEN='\033[0;32m'
|
| 14 |
+
BLUE='\033[0;34m'
|
| 15 |
+
YELLOW='\033[1;33m'
|
| 16 |
+
NC='\033[0m' # No Color
|
| 17 |
+
|
| 18 |
+
# Check if we're in the right directory
|
| 19 |
+
if [ ! -f "package.json" ]; then
|
| 20 |
+
echo "❌ Error: Please run this script from the project root directory"
|
| 21 |
+
exit 1
|
| 22 |
+
fi
|
| 23 |
+
|
| 24 |
+
# Step 1: Install dependencies
|
| 25 |
+
echo -e "${BLUE}Step 1: Installing dependencies...${NC}"
|
| 26 |
+
pnpm install --filter @eu-ai-act/mcp-server --filter @eu-ai-act/test-agent
|
| 27 |
+
echo -e "${GREEN}✅ Dependencies installed${NC}"
|
| 28 |
+
echo ""
|
| 29 |
+
|
| 30 |
+
# Step 2: Build MCP server
|
| 31 |
+
echo -e "${BLUE}Step 2: Building MCP server...${NC}"
|
| 32 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 33 |
+
echo -e "${GREEN}✅ MCP server built successfully${NC}"
|
| 34 |
+
echo ""
|
| 35 |
+
|
| 36 |
+
# Step 3: Run tests
|
| 37 |
+
echo -e "${BLUE}Step 3: Running test agent...${NC}"
|
| 38 |
+
echo ""
|
| 39 |
+
pnpm --filter @eu-ai-act/test-agent dev
|
| 40 |
+
echo ""
|
| 41 |
+
|
| 42 |
+
# Success message
|
| 43 |
+
echo ""
|
| 44 |
+
echo -e "${GREEN}========================================"
|
| 45 |
+
echo "✅ All tests completed successfully!"
|
| 46 |
+
echo "========================================${NC}"
|
| 47 |
+
echo ""
|
| 48 |
+
echo -e "${YELLOW}Next Steps:${NC}"
|
| 49 |
+
echo "1. Configure Claude Desktop (see QUICKSTART.md)"
|
| 50 |
+
echo "2. Read packages/eu-ai-act-mcp/README.md for API docs"
|
| 51 |
+
echo "3. See IMPLEMENTATION.md for architecture details"
|
| 52 |
+
echo ""
|
| 53 |
+
echo "Your MCP server is ready to use! 🎉"
|
| 54 |
+
|
SUBMISSION.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎂 MCP 1st Birthday Hackathon Submission
|
| 2 |
+
|
| 3 |
+
## EU AI Act Compliance Agent
|
| 4 |
+
|
| 5 |
+
### 🔗 Links
|
| 6 |
+
|
| 7 |
+
| Resource | URL |
|
| 8 |
+
| --------------- | ------------------------------------------------------------------------------- |
|
| 9 |
+
| **Live Demo** | [HF Space](https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance) |
|
| 10 |
+
| **GitHub** | [Repository](https://github.com/your-org/eu-ai-act-compliance) |
|
| 11 |
+
| **Social Post** | [Twitter/X Post](#) |
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 📋 Submission Details
|
| 16 |
+
|
| 17 |
+
### Tracks
|
| 18 |
+
|
| 19 |
+
- ✅ **Track 1: Building MCP** - MCP Server with compliance tools
|
| 20 |
+
- ✅ **Track 2: MCP in Action** - Agentic application with Gradio UI
|
| 21 |
+
|
| 22 |
+
### Tags
|
| 23 |
+
|
| 24 |
+
```
|
| 25 |
+
mcp, agents, eu-ai-act, compliance, legal-tech, gradio,
|
| 26 |
+
track-1-mcp-servers, track-2-agentic-applications
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## 🎯 Project Overview
|
| 32 |
+
|
| 33 |
+
The **EU AI Act Compliance Agent** helps organizations navigate the European Union's AI Act (Regulation 2024/1689) — the world's first comprehensive AI regulation framework.
|
| 34 |
+
|
| 35 |
+
### The Problem
|
| 36 |
+
|
| 37 |
+
- 📋 **Complex Classification** — AI systems must be classified by risk level
|
| 38 |
+
- 📝 **Documentation** — Extensive technical documentation required
|
| 39 |
+
- 🔍 **Transparency** — Clear disclosure obligations
|
| 40 |
+
- ⏰ **Tight Deadlines** — Phased implementation starting 2025
|
| 41 |
+
|
| 42 |
+
### Our Solution
|
| 43 |
+
|
| 44 |
+
Three MCP tools + AI Agent for automated compliance:
|
| 45 |
+
|
| 46 |
+
| Tool | Purpose |
|
| 47 |
+
| ----------------------- | ------------------------------- |
|
| 48 |
+
| `discover_organization` | Research & profile organization |
|
| 49 |
+
| `discover_ai_services` | Find & classify AI systems |
|
| 50 |
+
| `assess_compliance` | Generate compliance assessment |
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## 🛠️ Tech Stack
|
| 55 |
+
|
| 56 |
+
- **Gradio 6** — Interactive web UI
|
| 57 |
+
- **xAI Grok** — AI reasoning and tool calling
|
| 58 |
+
- **Tavily AI** — Web research
|
| 59 |
+
- **Model Context Protocol** — Tool integration
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## ✨ Key Features
|
| 64 |
+
|
| 65 |
+
### Track 1: MCP Server
|
| 66 |
+
|
| 67 |
+
1. **Organization Discovery** — Real-time web research using Tavily
|
| 68 |
+
2. **AI System Classification** — Risk tiers per EU AI Act Annex III
|
| 69 |
+
3. **Compliance Assessment** — Gap analysis with documentation templates
|
| 70 |
+
|
| 71 |
+
### Track 2: AI Agent
|
| 72 |
+
|
| 73 |
+
1. **Conversational Interface** — Natural language interaction
|
| 74 |
+
2. **Tool Orchestration** — Intelligent multi-tool workflows
|
| 75 |
+
3. **Document Generation** — Ready-to-use compliance templates
|
| 76 |
+
4. **Real-time Streaming** — Progressive response display
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## 📊 Demo Workflow
|
| 81 |
+
|
| 82 |
+
```
|
| 83 |
+
User: "Analyze OpenAI's EU AI Act compliance"
|
| 84 |
+
|
| 85 |
+
Agent:
|
| 86 |
+
├── 🔧 discover_organization("OpenAI")
|
| 87 |
+
│ └── ✅ Found: AI company, Expert maturity, Provider role
|
| 88 |
+
├── 🔧 discover_ai_services(orgContext)
|
| 89 |
+
│ └── ✅ Found: 5 AI systems (2 high-risk, 3 limited-risk)
|
| 90 |
+
├── 🔧 assess_compliance(orgContext, servicesContext)
|
| 91 |
+
│ └── ✅ Score: 65/100, 3 critical gaps identified
|
| 92 |
+
└── 📝 Generated compliance report with recommendations
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
---
|
| 96 |
+
|
| 97 |
+
## 🏆 Why This Matters
|
| 98 |
+
|
| 99 |
+
1. **Real Regulation** — EU AI Act is live, affecting millions of organizations
|
| 100 |
+
2. **Practical Tools** — Automates tedious compliance workflows
|
| 101 |
+
3. **Educational** — Helps understand complex legal requirements
|
| 102 |
+
4. **Actionable** — Generates usable documentation templates
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
## 👥 Team
|
| 107 |
+
|
| 108 |
+
**Team EU Compliance**
|
| 109 |
+
|
| 110 |
+
Building the future of AI governance 🇪🇺
|
| 111 |
+
|
| 112 |
+
****
|
| 113 |
+
<div align="center">
|
| 114 |
+
|
| 115 |
+
**Built with ❤️ for the MCP 1st Birthday Hackathon**
|
| 116 |
+
|
| 117 |
+
🎂 Happy 1st Birthday, MCP! 🎂
|
| 118 |
+
|
| 119 |
+
</div>
|
| 120 |
+
|
apps/eu-ai-act-agent/.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
.pnp
|
| 4 |
+
.pnp.js
|
| 5 |
+
|
| 6 |
+
# Build outputs
|
| 7 |
+
dist/
|
| 8 |
+
build/
|
| 9 |
+
*.tsbuildinfo
|
| 10 |
+
|
| 11 |
+
# Environment
|
| 12 |
+
.env
|
| 13 |
+
.env.local
|
| 14 |
+
.env.*.local
|
| 15 |
+
|
| 16 |
+
# Python
|
| 17 |
+
__pycache__/
|
| 18 |
+
*.py[cod]
|
| 19 |
+
*$py.class
|
| 20 |
+
*.so
|
| 21 |
+
.Python
|
| 22 |
+
venv/
|
| 23 |
+
env/
|
| 24 |
+
ENV/
|
| 25 |
+
|
| 26 |
+
# IDEs
|
| 27 |
+
.vscode/
|
| 28 |
+
.idea/
|
| 29 |
+
*.swp
|
| 30 |
+
*.swo
|
| 31 |
+
*~
|
| 32 |
+
|
| 33 |
+
# OS
|
| 34 |
+
.DS_Store
|
| 35 |
+
Thumbs.db
|
| 36 |
+
|
| 37 |
+
# Logs
|
| 38 |
+
logs/
|
| 39 |
+
*.log
|
| 40 |
+
npm-debug.log*
|
| 41 |
+
yarn-debug.log*
|
| 42 |
+
yarn-error.log*
|
| 43 |
+
|
| 44 |
+
# Testing
|
| 45 |
+
coverage/
|
| 46 |
+
.nyc_output/
|
| 47 |
+
|
| 48 |
+
# Misc
|
| 49 |
+
.cache/
|
| 50 |
+
temp/
|
| 51 |
+
tmp/
|
| 52 |
+
|
apps/eu-ai-act-agent/.python-version
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
3.10
|
| 2 |
+
|
apps/eu-ai-act-agent/API.md
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔌 API Reference
|
| 2 |
+
|
| 3 |
+
Complete reference for the EU AI Act Compliance Agent API.
|
| 4 |
+
|
| 5 |
+
## Base URL
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
http://localhost:3001
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
## Authentication
|
| 12 |
+
|
| 13 |
+
Currently no authentication required for local development. Add API key authentication for production deployment.
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## Endpoints
|
| 18 |
+
|
| 19 |
+
### 1. Health Check
|
| 20 |
+
|
| 21 |
+
Check if the API server is running and healthy.
|
| 22 |
+
|
| 23 |
+
**Endpoint**: `GET /health`
|
| 24 |
+
|
| 25 |
+
**Response**:
|
| 26 |
+
```json
|
| 27 |
+
{
|
| 28 |
+
"status": "ok",
|
| 29 |
+
"service": "EU AI Act Compliance Agent",
|
| 30 |
+
"version": "0.1.0"
|
| 31 |
+
}
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
**Example**:
|
| 35 |
+
```bash
|
| 36 |
+
curl http://localhost:3001/health
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
---
|
| 40 |
+
|
| 41 |
+
### 2. Chat Endpoint
|
| 42 |
+
|
| 43 |
+
Send a message to the AI agent and receive a streaming response.
|
| 44 |
+
|
| 45 |
+
**Endpoint**: `POST /api/chat`
|
| 46 |
+
|
| 47 |
+
**Content-Type**: `application/json`
|
| 48 |
+
|
| 49 |
+
**Request Body**:
|
| 50 |
+
```json
|
| 51 |
+
{
|
| 52 |
+
"message": "What is the EU AI Act?",
|
| 53 |
+
"history": [
|
| 54 |
+
{
|
| 55 |
+
"role": "user",
|
| 56 |
+
"content": "Previous user message"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"role": "assistant",
|
| 60 |
+
"content": "Previous assistant response"
|
| 61 |
+
}
|
| 62 |
+
]
|
| 63 |
+
}
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
**Parameters**:
|
| 67 |
+
- `message` (string, required): The user's input message
|
| 68 |
+
- `history` (array, optional): Conversation history for context
|
| 69 |
+
|
| 70 |
+
**Response Format**: Server-Sent Events (SSE) / Event Stream
|
| 71 |
+
|
| 72 |
+
**Response Events**:
|
| 73 |
+
|
| 74 |
+
1. **Text Chunk**:
|
| 75 |
+
```json
|
| 76 |
+
{
|
| 77 |
+
"type": "text",
|
| 78 |
+
"content": "The EU AI Act is..."
|
| 79 |
+
}
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
2. **Tool Call** (when agent uses a tool):
|
| 83 |
+
```json
|
| 84 |
+
{
|
| 85 |
+
"type": "tool_call",
|
| 86 |
+
"tool": "discover_organization",
|
| 87 |
+
"args": {...}
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
3. **Tool Result**:
|
| 92 |
+
```json
|
| 93 |
+
{
|
| 94 |
+
"type": "tool_result",
|
| 95 |
+
"tool": "discover_organization",
|
| 96 |
+
"result": {...}
|
| 97 |
+
}
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
4. **Done**:
|
| 101 |
+
```json
|
| 102 |
+
{
|
| 103 |
+
"type": "done"
|
| 104 |
+
}
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
5. **Error**:
|
| 108 |
+
```json
|
| 109 |
+
{
|
| 110 |
+
"type": "error",
|
| 111 |
+
"error": "Error message"
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
**Example**:
|
| 116 |
+
```bash
|
| 117 |
+
curl -X POST http://localhost:3001/api/chat \
|
| 118 |
+
-H "Content-Type: application/json" \
|
| 119 |
+
-d '{
|
| 120 |
+
"message": "What is the EU AI Act?",
|
| 121 |
+
"history": []
|
| 122 |
+
}'
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
**JavaScript Example**:
|
| 126 |
+
```javascript
|
| 127 |
+
const response = await fetch('http://localhost:3001/api/chat', {
|
| 128 |
+
method: 'POST',
|
| 129 |
+
headers: {
|
| 130 |
+
'Content-Type': 'application/json',
|
| 131 |
+
},
|
| 132 |
+
body: JSON.stringify({
|
| 133 |
+
message: 'What is the EU AI Act?',
|
| 134 |
+
history: []
|
| 135 |
+
})
|
| 136 |
+
});
|
| 137 |
+
|
| 138 |
+
// Read the streaming response
|
| 139 |
+
const reader = response.body.getReader();
|
| 140 |
+
const decoder = new TextDecoder();
|
| 141 |
+
|
| 142 |
+
while (true) {
|
| 143 |
+
const { done, value } = await reader.read();
|
| 144 |
+
if (done) break;
|
| 145 |
+
|
| 146 |
+
const chunk = decoder.decode(value);
|
| 147 |
+
const lines = chunk.split('\n');
|
| 148 |
+
|
| 149 |
+
for (const line of lines) {
|
| 150 |
+
if (line.startsWith('data: ')) {
|
| 151 |
+
const data = JSON.parse(line.substring(6));
|
| 152 |
+
console.log(data);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
**Python Example**:
|
| 159 |
+
```python
|
| 160 |
+
import requests
|
| 161 |
+
import json
|
| 162 |
+
|
| 163 |
+
response = requests.post(
|
| 164 |
+
'http://localhost:3001/api/chat',
|
| 165 |
+
json={
|
| 166 |
+
'message': 'What is the EU AI Act?',
|
| 167 |
+
'history': []
|
| 168 |
+
},
|
| 169 |
+
stream=True
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
for line in response.iter_lines():
|
| 173 |
+
if line:
|
| 174 |
+
line_str = line.decode('utf-8')
|
| 175 |
+
if line_str.startswith('data: '):
|
| 176 |
+
data = json.loads(line_str[6:])
|
| 177 |
+
print(data)
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
### 3. Tools Endpoint
|
| 183 |
+
|
| 184 |
+
Get a list of available MCP tools.
|
| 185 |
+
|
| 186 |
+
**Endpoint**: `GET /api/tools`
|
| 187 |
+
|
| 188 |
+
**Response**:
|
| 189 |
+
```json
|
| 190 |
+
{
|
| 191 |
+
"tools": [
|
| 192 |
+
{
|
| 193 |
+
"name": "discover_organization",
|
| 194 |
+
"description": "Discover and profile an organization for EU AI Act compliance..."
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"name": "discover_ai_services",
|
| 198 |
+
"description": "Discover and classify AI systems within an organization..."
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"name": "assess_compliance",
|
| 202 |
+
"description": "Assess EU AI Act compliance and generate documentation..."
|
| 203 |
+
}
|
| 204 |
+
]
|
| 205 |
+
}
|
| 206 |
+
```
|
| 207 |
+
|
| 208 |
+
**Example**:
|
| 209 |
+
```bash
|
| 210 |
+
curl http://localhost:3001/api/tools
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## Message Types
|
| 216 |
+
|
| 217 |
+
### User Message
|
| 218 |
+
```typescript
|
| 219 |
+
interface UserMessage {
|
| 220 |
+
role: "user";
|
| 221 |
+
content: string;
|
| 222 |
+
}
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
### Assistant Message
|
| 226 |
+
```typescript
|
| 227 |
+
interface AssistantMessage {
|
| 228 |
+
role: "assistant";
|
| 229 |
+
content: string;
|
| 230 |
+
}
|
| 231 |
+
```
|
| 232 |
+
|
| 233 |
+
### System Message (internal)
|
| 234 |
+
```typescript
|
| 235 |
+
interface SystemMessage {
|
| 236 |
+
role: "system";
|
| 237 |
+
content: string;
|
| 238 |
+
}
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## Tool Schemas
|
| 244 |
+
|
| 245 |
+
### discover_organization
|
| 246 |
+
|
| 247 |
+
**Description**: Research and profile an organization for EU AI Act compliance.
|
| 248 |
+
|
| 249 |
+
**Parameters**:
|
| 250 |
+
```typescript
|
| 251 |
+
{
|
| 252 |
+
organizationName: string; // Required
|
| 253 |
+
domain?: string; // Optional, auto-discovered
|
| 254 |
+
context?: string; // Optional, additional context
|
| 255 |
+
}
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
**Returns**:
|
| 259 |
+
```typescript
|
| 260 |
+
{
|
| 261 |
+
organization: {
|
| 262 |
+
name: string;
|
| 263 |
+
sector: string;
|
| 264 |
+
size: "SME" | "Large Enterprise" | "Public Body" | "Micro Enterprise";
|
| 265 |
+
aiMaturityLevel: "Nascent" | "Developing" | "Advanced" | "Expert";
|
| 266 |
+
// ... more fields
|
| 267 |
+
},
|
| 268 |
+
regulatoryContext: {
|
| 269 |
+
applicableFrameworks: string[];
|
| 270 |
+
complianceDeadlines: Array<{...}>;
|
| 271 |
+
// ... more fields
|
| 272 |
+
},
|
| 273 |
+
metadata: {
|
| 274 |
+
completenessScore: number; // 0-100
|
| 275 |
+
// ... more fields
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
### discover_ai_services
|
| 281 |
+
|
| 282 |
+
**Description**: Discover and classify AI systems within an organization.
|
| 283 |
+
|
| 284 |
+
**Parameters**:
|
| 285 |
+
```typescript
|
| 286 |
+
{
|
| 287 |
+
organizationContext?: any; // Optional, from discover_organization
|
| 288 |
+
systemNames?: string[]; // Optional, specific systems to discover
|
| 289 |
+
scope?: string; // Optional: 'all', 'high-risk-only', 'production-only'
|
| 290 |
+
context?: string; // Optional, additional context
|
| 291 |
+
}
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
**Returns**:
|
| 295 |
+
```typescript
|
| 296 |
+
{
|
| 297 |
+
systems: Array<{
|
| 298 |
+
system: {
|
| 299 |
+
name: string;
|
| 300 |
+
description: string;
|
| 301 |
+
status: "Development" | "Testing" | "Production" | "Deprecated";
|
| 302 |
+
// ... more fields
|
| 303 |
+
},
|
| 304 |
+
riskClassification: {
|
| 305 |
+
category: "Unacceptable" | "High" | "Limited" | "Minimal";
|
| 306 |
+
riskScore: number; // 0-100
|
| 307 |
+
annexIIICategory?: string;
|
| 308 |
+
// ... more fields
|
| 309 |
+
},
|
| 310 |
+
complianceStatus: {
|
| 311 |
+
// ... compliance fields
|
| 312 |
+
}
|
| 313 |
+
}>,
|
| 314 |
+
riskSummary: {
|
| 315 |
+
highRiskCount: number;
|
| 316 |
+
limitedRiskCount: number;
|
| 317 |
+
// ... more counts
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
```
|
| 321 |
+
|
| 322 |
+
### assess_compliance
|
| 323 |
+
|
| 324 |
+
**Description**: Assess compliance and generate documentation.
|
| 325 |
+
|
| 326 |
+
**Parameters**:
|
| 327 |
+
```typescript
|
| 328 |
+
{
|
| 329 |
+
organizationContext?: any; // Optional, from discover_organization
|
| 330 |
+
aiServicesContext?: any; // Optional, from discover_ai_services
|
| 331 |
+
focusAreas?: string[]; // Optional, specific areas to focus on
|
| 332 |
+
generateDocumentation?: boolean; // Optional, default: true
|
| 333 |
+
}
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
**Returns**:
|
| 337 |
+
```typescript
|
| 338 |
+
{
|
| 339 |
+
assessment: {
|
| 340 |
+
overallScore: number; // 0-100
|
| 341 |
+
gaps: Array<{
|
| 342 |
+
area: string;
|
| 343 |
+
severity: "Critical" | "High" | "Medium" | "Low";
|
| 344 |
+
article: string;
|
| 345 |
+
description: string;
|
| 346 |
+
recommendation: string;
|
| 347 |
+
}>,
|
| 348 |
+
recommendations: Array<{...}>
|
| 349 |
+
},
|
| 350 |
+
documentation?: {
|
| 351 |
+
riskManagementTemplate: string; // Markdown
|
| 352 |
+
technicalDocumentation: string; // Markdown
|
| 353 |
+
conformityAssessment: string; // Markdown
|
| 354 |
+
transparencyNotice: string; // Markdown
|
| 355 |
+
// ... more templates
|
| 356 |
+
},
|
| 357 |
+
reasoning: string; // Chain-of-thought explanation
|
| 358 |
+
}
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
---
|
| 362 |
+
|
| 363 |
+
## Error Handling
|
| 364 |
+
|
| 365 |
+
### Common Error Responses
|
| 366 |
+
|
| 367 |
+
**400 Bad Request**:
|
| 368 |
+
```json
|
| 369 |
+
{
|
| 370 |
+
"error": "Message is required"
|
| 371 |
+
}
|
| 372 |
+
```
|
| 373 |
+
|
| 374 |
+
**500 Internal Server Error**:
|
| 375 |
+
```json
|
| 376 |
+
{
|
| 377 |
+
"error": "Internal server error",
|
| 378 |
+
"message": "Detailed error message"
|
| 379 |
+
}
|
| 380 |
+
```
|
| 381 |
+
|
| 382 |
+
### Error Types
|
| 383 |
+
|
| 384 |
+
1. **Missing Parameters**: 400 error when required parameters are not provided
|
| 385 |
+
2. **API Connection**: 500 error if OpenAI API is unreachable
|
| 386 |
+
3. **Rate Limiting**: 429 error if rate limits are exceeded
|
| 387 |
+
4. **Tool Execution**: 500 error if MCP tools fail
|
| 388 |
+
|
| 389 |
+
---
|
| 390 |
+
|
| 391 |
+
## Rate Limiting
|
| 392 |
+
|
| 393 |
+
Currently no rate limiting implemented. For production, consider adding:
|
| 394 |
+
|
| 395 |
+
```javascript
|
| 396 |
+
import rateLimit from 'express-rate-limit';
|
| 397 |
+
|
| 398 |
+
const limiter = rateLimit({
|
| 399 |
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
| 400 |
+
max: 100 // limit each IP to 100 requests per windowMs
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
app.use('/api/', limiter);
|
| 404 |
+
```
|
| 405 |
+
|
| 406 |
+
---
|
| 407 |
+
|
| 408 |
+
## CORS Configuration
|
| 409 |
+
|
| 410 |
+
**Current Setup**:
|
| 411 |
+
```javascript
|
| 412 |
+
cors({
|
| 413 |
+
origin: ["http://localhost:7860", "http://127.0.0.1:7860"],
|
| 414 |
+
credentials: true,
|
| 415 |
+
})
|
| 416 |
+
```
|
| 417 |
+
|
| 418 |
+
**For Production**: Configure specific allowed origins:
|
| 419 |
+
```javascript
|
| 420 |
+
cors({
|
| 421 |
+
origin: ["https://your-gradio-app.com"],
|
| 422 |
+
credentials: true,
|
| 423 |
+
})
|
| 424 |
+
```
|
| 425 |
+
|
| 426 |
+
---
|
| 427 |
+
|
| 428 |
+
## WebSocket Support
|
| 429 |
+
|
| 430 |
+
Currently uses HTTP streaming (SSE). For WebSocket support, add:
|
| 431 |
+
|
| 432 |
+
```javascript
|
| 433 |
+
import { WebSocketServer } from 'ws';
|
| 434 |
+
|
| 435 |
+
const wss = new WebSocketServer({ server });
|
| 436 |
+
|
| 437 |
+
wss.on('connection', (ws) => {
|
| 438 |
+
ws.on('message', async (message) => {
|
| 439 |
+
const { content, history } = JSON.parse(message);
|
| 440 |
+
// Stream response via WebSocket
|
| 441 |
+
for await (const chunk of result.textStream) {
|
| 442 |
+
ws.send(JSON.stringify({ type: 'text', content: chunk }));
|
| 443 |
+
}
|
| 444 |
+
});
|
| 445 |
+
});
|
| 446 |
+
```
|
| 447 |
+
|
| 448 |
+
---
|
| 449 |
+
|
| 450 |
+
## Environment Variables
|
| 451 |
+
|
| 452 |
+
Required for API server:
|
| 453 |
+
|
| 454 |
+
```bash
|
| 455 |
+
# Required
|
| 456 |
+
OPENAI_API_KEY=sk-your-openai-api-key
|
| 457 |
+
|
| 458 |
+
# Optional
|
| 459 |
+
TAVILY_API_KEY=tvly-your-tavily-api-key
|
| 460 |
+
PORT=3001
|
| 461 |
+
|
| 462 |
+
# For production
|
| 463 |
+
NODE_ENV=production
|
| 464 |
+
API_KEY=your-api-authentication-key
|
| 465 |
+
ALLOWED_ORIGINS=https://your-app.com
|
| 466 |
+
```
|
| 467 |
+
|
| 468 |
+
---
|
| 469 |
+
|
| 470 |
+
## Testing the API
|
| 471 |
+
|
| 472 |
+
### Using curl
|
| 473 |
+
|
| 474 |
+
**Health check**:
|
| 475 |
+
```bash
|
| 476 |
+
curl http://localhost:3001/health
|
| 477 |
+
```
|
| 478 |
+
|
| 479 |
+
**Simple chat**:
|
| 480 |
+
```bash
|
| 481 |
+
curl -X POST http://localhost:3001/api/chat \
|
| 482 |
+
-H "Content-Type: application/json" \
|
| 483 |
+
-d '{"message":"What is the EU AI Act?"}'
|
| 484 |
+
```
|
| 485 |
+
|
| 486 |
+
**Chat with history**:
|
| 487 |
+
```bash
|
| 488 |
+
curl -X POST http://localhost:3001/api/chat \
|
| 489 |
+
-H "Content-Type: application/json" \
|
| 490 |
+
-d '{
|
| 491 |
+
"message": "Tell me more",
|
| 492 |
+
"history": [
|
| 493 |
+
{"role": "user", "content": "What is the EU AI Act?"},
|
| 494 |
+
{"role": "assistant", "content": "The EU AI Act is..."}
|
| 495 |
+
]
|
| 496 |
+
}'
|
| 497 |
+
```
|
| 498 |
+
|
| 499 |
+
### Using Postman
|
| 500 |
+
|
| 501 |
+
1. Create a new POST request to `http://localhost:3001/api/chat`
|
| 502 |
+
2. Set Headers: `Content-Type: application/json`
|
| 503 |
+
3. Set Body (raw JSON):
|
| 504 |
+
```json
|
| 505 |
+
{
|
| 506 |
+
"message": "What is the EU AI Act?",
|
| 507 |
+
"history": []
|
| 508 |
+
}
|
| 509 |
+
```
|
| 510 |
+
4. Send and view streaming response
|
| 511 |
+
|
| 512 |
+
---
|
| 513 |
+
|
| 514 |
+
## Monitoring and Logging
|
| 515 |
+
|
| 516 |
+
**Console Logs**:
|
| 517 |
+
- All requests are logged to console
|
| 518 |
+
- Tool executions are logged
|
| 519 |
+
- Errors are logged with stack traces
|
| 520 |
+
|
| 521 |
+
**Add Structured Logging**:
|
| 522 |
+
```javascript
|
| 523 |
+
import winston from 'winston';
|
| 524 |
+
|
| 525 |
+
const logger = winston.createLogger({
|
| 526 |
+
level: 'info',
|
| 527 |
+
format: winston.format.json(),
|
| 528 |
+
transports: [
|
| 529 |
+
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
| 530 |
+
new winston.transports.File({ filename: 'combined.log' })
|
| 531 |
+
]
|
| 532 |
+
});
|
| 533 |
+
|
| 534 |
+
app.use((req, res, next) => {
|
| 535 |
+
logger.info(`${req.method} ${req.url}`);
|
| 536 |
+
next();
|
| 537 |
+
});
|
| 538 |
+
```
|
| 539 |
+
|
| 540 |
+
---
|
| 541 |
+
|
| 542 |
+
## Security Best Practices
|
| 543 |
+
|
| 544 |
+
1. **Add Authentication**: Use API keys or JWT tokens
|
| 545 |
+
2. **Rate Limiting**: Prevent abuse
|
| 546 |
+
3. **Input Validation**: Sanitize all inputs
|
| 547 |
+
4. **HTTPS**: Use TLS in production
|
| 548 |
+
5. **CORS**: Restrict origins
|
| 549 |
+
6. **Secrets**: Never commit API keys
|
| 550 |
+
7. **Monitoring**: Log all requests and errors
|
| 551 |
+
|
| 552 |
+
---
|
| 553 |
+
|
| 554 |
+
## Performance Optimization
|
| 555 |
+
|
| 556 |
+
1. **Caching**: Cache organization/system discoveries
|
| 557 |
+
```javascript
|
| 558 |
+
import NodeCache from 'node-cache';
|
| 559 |
+
const cache = new NodeCache({ stdTTL: 3600 });
|
| 560 |
+
```
|
| 561 |
+
|
| 562 |
+
2. **Compression**: Compress responses
|
| 563 |
+
```javascript
|
| 564 |
+
import compression from 'compression';
|
| 565 |
+
app.use(compression());
|
| 566 |
+
```
|
| 567 |
+
|
| 568 |
+
3. **Load Balancing**: Use multiple instances
|
| 569 |
+
4. **Queuing**: Implement job queue for long tasks
|
| 570 |
+
|
| 571 |
+
---
|
| 572 |
+
|
| 573 |
+
## Support
|
| 574 |
+
|
| 575 |
+
- 📖 Full documentation: See README.md
|
| 576 |
+
- 💬 Issues: GitHub Issues
|
| 577 |
+
- 🐛 Bug reports: Include API logs and request details
|
| 578 |
+
|
| 579 |
+
|
apps/eu-ai-act-agent/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🏗️ Architecture Documentation
|
| 2 |
+
|
| 3 |
+
Detailed technical architecture of the EU AI Act Compliance Agent.
|
| 4 |
+
|
| 5 |
+
## System Overview
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 9 |
+
│ CLIENT LAYER │
|
| 10 |
+
│ ┌───────────────────────────────────────────────────────┐ │
|
| 11 |
+
│ │ Gradio Web Interface (Python) │ │
|
| 12 |
+
│ │ - Chat UI with history │ │
|
| 13 |
+
│ │ - Real-time streaming display │ │
|
| 14 |
+
│ │ - Document export features │ │
|
| 15 |
+
│ │ - Status monitoring │ │
|
| 16 |
+
│ └────────────────────┬──────────────────────────────────┘ │
|
| 17 |
+
└─────────────────────────┼──────────────────────────────────┘
|
| 18 |
+
│ HTTP/REST (SSE)
|
| 19 |
+
│
|
| 20 |
+
┌─────────────────────────┼──────────────────────────────────┐
|
| 21 |
+
│ API LAYER │
|
| 22 |
+
│ ┌────────────────────┴──────────────────────────────────┐ │
|
| 23 |
+
│ │ Express.js Server (Node.js/TypeScript) │ │
|
| 24 |
+
│ │ - RESTful endpoints │ │
|
| 25 |
+
│ │ - Server-Sent Events (SSE) for streaming │ │
|
| 26 |
+
│ │ - CORS configuration │ │
|
| 27 |
+
│ │ - Request validation │ │
|
| 28 |
+
│ └────────────────────┬──────────────────────────────────┘ │
|
| 29 |
+
└─────────────────────────┼──────────────────────────────────┘
|
| 30 |
+
│
|
| 31 |
+
┌─────────────────────────┼──────────────────────────────────┐
|
| 32 |
+
│ AGENT LAYER │
|
| 33 |
+
│ ┌────────────────────┴──────────────────────────────────┐ │
|
| 34 |
+
│ │ Vercel AI SDK v5 Agent │ │
|
| 35 |
+
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
| 36 |
+
│ │ │ Model: OpenAI gpt-5-chat-latest │ │ │
|
| 37 |
+
│ │ │ - Natural language understanding │ │ │
|
| 38 |
+
│ │ │ - Context management (conversation history) │ │ │
|
| 39 |
+
│ │ │ - Tool calling orchestration │ │ │
|
| 40 |
+
│ │ │ - Streaming response generation │ │ │
|
| 41 |
+
│ │ └──────────────────────────────────────────────────┘ │ │
|
| 42 |
+
│ │ │ │
|
| 43 |
+
│ │ ┌──────────────────────────────────────────────────┐ │ │
|
| 44 |
+
│ │ │ System Prompt │ │ │
|
| 45 |
+
│ │ │ - EU AI Act expert persona │ │ │
|
| 46 |
+
│ │ │ - Tool usage guidelines │ │ │
|
| 47 |
+
│ │ │ - Response formatting rules │ │ │
|
| 48 |
+
│ │ └──────────────────────────────────────────────────┘ │ │
|
| 49 |
+
│ └────────────────────┬──────────────────────────────────┘ │
|
| 50 |
+
└─────────────────────────┼──────────────────────────────────┘
|
| 51 |
+
│ Function Calling
|
| 52 |
+
│
|
| 53 |
+
┌───────────────────────���─┼──────────────────────────────────┐
|
| 54 |
+
│ TOOL LAYER │
|
| 55 |
+
│ ┌────────────────────┴──────────────────────────────────┐ │
|
| 56 |
+
│ │ MCP Tool Adapters (Vercel AI SDK tool format) │ │
|
| 57 |
+
│ │ │ │
|
| 58 |
+
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
| 59 |
+
│ │ │ 1. discover_organization │ │ │
|
| 60 |
+
│ │ │ - Tavily web research │ │ │
|
| 61 |
+
│ │ │ - Company profiling │ │ │
|
| 62 |
+
│ │ │ - Regulatory mapping │ │ │
|
| 63 |
+
│ │ └─────────────────────────────────────────────────┘ │ │
|
| 64 |
+
│ │ │ │
|
| 65 |
+
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
| 66 |
+
│ │ │ 2. discover_ai_services │ │ │
|
| 67 |
+
│ │ │ - AI system discovery │ │ │
|
| 68 |
+
│ │ │ - Risk classification │ │ │
|
| 69 |
+
│ │ │ - Compliance status assessment │ │ │
|
| 70 |
+
│ │ └─────────────────────────────────────────────────┘ │ │
|
| 71 |
+
│ │ │ │
|
| 72 |
+
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
| 73 |
+
│ │ │ 3. assess_compliance │ │ │
|
| 74 |
+
│ │ │ - Gap analysis (GPT-4) │ │ │
|
| 75 |
+
│ │ │ - Documentation generation │ │ │
|
| 76 |
+
│ │ │ - Recommendations │ │ │
|
| 77 |
+
│ │ └─────────────────────────────────────────────────┘ │ │
|
| 78 |
+
│ └────────────────────┬──────────────────────────────────┘ │
|
| 79 |
+
└─────────────────────────┼──────────────────────────────────┘
|
| 80 |
+
│
|
| 81 |
+
┌─────────────────────────┼──────────────────────────────────┐
|
| 82 |
+
│ EXTERNAL SERVICES │
|
| 83 |
+
│ ┌────────────────────┴──────────────────────────────────┐ │
|
| 84 |
+
│ │ OpenAI API (gpt-5-chat-latest) │ │
|
| 85 |
+
│ │ - Agent intelligence │ │
|
| 86 |
+
│ │ - Compliance assessment │ │
|
| 87 |
+
│ └───────────────────────────────────────────────────────┘ │
|
| 88 |
+
│ │
|
| 89 |
+
│ ┌───────────────────────────────────────────────────────┐ │
|
| 90 |
+
│ │ Tavily API (Optional) │ │
|
| 91 |
+
│ │ - Company research │ │
|
| 92 |
+
│ │ - Web data extraction │ │
|
| 93 |
+
│ └───────────────────────────────────────────────────────┘ │
|
| 94 |
+
└─────────────────────────────────────────────────────────────┘
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
---
|
| 98 |
+
|
| 99 |
+
## Component Details
|
| 100 |
+
|
| 101 |
+
### 1. Gradio Web Interface
|
| 102 |
+
|
| 103 |
+
**Technology**: Python 3.9+ with Gradio 5.x
|
| 104 |
+
|
| 105 |
+
**Purpose**: Provide user-friendly chat interface for the agent
|
| 106 |
+
|
| 107 |
+
**Key Files**:
|
| 108 |
+
- `src/gradio_app.py` - Main Gradio application
|
| 109 |
+
|
| 110 |
+
**Features**:
|
| 111 |
+
- Chat interface with conversation history
|
| 112 |
+
- Real-time streaming display
|
| 113 |
+
- Status indicator for API connection
|
| 114 |
+
- Example queries
|
| 115 |
+
- Export functionality (planned)
|
| 116 |
+
- Custom EU-themed styling
|
| 117 |
+
|
| 118 |
+
**Communication**: HTTP POST requests to Express API with streaming response handling
|
| 119 |
+
|
| 120 |
+
**Configuration**:
|
| 121 |
+
```python
|
| 122 |
+
demo.launch(
|
| 123 |
+
server_name="0.0.0.0",
|
| 124 |
+
server_port=7860,
|
| 125 |
+
share=False,
|
| 126 |
+
show_error=True,
|
| 127 |
+
)
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
### 2. Express API Server
|
| 133 |
+
|
| 134 |
+
**Technology**: Node.js 18+ with Express.js and TypeScript
|
| 135 |
+
|
| 136 |
+
**Purpose**: REST API layer connecting Gradio to the AI agent
|
| 137 |
+
|
| 138 |
+
**Key Files**:
|
| 139 |
+
- `src/server.ts` - Express server configuration
|
| 140 |
+
- `src/types/index.ts` - TypeScript type definitions
|
| 141 |
+
|
| 142 |
+
**Endpoints**:
|
| 143 |
+
- `GET /health` - Health check
|
| 144 |
+
- `POST /api/chat` - Main chat endpoint (streaming)
|
| 145 |
+
- `GET /api/tools` - List available tools
|
| 146 |
+
|
| 147 |
+
**Features**:
|
| 148 |
+
- CORS configuration for Gradio
|
| 149 |
+
- Server-Sent Events (SSE) for streaming
|
| 150 |
+
- Request validation
|
| 151 |
+
- Error handling
|
| 152 |
+
- Logging
|
| 153 |
+
|
| 154 |
+
**Configuration**:
|
| 155 |
+
```typescript
|
| 156 |
+
const app = express();
|
| 157 |
+
app.use(cors({
|
| 158 |
+
origin: ["http://localhost:7860", "http://127.0.0.1:7860"],
|
| 159 |
+
credentials: true,
|
| 160 |
+
}));
|
| 161 |
+
app.use(express.json());
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
+
|
| 166 |
+
### 3. Vercel AI SDK v5 Agent
|
| 167 |
+
|
| 168 |
+
**Technology**: Vercel AI SDK v5 with OpenAI provider
|
| 169 |
+
|
| 170 |
+
**Purpose**: Intelligent agent that understands queries and orchestrates tools
|
| 171 |
+
|
| 172 |
+
**Key Files**:
|
| 173 |
+
- `src/agent/index.ts` - Agent factory and configuration
|
| 174 |
+
- `src/agent/prompts.ts` - System prompt and instructions
|
| 175 |
+
- `src/agent/tools.ts` - Tool adapters
|
| 176 |
+
|
| 177 |
+
**Model**: OpenAI gpt-5-chat-latest
|
| 178 |
+
- Context window: 128k tokens
|
| 179 |
+
- Supports function calling
|
| 180 |
+
- Streaming responses
|
| 181 |
+
- Multi-step reasoning
|
| 182 |
+
|
| 183 |
+
**Configuration**:
|
| 184 |
+
```typescript
|
| 185 |
+
const model = openai("gpt-5-chat-latest");
|
| 186 |
+
|
| 187 |
+
streamText({
|
| 188 |
+
model,
|
| 189 |
+
messages: [...],
|
| 190 |
+
tools: {
|
| 191 |
+
discover_organization,
|
| 192 |
+
discover_ai_services,
|
| 193 |
+
assess_compliance,
|
| 194 |
+
},
|
| 195 |
+
maxSteps: 5, // Allow multi-step tool use
|
| 196 |
+
})
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
**Capabilities**:
|
| 200 |
+
- Natural language understanding
|
| 201 |
+
- Intent recognition
|
| 202 |
+
- Tool selection and orchestration
|
| 203 |
+
- Context management
|
| 204 |
+
- Response streaming
|
| 205 |
+
- Error handling
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
### 4. MCP Tool Adapters
|
| 210 |
+
|
| 211 |
+
**Technology**: Vercel AI SDK `tool()` wrapper + MCP tools
|
| 212 |
+
|
| 213 |
+
**Purpose**: Bridge between Vercel AI SDK and MCP tools
|
| 214 |
+
|
| 215 |
+
**Key File**: `src/agent/tools.ts`
|
| 216 |
+
|
| 217 |
+
**Adapter Pattern**:
|
| 218 |
+
```typescript
|
| 219 |
+
import { tool } from "ai";
|
| 220 |
+
import { z } from "zod";
|
| 221 |
+
import { mcpToolFunction } from "../../eu-ai-act-mcp/src/tools/...";
|
| 222 |
+
|
| 223 |
+
export const myTool = tool({
|
| 224 |
+
description: "...",
|
| 225 |
+
parameters: z.object({...}),
|
| 226 |
+
execute: async (params) => {
|
| 227 |
+
return await mcpToolFunction(params);
|
| 228 |
+
},
|
| 229 |
+
});
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
**Three Tools**:
|
| 233 |
+
1. `discover_organization` - Organization profiling
|
| 234 |
+
2. `discover_ai_services` - AI system discovery
|
| 235 |
+
3. `assess_compliance` - Compliance assessment
|
| 236 |
+
|
| 237 |
+
---
|
| 238 |
+
|
| 239 |
+
### 5. MCP Server (Shared)
|
| 240 |
+
|
| 241 |
+
**Technology**: Model Context Protocol SDK
|
| 242 |
+
|
| 243 |
+
**Purpose**: Reusable compliance tools
|
| 244 |
+
|
| 245 |
+
**Location**: `packages/eu-ai-act-mcp/`
|
| 246 |
+
|
| 247 |
+
**Integration**: Tools are imported directly by the agent adapters
|
| 248 |
+
|
| 249 |
+
**Note**: No separate MCP server process needed for the agent. Tools are used as libraries.
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## Data Flow
|
| 254 |
+
|
| 255 |
+
### Basic Chat Flow
|
| 256 |
+
|
| 257 |
+
```
|
| 258 |
+
User Input (Gradio)
|
| 259 |
+
↓
|
| 260 |
+
POST /api/chat {message, history}
|
| 261 |
+
↓
|
| 262 |
+
Express Server validates request
|
| 263 |
+
↓
|
| 264 |
+
Agent.streamText({messages})
|
| 265 |
+
↓
|
| 266 |
+
gpt-5-chat-latest processes with system prompt
|
| 267 |
+
↓
|
| 268 |
+
Streaming response chunks
|
| 269 |
+
↓
|
| 270 |
+
SSE: data: {type: "text", content: "..."}
|
| 271 |
+
↓
|
| 272 |
+
Gradio displays in real-time
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
### Tool Calling Flow
|
| 276 |
+
|
| 277 |
+
```
|
| 278 |
+
User: "Analyze OpenAI's compliance"
|
| 279 |
+
↓
|
| 280 |
+
Agent recognizes need for tools
|
| 281 |
+
↓
|
| 282 |
+
Step 1: Call discover_organization("OpenAI")
|
| 283 |
+
├─ Tavily API search
|
| 284 |
+
├─ Data extraction
|
| 285 |
+
└─ Return organization profile
|
| 286 |
+
↓
|
| 287 |
+
Step 2: Call discover_ai_services(orgContext)
|
| 288 |
+
├─ System classification
|
| 289 |
+
├─ Risk assessment
|
| 290 |
+
└─ Return systems inventory
|
| 291 |
+
↓
|
| 292 |
+
Step 3: Call assess_compliance(org, systems)
|
| 293 |
+
├─ GPT-4 analysis
|
| 294 |
+
├─ Gap identification
|
| 295 |
+
└─ Return assessment + docs
|
| 296 |
+
↓
|
| 297 |
+
Agent synthesizes results
|
| 298 |
+
↓
|
| 299 |
+
Stream final response to user
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
---
|
| 303 |
+
|
| 304 |
+
## Technology Stack Summary
|
| 305 |
+
|
| 306 |
+
| Layer | Technology | Version | Purpose |
|
| 307 |
+
|-------|-----------|---------|---------|
|
| 308 |
+
| UI | Gradio | 5.9.1+ | Web interface |
|
| 309 |
+
| API | Express.js | 4.21+ | REST server |
|
| 310 |
+
| Language | TypeScript | 5.9+ | Type safety |
|
| 311 |
+
| Agent | Vercel AI SDK | 5.0+ | AI orchestration |
|
| 312 |
+
| Model | gpt-5-chat-latest | Latest | Intelligence |
|
| 313 |
+
| Tools | MCP SDK | 1.23+ | Tool protocol |
|
| 314 |
+
| Research | Tavily | 0.5+ | Web search |
|
| 315 |
+
| Validation | Zod | 3.23+ | Schema validation |
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## Configuration Management
|
| 320 |
+
|
| 321 |
+
### Environment Variables
|
| 322 |
+
|
| 323 |
+
**Workspace Root** `.env`:
|
| 324 |
+
```bash
|
| 325 |
+
OPENAI_API_KEY=sk-...
|
| 326 |
+
TAVILY_API_KEY=tvly-...
|
| 327 |
+
PORT=3001
|
| 328 |
+
```
|
| 329 |
+
|
| 330 |
+
**Loading**:
|
| 331 |
+
```typescript
|
| 332 |
+
import { config } from "dotenv";
|
| 333 |
+
config({ path: resolve(__dirname, "../../.env") });
|
| 334 |
+
```
|
| 335 |
+
|
| 336 |
+
### Package Configuration
|
| 337 |
+
|
| 338 |
+
**Monorepo Structure**:
|
| 339 |
+
```
|
| 340 |
+
packages/eu-ai-act-mcp/ # MCP tools
|
| 341 |
+
apps/eu-ai-act-agent/ # Agent + UI
|
| 342 |
+
├── src/
|
| 343 |
+
│ ├── server.ts # Express server
|
| 344 |
+
│ ├── gradio_app.py # Gradio UI
|
| 345 |
+
│ └── agent/
|
| 346 |
+
│ ├── index.ts # Agent config
|
| 347 |
+
│ ├── tools.ts # Tool adapters
|
| 348 |
+
│ └── prompts.ts # System prompt
|
| 349 |
+
└── package.json
|
| 350 |
+
```
|
| 351 |
+
|
| 352 |
+
**Dependencies**:
|
| 353 |
+
- Agent depends on MCP package
|
| 354 |
+
- Imports tools directly (no RPC)
|
| 355 |
+
- Shared TypeScript config
|
| 356 |
+
|
| 357 |
+
---
|
| 358 |
+
|
| 359 |
+
## Scaling Considerations
|
| 360 |
+
|
| 361 |
+
### Horizontal Scaling
|
| 362 |
+
|
| 363 |
+
**Current**: Single instance of Express + Gradio
|
| 364 |
+
|
| 365 |
+
**For Production**:
|
| 366 |
+
1. **Multiple API Instances**:
|
| 367 |
+
```
|
| 368 |
+
Load Balancer
|
| 369 |
+
├─ API Server 1
|
| 370 |
+
├─ API Server 2
|
| 371 |
+
└─ API Server 3
|
| 372 |
+
```
|
| 373 |
+
|
| 374 |
+
2. **Session Management**:
|
| 375 |
+
- Use Redis for conversation history
|
| 376 |
+
- Sticky sessions at load balancer
|
| 377 |
+
- Stateless API design
|
| 378 |
+
|
| 379 |
+
3. **Gradio Scaling**:
|
| 380 |
+
- Multiple Gradio instances
|
| 381 |
+
- Shared API endpoint
|
| 382 |
+
- CDN for static assets
|
| 383 |
+
|
| 384 |
+
### Vertical Scaling
|
| 385 |
+
|
| 386 |
+
- Increase Node.js worker threads
|
| 387 |
+
- Use clustering module
|
| 388 |
+
- Optimize Python workers
|
| 389 |
+
|
| 390 |
+
### Caching Strategy
|
| 391 |
+
|
| 392 |
+
```typescript
|
| 393 |
+
import NodeCache from 'node-cache';
|
| 394 |
+
|
| 395 |
+
const orgCache = new NodeCache({ stdTTL: 3600 });
|
| 396 |
+
const systemCache = new NodeCache({ stdTTL: 1800 });
|
| 397 |
+
|
| 398 |
+
// Cache organization discoveries
|
| 399 |
+
if (orgCache.has(orgName)) {
|
| 400 |
+
return orgCache.get(orgName);
|
| 401 |
+
}
|
| 402 |
+
```
|
| 403 |
+
|
| 404 |
+
---
|
| 405 |
+
|
| 406 |
+
## Security Architecture
|
| 407 |
+
|
| 408 |
+
### Current State (Development)
|
| 409 |
+
|
| 410 |
+
- No authentication
|
| 411 |
+
- Open CORS for localhost
|
| 412 |
+
- Environment variables for API keys
|
| 413 |
+
- No encryption at rest
|
| 414 |
+
|
| 415 |
+
### Production Requirements
|
| 416 |
+
|
| 417 |
+
1. **Authentication**:
|
| 418 |
+
```typescript
|
| 419 |
+
import jwt from 'jsonwebtoken';
|
| 420 |
+
|
| 421 |
+
app.use('/api/', (req, res, next) => {
|
| 422 |
+
const token = req.headers.authorization;
|
| 423 |
+
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
|
| 424 |
+
if (err) return res.status(401).send('Unauthorized');
|
| 425 |
+
req.user = decoded;
|
| 426 |
+
next();
|
| 427 |
+
});
|
| 428 |
+
});
|
| 429 |
+
```
|
| 430 |
+
|
| 431 |
+
2. **Rate Limiting**:
|
| 432 |
+
```typescript
|
| 433 |
+
import rateLimit from 'express-rate-limit';
|
| 434 |
+
|
| 435 |
+
const limiter = rateLimit({
|
| 436 |
+
windowMs: 15 * 60 * 1000,
|
| 437 |
+
max: 100
|
| 438 |
+
});
|
| 439 |
+
app.use('/api/', limiter);
|
| 440 |
+
```
|
| 441 |
+
|
| 442 |
+
3. **Input Validation**:
|
| 443 |
+
```typescript
|
| 444 |
+
import { z } from 'zod';
|
| 445 |
+
|
| 446 |
+
const ChatRequestSchema = z.object({
|
| 447 |
+
message: z.string().min(1).max(5000),
|
| 448 |
+
history: z.array(z.object({
|
| 449 |
+
role: z.enum(['user', 'assistant']),
|
| 450 |
+
content: z.string()
|
| 451 |
+
})).max(50)
|
| 452 |
+
});
|
| 453 |
+
```
|
| 454 |
+
|
| 455 |
+
4. **HTTPS Only**:
|
| 456 |
+
```typescript
|
| 457 |
+
if (process.env.NODE_ENV === 'production' && !req.secure) {
|
| 458 |
+
return res.redirect('https://' + req.headers.host + req.url);
|
| 459 |
+
}
|
| 460 |
+
```
|
| 461 |
+
|
| 462 |
+
---
|
| 463 |
+
|
| 464 |
+
## Monitoring & Observability
|
| 465 |
+
|
| 466 |
+
### Logging
|
| 467 |
+
|
| 468 |
+
**Current**: Console logs
|
| 469 |
+
|
| 470 |
+
**Recommended**:
|
| 471 |
+
```typescript
|
| 472 |
+
import winston from 'winston';
|
| 473 |
+
|
| 474 |
+
const logger = winston.createLogger({
|
| 475 |
+
level: 'info',
|
| 476 |
+
format: winston.format.json(),
|
| 477 |
+
transports: [
|
| 478 |
+
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
| 479 |
+
new winston.transports.File({ filename: 'combined.log' })
|
| 480 |
+
]
|
| 481 |
+
});
|
| 482 |
+
```
|
| 483 |
+
|
| 484 |
+
### Metrics
|
| 485 |
+
|
| 486 |
+
Track:
|
| 487 |
+
- Request rate
|
| 488 |
+
- Response time
|
| 489 |
+
- Tool execution time
|
| 490 |
+
- Error rate
|
| 491 |
+
- OpenAI API usage
|
| 492 |
+
|
| 493 |
+
### Alerting
|
| 494 |
+
|
| 495 |
+
Monitor:
|
| 496 |
+
- API downtime
|
| 497 |
+
- High error rates
|
| 498 |
+
- OpenAI rate limits
|
| 499 |
+
- Disk space (logs)
|
| 500 |
+
|
| 501 |
+
---
|
| 502 |
+
|
| 503 |
+
## Development Workflow
|
| 504 |
+
|
| 505 |
+
### Local Development
|
| 506 |
+
|
| 507 |
+
```bash
|
| 508 |
+
# Terminal 1: Watch mode for API
|
| 509 |
+
pnpm dev
|
| 510 |
+
|
| 511 |
+
# Terminal 2: Python app
|
| 512 |
+
python3 src/gradio_app.py
|
| 513 |
+
|
| 514 |
+
# Terminal 3: Watch MCP changes
|
| 515 |
+
pnpm --filter @eu-ai-act/mcp-server dev
|
| 516 |
+
```
|
| 517 |
+
|
| 518 |
+
### Testing
|
| 519 |
+
|
| 520 |
+
```bash
|
| 521 |
+
# Unit tests (future)
|
| 522 |
+
pnpm test
|
| 523 |
+
|
| 524 |
+
# Integration tests (future)
|
| 525 |
+
pnpm test:integration
|
| 526 |
+
|
| 527 |
+
# Manual testing
|
| 528 |
+
curl http://localhost:3001/health
|
| 529 |
+
```
|
| 530 |
+
|
| 531 |
+
### Building
|
| 532 |
+
|
| 533 |
+
```bash
|
| 534 |
+
# Build MCP server
|
| 535 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 536 |
+
|
| 537 |
+
# Build agent
|
| 538 |
+
pnpm --filter @eu-ai-act/agent build
|
| 539 |
+
|
| 540 |
+
# Build all
|
| 541 |
+
pnpm build
|
| 542 |
+
```
|
| 543 |
+
|
| 544 |
+
---
|
| 545 |
+
|
| 546 |
+
## Deployment Architecture
|
| 547 |
+
|
| 548 |
+
### Recommended: Vercel + Hugging Face
|
| 549 |
+
|
| 550 |
+
```
|
| 551 |
+
[Vercel] [Hugging Face Spaces]
|
| 552 |
+
↓ ↓
|
| 553 |
+
Express API (Node.js) Gradio UI (Python)
|
| 554 |
+
↓ ↓
|
| 555 |
+
gpt-5-chat-latest + MCP Tools ↓
|
| 556 |
+
↑ ↓
|
| 557 |
+
←──────── HTTP/SSE ───────────┘
|
| 558 |
+
```
|
| 559 |
+
|
| 560 |
+
**Benefits**:
|
| 561 |
+
- Vercel: Serverless scaling, CDN, automatic HTTPS
|
| 562 |
+
- HF Spaces: Free Gradio hosting, GPU access (if needed)
|
| 563 |
+
- Separation of concerns
|
| 564 |
+
|
| 565 |
+
### Alternative: Single Server
|
| 566 |
+
|
| 567 |
+
```
|
| 568 |
+
[VPS / Cloud VM]
|
| 569 |
+
├─ Nginx (reverse proxy)
|
| 570 |
+
├─ Express API :3001
|
| 571 |
+
├─ Gradio UI :7860
|
| 572 |
+
└─ PM2 (process manager)
|
| 573 |
+
```
|
| 574 |
+
|
| 575 |
+
**Benefits**:
|
| 576 |
+
- Simpler deployment
|
| 577 |
+
- Lower latency (same server)
|
| 578 |
+
- Full control
|
| 579 |
+
|
| 580 |
+
---
|
| 581 |
+
|
| 582 |
+
## Performance Optimization
|
| 583 |
+
|
| 584 |
+
### Response Time
|
| 585 |
+
|
| 586 |
+
- **Current**: ~2-5 seconds for simple queries
|
| 587 |
+
- **With Tools**: ~10-30 seconds (Tavily + GPT-4 analysis)
|
| 588 |
+
- **Optimization**:
|
| 589 |
+
- Cache Tavily results (24h TTL)
|
| 590 |
+
- Parallel tool execution where possible
|
| 591 |
+
- Stream responses immediately
|
| 592 |
+
|
| 593 |
+
### Cost Optimization
|
| 594 |
+
|
| 595 |
+
- Use gpt-5-chat-latest-mini for simple queries (future)
|
| 596 |
+
- Cache frequently requested data
|
| 597 |
+
- Batch processing where applicable
|
| 598 |
+
- Monitor token usage
|
| 599 |
+
|
| 600 |
+
---
|
| 601 |
+
|
| 602 |
+
## Future Enhancements
|
| 603 |
+
|
| 604 |
+
1. **WebSocket Support**: Replace SSE with WebSockets
|
| 605 |
+
2. **Multi-tenancy**: Support multiple organizations
|
| 606 |
+
3. **Persistent Storage**: Database for assessments
|
| 607 |
+
4. **Advanced Analytics**: Compliance dashboards
|
| 608 |
+
5. **Document Export**: PDF/DOCX generation
|
| 609 |
+
6. **Email Reports**: Scheduled compliance reports
|
| 610 |
+
7. **API Management**: Rate limiting, quotas, billing
|
| 611 |
+
8. **Advanced Caching**: Redis cluster
|
| 612 |
+
9. **Internationalization**: Multi-language support
|
| 613 |
+
10. **Mobile App**: React Native companion app
|
| 614 |
+
|
| 615 |
+
---
|
| 616 |
+
|
| 617 |
+
## Troubleshooting
|
| 618 |
+
|
| 619 |
+
### Common Issues
|
| 620 |
+
|
| 621 |
+
1. **Agent not responding**:
|
| 622 |
+
- Check OPENAI_API_KEY
|
| 623 |
+
- Verify API server is running
|
| 624 |
+
- Check console for errors
|
| 625 |
+
|
| 626 |
+
2. **Tool calls failing**:
|
| 627 |
+
- Ensure MCP server is built
|
| 628 |
+
- Check tool imports in tools.ts
|
| 629 |
+
- Verify environment variables
|
| 630 |
+
|
| 631 |
+
3. **Gradio connection issues**:
|
| 632 |
+
- Verify API_URL in gradio_app.py
|
| 633 |
+
- Check CORS configuration
|
| 634 |
+
- Ensure port 3001 is open
|
| 635 |
+
|
| 636 |
+
---
|
| 637 |
+
|
| 638 |
+
## Architecture Decisions (ADRs)
|
| 639 |
+
|
| 640 |
+
### Why Vercel AI SDK v5?
|
| 641 |
+
|
| 642 |
+
- Native streaming support
|
| 643 |
+
- Tool calling abstraction
|
| 644 |
+
- TypeScript-first
|
| 645 |
+
- Active development
|
| 646 |
+
- Good documentation
|
| 647 |
+
|
| 648 |
+
### Why Gradio?
|
| 649 |
+
|
| 650 |
+
- Rapid prototyping
|
| 651 |
+
- Built-in chat UI
|
| 652 |
+
- Python ecosystem
|
| 653 |
+
- Easy deployment (HF Spaces)
|
| 654 |
+
- No frontend expertise needed
|
| 655 |
+
|
| 656 |
+
### Why Express?
|
| 657 |
+
|
| 658 |
+
- Lightweight
|
| 659 |
+
- TypeScript support
|
| 660 |
+
- Large ecosystem
|
| 661 |
+
- Easy to understand
|
| 662 |
+
- Flexible
|
| 663 |
+
|
| 664 |
+
### Why Direct Tool Import?
|
| 665 |
+
|
| 666 |
+
- Simpler architecture
|
| 667 |
+
- No RPC overhead
|
| 668 |
+
- Shared code between MCP server and agent
|
| 669 |
+
- Easier debugging
|
| 670 |
+
|
| 671 |
+
---
|
| 672 |
+
|
| 673 |
+
**Questions?** See [README.md](README.md) or [API.md](API.md)
|
| 674 |
+
|
apps/eu-ai-act-agent/DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Deployment Guide
|
| 2 |
+
|
| 3 |
+
## Prerequisites
|
| 4 |
+
|
| 5 |
+
### System Requirements
|
| 6 |
+
- **Node.js** 18+ and pnpm 8+
|
| 7 |
+
- **Python** 3.9+ with uv (fast package manager)
|
| 8 |
+
- **Git** for cloning the repository
|
| 9 |
+
|
| 10 |
+
### API Keys
|
| 11 |
+
1. **OpenAI API Key** (required)
|
| 12 |
+
- Sign up at https://platform.openai.com/
|
| 13 |
+
- Create an API key
|
| 14 |
+
- Set as `OPENAI_API_KEY` environment variable
|
| 15 |
+
|
| 16 |
+
2. **Tavily API Key** (optional, recommended)
|
| 17 |
+
- Sign up at https://app.tavily.com
|
| 18 |
+
- Get 1,000 free credits/month
|
| 19 |
+
- Set as `TAVILY_API_KEY` environment variable
|
| 20 |
+
|
| 21 |
+
## Local Development
|
| 22 |
+
|
| 23 |
+
### 1. Clone and Install
|
| 24 |
+
|
| 25 |
+
```bash
|
| 26 |
+
# Clone the repository
|
| 27 |
+
git clone <repo-url>
|
| 28 |
+
cd mcp-1st-birthday-ai-act
|
| 29 |
+
|
| 30 |
+
# Install uv (fast Python package manager)
|
| 31 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 32 |
+
|
| 33 |
+
# Install Node.js dependencies (from workspace root)
|
| 34 |
+
pnpm install
|
| 35 |
+
|
| 36 |
+
# Install Python dependencies
|
| 37 |
+
cd apps/eu-ai-act-agent
|
| 38 |
+
uv pip install -r requirements.txt
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
### 2. Configure Environment
|
| 42 |
+
|
| 43 |
+
Create `.env` file in the workspace root:
|
| 44 |
+
|
| 45 |
+
```bash
|
| 46 |
+
# Required
|
| 47 |
+
OPENAI_API_KEY=sk-your-openai-api-key
|
| 48 |
+
|
| 49 |
+
# Optional (for enhanced organization discovery)
|
| 50 |
+
TAVILY_API_KEY=tvly-your-tavily-api-key
|
| 51 |
+
|
| 52 |
+
# Server configuration
|
| 53 |
+
PORT=3001
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 3. Build MCP Server
|
| 57 |
+
|
| 58 |
+
The agent depends on the MCP server tools, so build it first:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
# From workspace root
|
| 62 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### 4. Start Development Servers
|
| 66 |
+
|
| 67 |
+
**Option A: Run both servers** (recommended)
|
| 68 |
+
|
| 69 |
+
Terminal 1 - API Server:
|
| 70 |
+
```bash
|
| 71 |
+
cd apps/eu-ai-act-agent
|
| 72 |
+
pnpm dev
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Terminal 2 - Gradio UI:
|
| 76 |
+
```bash
|
| 77 |
+
cd apps/eu-ai-act-agent
|
| 78 |
+
pnpm gradio
|
| 79 |
+
# or: uv run src/gradio_app.py
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
**Option B: Use workspace commands**
|
| 83 |
+
```bash
|
| 84 |
+
# Terminal 1
|
| 85 |
+
pnpm --filter @eu-ai-act/agent dev
|
| 86 |
+
|
| 87 |
+
# Terminal 2
|
| 88 |
+
pnpm --filter @eu-ai-act/agent gradio
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
### 5. Access the Application
|
| 92 |
+
|
| 93 |
+
- **Gradio UI**: http://localhost:7860
|
| 94 |
+
- **API Server**: http://localhost:3001
|
| 95 |
+
- **Health Check**: http://localhost:3001/health
|
| 96 |
+
|
| 97 |
+
## Production Deployment
|
| 98 |
+
|
| 99 |
+
### Option 1: Vercel (API) + Hugging Face Spaces (Gradio)
|
| 100 |
+
|
| 101 |
+
**Deploy API Server to Vercel:**
|
| 102 |
+
|
| 103 |
+
1. Create `vercel.json` in `apps/eu-ai-act-agent/`:
|
| 104 |
+
```json
|
| 105 |
+
{
|
| 106 |
+
"version": 2,
|
| 107 |
+
"builds": [
|
| 108 |
+
{
|
| 109 |
+
"src": "dist/server.js",
|
| 110 |
+
"use": "@vercel/node"
|
| 111 |
+
}
|
| 112 |
+
],
|
| 113 |
+
"routes": [
|
| 114 |
+
{
|
| 115 |
+
"src": "/(.*)",
|
| 116 |
+
"dest": "dist/server.js"
|
| 117 |
+
}
|
| 118 |
+
],
|
| 119 |
+
"env": {
|
| 120 |
+
"OPENAI_API_KEY": "@openai-api-key",
|
| 121 |
+
"TAVILY_API_KEY": "@tavily-api-key"
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
2. Deploy:
|
| 127 |
+
```bash
|
| 128 |
+
cd apps/eu-ai-act-agent
|
| 129 |
+
pnpm build
|
| 130 |
+
vercel --prod
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
**Deploy Gradio to Hugging Face Spaces:**
|
| 134 |
+
|
| 135 |
+
1. Create a new Space at https://huggingface.co/spaces
|
| 136 |
+
2. Choose Gradio SDK
|
| 137 |
+
3. Push your code:
|
| 138 |
+
```bash
|
| 139 |
+
git remote add hf https://huggingface.co/spaces/<username>/<space-name>
|
| 140 |
+
git push hf main
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
4. Set environment variables in Space settings:
|
| 144 |
+
- `API_URL=https://your-vercel-app.vercel.app`
|
| 145 |
+
|
| 146 |
+
### Option 2: Docker Compose
|
| 147 |
+
|
| 148 |
+
Create `docker-compose.yml`:
|
| 149 |
+
|
| 150 |
+
```yaml
|
| 151 |
+
version: '3.8'
|
| 152 |
+
|
| 153 |
+
services:
|
| 154 |
+
api:
|
| 155 |
+
build:
|
| 156 |
+
context: .
|
| 157 |
+
dockerfile: Dockerfile.api
|
| 158 |
+
ports:
|
| 159 |
+
- "3001:3001"
|
| 160 |
+
environment:
|
| 161 |
+
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
| 162 |
+
- TAVILY_API_KEY=${TAVILY_API_KEY}
|
| 163 |
+
restart: unless-stopped
|
| 164 |
+
|
| 165 |
+
gradio:
|
| 166 |
+
build:
|
| 167 |
+
context: .
|
| 168 |
+
dockerfile: Dockerfile.gradio
|
| 169 |
+
ports:
|
| 170 |
+
- "7860:7860"
|
| 171 |
+
environment:
|
| 172 |
+
- API_URL=http://api:3001
|
| 173 |
+
depends_on:
|
| 174 |
+
- api
|
| 175 |
+
restart: unless-stopped
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
Deploy:
|
| 179 |
+
```bash
|
| 180 |
+
docker-compose up -d
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
### Option 3: Railway / Render
|
| 184 |
+
|
| 185 |
+
Both platforms support Node.js and Python apps:
|
| 186 |
+
|
| 187 |
+
1. **API Server**:
|
| 188 |
+
- Build command: `pnpm build`
|
| 189 |
+
- Start command: `pnpm start`
|
| 190 |
+
- Add environment variables
|
| 191 |
+
|
| 192 |
+
2. **Gradio App**:
|
| 193 |
+
- Build command: `curl -LsSf https://astral.sh/uv/install.sh | sh && uv pip install -r requirements.txt`
|
| 194 |
+
- Start command: `uv run src/gradio_app.py`
|
| 195 |
+
- Set `API_URL` to your API server URL
|
| 196 |
+
|
| 197 |
+
## Environment Variables
|
| 198 |
+
|
| 199 |
+
### Required
|
| 200 |
+
- `OPENAI_API_KEY` - OpenAI API key for GPT-4 (used by agent and assess_compliance tool)
|
| 201 |
+
|
| 202 |
+
### Optional
|
| 203 |
+
- `TAVILY_API_KEY` - Tavily API key for enhanced organization research
|
| 204 |
+
- `PORT` - API server port (default: 3001)
|
| 205 |
+
- `API_URL` - Full URL to API server (for Gradio, default: http://localhost:3001)
|
| 206 |
+
|
| 207 |
+
## Troubleshooting
|
| 208 |
+
|
| 209 |
+
### API Server Issues
|
| 210 |
+
|
| 211 |
+
**Problem**: Server won't start
|
| 212 |
+
```bash
|
| 213 |
+
# Check Node.js version
|
| 214 |
+
node --version # Should be 18+
|
| 215 |
+
|
| 216 |
+
# Rebuild dependencies
|
| 217 |
+
pnpm install
|
| 218 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 219 |
+
pnpm --filter @eu-ai-act/agent build
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
**Problem**: Tools not working
|
| 223 |
+
```bash
|
| 224 |
+
# Verify MCP server is built
|
| 225 |
+
ls packages/eu-ai-act-mcp/dist/
|
| 226 |
+
|
| 227 |
+
# Check environment variables
|
| 228 |
+
echo $OPENAI_API_KEY
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
### Gradio Issues
|
| 232 |
+
|
| 233 |
+
**Problem**: Can't connect to API
|
| 234 |
+
- Verify API server is running: `curl http://localhost:3001/health`
|
| 235 |
+
- Check `API_URL` in environment or `src/gradio_app.py`
|
| 236 |
+
|
| 237 |
+
**Problem**: Python dependencies missing
|
| 238 |
+
```bash
|
| 239 |
+
# Install uv if not already installed
|
| 240 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 241 |
+
|
| 242 |
+
# Install dependencies
|
| 243 |
+
uv pip install -r requirements.txt
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
### General Issues
|
| 247 |
+
|
| 248 |
+
**Problem**: CORS errors
|
| 249 |
+
- Ensure Gradio runs on port 7860 (default)
|
| 250 |
+
- Check CORS settings in `src/server.ts`
|
| 251 |
+
|
| 252 |
+
**Problem**: Rate limits
|
| 253 |
+
- OpenAI has rate limits based on your plan
|
| 254 |
+
- Consider implementing request queuing or caching
|
| 255 |
+
|
| 256 |
+
## Performance Optimization
|
| 257 |
+
|
| 258 |
+
1. **Enable Caching**: Add Redis for caching organization/system discoveries
|
| 259 |
+
2. **Use Streaming**: Already enabled for real-time responses
|
| 260 |
+
3. **Optimize Tools**: Cache Tavily research results
|
| 261 |
+
4. **Load Balancing**: Use multiple API server instances behind a load balancer
|
| 262 |
+
|
| 263 |
+
## Monitoring
|
| 264 |
+
|
| 265 |
+
### Health Checks
|
| 266 |
+
```bash
|
| 267 |
+
# API health
|
| 268 |
+
curl http://localhost:3001/health
|
| 269 |
+
|
| 270 |
+
# Tools status
|
| 271 |
+
curl http://localhost:3001/api/tools
|
| 272 |
+
```
|
| 273 |
+
|
| 274 |
+
### Logging
|
| 275 |
+
- API logs: Check console output or configure logging service
|
| 276 |
+
- Gradio logs: Built-in console logging
|
| 277 |
+
- Consider adding: Sentry, LogRocket, or DataDog
|
| 278 |
+
|
| 279 |
+
## Security
|
| 280 |
+
|
| 281 |
+
1. **API Keys**: Never commit to Git, use environment variables
|
| 282 |
+
2. **CORS**: Restrict origins in production
|
| 283 |
+
3. **Rate Limiting**: Add rate limiting middleware
|
| 284 |
+
4. **Authentication**: Consider adding API authentication for production
|
| 285 |
+
5. **HTTPS**: Always use HTTPS in production
|
| 286 |
+
|
| 287 |
+
## Scaling
|
| 288 |
+
|
| 289 |
+
For high traffic:
|
| 290 |
+
1. Deploy multiple API server instances
|
| 291 |
+
2. Use Redis for session management
|
| 292 |
+
3. Implement request queuing (Bull/BullMQ)
|
| 293 |
+
4. Consider serverless functions for tools
|
| 294 |
+
5. Use CDN for static assets
|
| 295 |
+
|
| 296 |
+
## Support
|
| 297 |
+
|
| 298 |
+
- 📖 Documentation: See README.md
|
| 299 |
+
- 🐛 Issues: GitHub Issues
|
| 300 |
+
- 💬 Discussions: GitHub Discussions
|
| 301 |
+
|
| 302 |
+
|
apps/eu-ai-act-agent/Dockerfile
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EU AI Act Compliance Agent - Hugging Face Spaces
|
| 2 |
+
# Deploys Agent + MCP Server from monorepo
|
| 3 |
+
|
| 4 |
+
FROM node:20-slim
|
| 5 |
+
|
| 6 |
+
# Install Python, pnpm, and uv (to /usr/local/bin for all users)
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
python3 \
|
| 9 |
+
python3-venv \
|
| 10 |
+
curl \
|
| 11 |
+
&& npm install -g pnpm \
|
| 12 |
+
&& curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \
|
| 13 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# Use existing node user (UID 1000) for HF Spaces compatibility
|
| 16 |
+
USER node
|
| 17 |
+
ENV HOME=/home/node
|
| 18 |
+
|
| 19 |
+
WORKDIR $HOME/app
|
| 20 |
+
|
| 21 |
+
# Copy entire monorepo
|
| 22 |
+
COPY --chown=node . .
|
| 23 |
+
|
| 24 |
+
# Install Node dependencies
|
| 25 |
+
RUN pnpm install --frozen-lockfile
|
| 26 |
+
|
| 27 |
+
# Build MCP server and Agent
|
| 28 |
+
RUN pnpm --filter @eu-ai-act/mcp-server build
|
| 29 |
+
RUN pnpm --filter @eu-ai-act/agent build
|
| 30 |
+
|
| 31 |
+
# Create Python venv and install dependencies with uv
|
| 32 |
+
RUN uv venv $HOME/venv && \
|
| 33 |
+
. $HOME/venv/bin/activate && \
|
| 34 |
+
uv pip install --no-cache -r apps/eu-ai-act-agent/requirements.txt
|
| 35 |
+
|
| 36 |
+
# Environment
|
| 37 |
+
# API_URL: Internal communication between Gradio and API server (localhost works inside container)
|
| 38 |
+
# PUBLIC_URL: External HF Spaces URL for links shown to users
|
| 39 |
+
ENV NODE_ENV=production \
|
| 40 |
+
PORT=3001 \
|
| 41 |
+
API_URL=http://localhost:3001 \
|
| 42 |
+
PUBLIC_URL=https://mcp-1st-birthday-eu-ai-act-compliance-agent.hf.space \
|
| 43 |
+
GRADIO_SERVER_NAME=0.0.0.0 \
|
| 44 |
+
GRADIO_SERVER_PORT=7860 \
|
| 45 |
+
GRADIO_SHARE=false \
|
| 46 |
+
PATH=/home/node/venv/bin:$PATH \
|
| 47 |
+
VIRTUAL_ENV=/home/node/venv \
|
| 48 |
+
MCP_SERVER_PATH=/home/node/app/packages/eu-ai-act-mcp/dist/index.js
|
| 49 |
+
|
| 50 |
+
# Set working directory to the agent app for CMD
|
| 51 |
+
WORKDIR $HOME/app/apps/eu-ai-act-agent
|
| 52 |
+
|
| 53 |
+
EXPOSE 7860
|
| 54 |
+
|
| 55 |
+
# Start API server + Main Agent UI (port 7860)
|
| 56 |
+
# ChatGPT MCP Server is deployed separately at:
|
| 57 |
+
# https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-chatgpt-mcp
|
| 58 |
+
CMD node dist/server.js & \
|
| 59 |
+
sleep 2 && \
|
| 60 |
+
python src/gradio_app.py
|
| 61 |
+
|
apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EU AI Act - ChatGPT MCP Server
|
| 2 |
+
# Standalone MCP server for ChatGPT Apps integration
|
| 3 |
+
# Deploys ONLY the MCP tools (discover_organization, discover_ai_services, assess_compliance)
|
| 4 |
+
|
| 5 |
+
FROM node:20-slim
|
| 6 |
+
|
| 7 |
+
# Install Python, pnpm, and uv
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
python3 \
|
| 10 |
+
python3-venv \
|
| 11 |
+
curl \
|
| 12 |
+
&& npm install -g pnpm \
|
| 13 |
+
&& curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \
|
| 14 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
+
|
| 16 |
+
# Use existing node user (UID 1000) for HF Spaces compatibility
|
| 17 |
+
USER node
|
| 18 |
+
ENV HOME=/home/node
|
| 19 |
+
|
| 20 |
+
WORKDIR $HOME/app
|
| 21 |
+
|
| 22 |
+
# Copy entire monorepo
|
| 23 |
+
COPY --chown=node . .
|
| 24 |
+
|
| 25 |
+
# Install Node dependencies
|
| 26 |
+
RUN pnpm install --frozen-lockfile
|
| 27 |
+
|
| 28 |
+
# Build MCP server and Agent (needed for API)
|
| 29 |
+
RUN pnpm --filter @eu-ai-act/mcp-server build
|
| 30 |
+
RUN pnpm --filter @eu-ai-act/agent build
|
| 31 |
+
|
| 32 |
+
# Create Python venv and install dependencies
|
| 33 |
+
RUN uv venv $HOME/venv && \
|
| 34 |
+
. $HOME/venv/bin/activate && \
|
| 35 |
+
uv pip install --no-cache -r apps/eu-ai-act-agent/requirements.txt
|
| 36 |
+
|
| 37 |
+
# Environment
|
| 38 |
+
ENV NODE_ENV=production \
|
| 39 |
+
PORT=3001 \
|
| 40 |
+
API_URL=http://localhost:3001 \
|
| 41 |
+
PUBLIC_URL=https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space \
|
| 42 |
+
CHATGPT_APP_SERVER_NAME=0.0.0.0 \
|
| 43 |
+
CHATGPT_APP_SERVER_PORT=7860 \
|
| 44 |
+
PATH=/home/node/venv/bin:$PATH \
|
| 45 |
+
VIRTUAL_ENV=/home/node/venv \
|
| 46 |
+
MCP_SERVER_PATH=/home/node/app/packages/eu-ai-act-mcp/dist/index.js
|
| 47 |
+
|
| 48 |
+
WORKDIR $HOME/app/apps/eu-ai-act-agent
|
| 49 |
+
|
| 50 |
+
EXPOSE 7860
|
| 51 |
+
|
| 52 |
+
# Start API server + ChatGPT MCP App on port 7860
|
| 53 |
+
# MCP URL will be: PUBLIC_URL/gradio_api/mcp/
|
| 54 |
+
CMD node dist/server.js & \
|
| 55 |
+
sleep 2 && \
|
| 56 |
+
python src/chatgpt_app.py
|
| 57 |
+
|
apps/eu-ai-act-agent/EXAMPLES.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 💡 Usage Examples
|
| 2 |
+
|
| 3 |
+
Real-world examples of using the EU AI Act Compliance Agent.
|
| 4 |
+
|
| 5 |
+
## 🎯 Example 1: Understanding the Basics
|
| 6 |
+
|
| 7 |
+
### Question
|
| 8 |
+
```
|
| 9 |
+
What is the EU AI Act and why should I care about it?
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
### Agent Response
|
| 13 |
+
The agent will explain:
|
| 14 |
+
- Overview of the EU AI Act (Regulation 2024/1689)
|
| 15 |
+
- Why it matters for organizations deploying AI in Europe
|
| 16 |
+
- Key risk categories and their implications
|
| 17 |
+
- Important deadlines (Feb 2025, Aug 2026, Aug 2027)
|
| 18 |
+
- Penalties for non-compliance
|
| 19 |
+
|
| 20 |
+
**Use Case**: Educating stakeholders and leadership teams
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## 🏢 Example 2: Organization Discovery
|
| 25 |
+
|
| 26 |
+
### Question
|
| 27 |
+
```
|
| 28 |
+
Discover and analyze OpenAI's compliance profile
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Agent Workflow
|
| 32 |
+
1. **Calls `discover_organization`** with "OpenAI"
|
| 33 |
+
- Searches company information via Tavily
|
| 34 |
+
- Identifies sector, size, AI maturity level
|
| 35 |
+
- Maps EU presence and regulatory obligations
|
| 36 |
+
|
| 37 |
+
2. **Returns Profile**:
|
| 38 |
+
```json
|
| 39 |
+
{
|
| 40 |
+
"organization": {
|
| 41 |
+
"name": "OpenAI",
|
| 42 |
+
"sector": "AI Research & Development",
|
| 43 |
+
"size": "Large Enterprise",
|
| 44 |
+
"aiMaturityLevel": "Expert",
|
| 45 |
+
"euPresence": true,
|
| 46 |
+
"primaryRole": "Provider"
|
| 47 |
+
},
|
| 48 |
+
"regulatoryContext": {
|
| 49 |
+
"hasQualityManagementSystem": true,
|
| 50 |
+
"hasRiskManagementSystem": true,
|
| 51 |
+
"complianceDeadlines": [...]
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
3. **Provides Insights**:
|
| 57 |
+
- OpenAI is a Large Enterprise with Expert AI maturity
|
| 58 |
+
- As an AI Provider, they must comply with Articles 16-29
|
| 59 |
+
- They need authorized representative in EU (Article 22)
|
| 60 |
+
- Must register high-risk systems in EU database
|
| 61 |
+
|
| 62 |
+
**Use Case**: Initial compliance assessment for your organization or analyzing competitors
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 🤖 Example 3: AI System Classification
|
| 67 |
+
|
| 68 |
+
### Question
|
| 69 |
+
```
|
| 70 |
+
Is a recruitment screening AI system high-risk under the EU AI Act?
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### Agent Response
|
| 74 |
+
Yes, it's HIGH RISK per **Annex III, Section 4(a)**: "AI systems intended to be used for recruitment or selection of natural persons."
|
| 75 |
+
|
| 76 |
+
**Requirements**:
|
| 77 |
+
- ✅ Conformity assessment (Article 43) - Must undergo third-party assessment
|
| 78 |
+
- ✅ Technical documentation (Article 11, Annex IV) - Comprehensive system docs
|
| 79 |
+
- ✅ Risk management system (Article 9) - Continuous risk monitoring
|
| 80 |
+
- ✅ Data governance (Article 10) - Training data quality assurance
|
| 81 |
+
- ✅ Human oversight (Article 14) - Human-in-the-loop procedures
|
| 82 |
+
- ✅ Transparency (Article 13) - Clear information to users
|
| 83 |
+
- ✅ CE marking (Article 48) - Affixing CE mark
|
| 84 |
+
- ✅ EU database registration (Article 49) - Registration before deployment
|
| 85 |
+
|
| 86 |
+
**Timeline**:
|
| 87 |
+
- Compliance required by: **August 2, 2026**
|
| 88 |
+
|
| 89 |
+
**Use Case**: Determining if your AI system requires strict compliance measures
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 🔍 Example 4: Comprehensive System Discovery
|
| 94 |
+
|
| 95 |
+
### Question
|
| 96 |
+
```
|
| 97 |
+
Scan and classify all AI systems for a company called "TechCorp AI"
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Agent Workflow
|
| 101 |
+
1. **Calls `discover_organization`** for TechCorp AI
|
| 102 |
+
- Gets organization context
|
| 103 |
+
|
| 104 |
+
2. **Calls `discover_ai_services`** with organization context
|
| 105 |
+
- Discovers AI systems (real or mock data)
|
| 106 |
+
- Classifies each by risk level
|
| 107 |
+
- Identifies compliance gaps
|
| 108 |
+
|
| 109 |
+
3. **Returns Inventory**:
|
| 110 |
+
```json
|
| 111 |
+
{
|
| 112 |
+
"systems": [
|
| 113 |
+
{
|
| 114 |
+
"system": {
|
| 115 |
+
"name": "Customer Service Chatbot",
|
| 116 |
+
"status": "Production"
|
| 117 |
+
},
|
| 118 |
+
"riskClassification": {
|
| 119 |
+
"category": "Limited",
|
| 120 |
+
"riskScore": 35
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"system": {
|
| 125 |
+
"name": "Fraud Detection System",
|
| 126 |
+
"status": "Production"
|
| 127 |
+
},
|
| 128 |
+
"riskClassification": {
|
| 129 |
+
"category": "High",
|
| 130 |
+
"annexIIICategory": "Annex III, Section 5(d)",
|
| 131 |
+
"riskScore": 85
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
],
|
| 135 |
+
"riskSummary": {
|
| 136 |
+
"highRiskCount": 1,
|
| 137 |
+
"limitedRiskCount": 1,
|
| 138 |
+
"totalCount": 2
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
4. **Provides Summary**:
|
| 144 |
+
- TechCorp AI has 2 AI systems
|
| 145 |
+
- 1 high-risk system requiring immediate attention
|
| 146 |
+
- 1 limited-risk system with transparency obligations
|
| 147 |
+
|
| 148 |
+
**Use Case**: Creating a comprehensive AI system inventory for compliance planning
|
| 149 |
+
|
| 150 |
+
---
|
| 151 |
+
|
| 152 |
+
## 📄 Example 5: Documentation Generation
|
| 153 |
+
|
| 154 |
+
### Question
|
| 155 |
+
```
|
| 156 |
+
Generate EU AI Act compliance documentation for our customer support chatbot
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
### Agent Workflow
|
| 160 |
+
1. **Classifies the system** as Limited Risk (Article 50)
|
| 161 |
+
|
| 162 |
+
2. **Calls `assess_compliance`** with:
|
| 163 |
+
- System type: Customer support chatbot
|
| 164 |
+
- Risk category: Limited Risk
|
| 165 |
+
- Generate documentation: true
|
| 166 |
+
|
| 167 |
+
3. **Generates Templates**:
|
| 168 |
+
|
| 169 |
+
**📋 Risk Assessment**
|
| 170 |
+
```markdown
|
| 171 |
+
# Risk Assessment: Customer Support Chatbot
|
| 172 |
+
|
| 173 |
+
## Risk Classification
|
| 174 |
+
- **Category**: Limited Risk
|
| 175 |
+
- **Article**: Article 50 (Transparency Obligations)
|
| 176 |
+
|
| 177 |
+
## Risk Analysis
|
| 178 |
+
The chatbot interacts with natural persons and must disclose
|
| 179 |
+
that the user is interacting with an AI system...
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
**📄 Technical Documentation**
|
| 183 |
+
```markdown
|
| 184 |
+
# Technical Documentation
|
| 185 |
+
|
| 186 |
+
## System Description
|
| 187 |
+
- **Name**: Customer Support Chatbot
|
| 188 |
+
- **Purpose**: Automated customer service
|
| 189 |
+
- **Technology**: Natural Language Processing
|
| 190 |
+
|
| 191 |
+
## Compliance Requirements
|
| 192 |
+
1. Transparency Notice (Article 50)...
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
**📢 Transparency Notice**
|
| 196 |
+
```markdown
|
| 197 |
+
# Transparency Notice
|
| 198 |
+
|
| 199 |
+
You are interacting with an AI system designed to assist
|
| 200 |
+
with customer service inquiries...
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
4. **Provides Export Options**:
|
| 204 |
+
- Download as Markdown
|
| 205 |
+
- Convert to PDF
|
| 206 |
+
- Generate Word document
|
| 207 |
+
|
| 208 |
+
**Use Case**: Creating required compliance documentation quickly
|
| 209 |
+
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
## ⚖️ Example 6: Compliance Gap Analysis
|
| 213 |
+
|
| 214 |
+
### Question
|
| 215 |
+
```
|
| 216 |
+
Analyze compliance gaps for our high-risk credit scoring AI
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
### Agent Workflow
|
| 220 |
+
1. **Calls `discover_ai_services`** with system details
|
| 221 |
+
- Classifies as High Risk (Annex III, Section 5(b))
|
| 222 |
+
|
| 223 |
+
2. **Calls `assess_compliance`**
|
| 224 |
+
- Analyzes current compliance status
|
| 225 |
+
- Identifies gaps
|
| 226 |
+
|
| 227 |
+
3. **Returns Gap Analysis**:
|
| 228 |
+
```json
|
| 229 |
+
{
|
| 230 |
+
"assessment": {
|
| 231 |
+
"overallScore": 45,
|
| 232 |
+
"gaps": [
|
| 233 |
+
{
|
| 234 |
+
"area": "Technical Documentation",
|
| 235 |
+
"severity": "Critical",
|
| 236 |
+
"article": "Article 11",
|
| 237 |
+
"description": "Missing comprehensive technical documentation per Annex IV",
|
| 238 |
+
"recommendation": "Create detailed system architecture and data flow documentation"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"area": "Conformity Assessment",
|
| 242 |
+
"severity": "Critical",
|
| 243 |
+
"article": "Article 43",
|
| 244 |
+
"description": "No conformity assessment conducted",
|
| 245 |
+
"recommendation": "Engage notified body for third-party assessment"
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"area": "Human Oversight",
|
| 249 |
+
"severity": "High",
|
| 250 |
+
"article": "Article 14",
|
| 251 |
+
"description": "Insufficient human oversight mechanisms",
|
| 252 |
+
"recommendation": "Implement human-in-the-loop review process"
|
| 253 |
+
}
|
| 254 |
+
],
|
| 255 |
+
"recommendations": [...]
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
4. **Provides Roadmap**:
|
| 261 |
+
- Priority 1: Technical documentation
|
| 262 |
+
- Priority 2: Conformity assessment
|
| 263 |
+
- Priority 3: Human oversight implementation
|
| 264 |
+
- Priority 4: Risk management system
|
| 265 |
+
|
| 266 |
+
**Use Case**: Understanding what needs to be done to achieve compliance
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
## 🎓 Example 7: Article-Specific Questions
|
| 271 |
+
|
| 272 |
+
### Question
|
| 273 |
+
```
|
| 274 |
+
Explain Article 14 on human oversight and how to implement it
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
### Agent Response
|
| 278 |
+
**Article 14: Human Oversight**
|
| 279 |
+
|
| 280 |
+
High-risk AI systems must be designed to enable effective oversight by natural persons during operation.
|
| 281 |
+
|
| 282 |
+
**Key Requirements**:
|
| 283 |
+
1. **Human-in-the-loop** (HITL) - Humans can intervene in real-time
|
| 284 |
+
2. **Human-on-the-loop** (HOTL) - Humans can monitor and intervene if needed
|
| 285 |
+
3. **Human-in-command** (HIC) - Humans can override AI decisions
|
| 286 |
+
|
| 287 |
+
**Implementation Steps**:
|
| 288 |
+
1. **Design interfaces** that enable humans to:
|
| 289 |
+
- Understand AI outputs
|
| 290 |
+
- Interpret system decisions
|
| 291 |
+
- Override or reverse decisions
|
| 292 |
+
- Interrupt system operation
|
| 293 |
+
|
| 294 |
+
2. **Provide training** to oversight personnel on:
|
| 295 |
+
- System capabilities and limitations
|
| 296 |
+
- Potential risks and biases
|
| 297 |
+
- When and how to intervene
|
| 298 |
+
|
| 299 |
+
3. **Document procedures** for:
|
| 300 |
+
- Escalation paths
|
| 301 |
+
- Override protocols
|
| 302 |
+
- Incident reporting
|
| 303 |
+
|
| 304 |
+
**Example Implementation**:
|
| 305 |
+
```typescript
|
| 306 |
+
// Human oversight mechanism
|
| 307 |
+
interface HumanOversight {
|
| 308 |
+
canOverride: boolean;
|
| 309 |
+
reviewRequired: boolean;
|
| 310 |
+
escalationPath: string[];
|
| 311 |
+
monitoringLevel: "HITL" | "HOTL" | "HIC";
|
| 312 |
+
}
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
**Use Case**: Understanding specific compliance requirements in depth
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## 🌐 Example 8: Multi-Step Analysis
|
| 320 |
+
|
| 321 |
+
### Question
|
| 322 |
+
```
|
| 323 |
+
I'm launching a new AI-powered hiring platform in the EU.
|
| 324 |
+
What do I need to do to be compliant?
|
| 325 |
+
```
|
| 326 |
+
|
| 327 |
+
### Agent Workflow (Automatic Multi-Step)
|
| 328 |
+
|
| 329 |
+
**Step 1: Risk Classification**
|
| 330 |
+
- Identifies this as HIGH RISK (Annex III, Section 4)
|
| 331 |
+
- Explains strict requirements apply
|
| 332 |
+
|
| 333 |
+
**Step 2: Organization Profile**
|
| 334 |
+
- Asks for or discovers organization details
|
| 335 |
+
- Determines provider role and obligations
|
| 336 |
+
|
| 337 |
+
**Step 3: System Discovery**
|
| 338 |
+
- Catalogs the hiring platform features
|
| 339 |
+
- Identifies which components are AI-driven
|
| 340 |
+
|
| 341 |
+
**Step 4: Compliance Assessment**
|
| 342 |
+
- Generates comprehensive compliance checklist
|
| 343 |
+
- Identifies documentation requirements
|
| 344 |
+
- Provides timeline and budget estimates
|
| 345 |
+
|
| 346 |
+
**Step 5: Documentation**
|
| 347 |
+
- Generates initial templates for:
|
| 348 |
+
- Risk management system
|
| 349 |
+
- Technical documentation
|
| 350 |
+
- Conformity assessment preparation
|
| 351 |
+
- Transparency notices
|
| 352 |
+
|
| 353 |
+
**Step 6: Roadmap**
|
| 354 |
+
- Creates phased implementation plan
|
| 355 |
+
- Suggests notified bodies for assessment
|
| 356 |
+
- Provides resource recommendations
|
| 357 |
+
|
| 358 |
+
**Use Case**: End-to-end compliance planning for new AI products
|
| 359 |
+
|
| 360 |
+
---
|
| 361 |
+
|
| 362 |
+
## 🔄 Example 9: Compliance Monitoring
|
| 363 |
+
|
| 364 |
+
### Question
|
| 365 |
+
```
|
| 366 |
+
We completed initial compliance last year.
|
| 367 |
+
How do we maintain ongoing compliance?
|
| 368 |
+
```
|
| 369 |
+
|
| 370 |
+
### Agent Response
|
| 371 |
+
|
| 372 |
+
**Ongoing Compliance Requirements**:
|
| 373 |
+
|
| 374 |
+
**1. Post-Market Monitoring (Article 72)**
|
| 375 |
+
- Establish monitoring system
|
| 376 |
+
- Collect and analyze incident data
|
| 377 |
+
- Report serious incidents to authorities
|
| 378 |
+
|
| 379 |
+
**2. Regular Updates**:
|
| 380 |
+
- Update technical documentation with system changes
|
| 381 |
+
- Re-assess risks after major updates
|
| 382 |
+
- Maintain conformity assessment validity
|
| 383 |
+
|
| 384 |
+
**3. Quality Management (Article 17)**:
|
| 385 |
+
- Conduct internal audits
|
| 386 |
+
- Maintain management review process
|
| 387 |
+
- Implement corrective actions
|
| 388 |
+
|
| 389 |
+
**4. Record Keeping**:
|
| 390 |
+
- Maintain logs for high-risk systems (Article 12)
|
| 391 |
+
- Keep documentation for 10 years
|
| 392 |
+
- Track all modifications
|
| 393 |
+
|
| 394 |
+
**5. Incident Response**:
|
| 395 |
+
- Report serious incidents within 15 days
|
| 396 |
+
- Implement corrective measures
|
| 397 |
+
- Update risk assessments
|
| 398 |
+
|
| 399 |
+
**Recommended Schedule**:
|
| 400 |
+
- Monthly: Review system logs and incidents
|
| 401 |
+
- Quarterly: Internal compliance audit
|
| 402 |
+
- Annually: Full compliance review and update documentation
|
| 403 |
+
- As needed: Risk re-assessment after changes
|
| 404 |
+
|
| 405 |
+
**Use Case**: Establishing continuous compliance processes
|
| 406 |
+
|
| 407 |
+
---
|
| 408 |
+
|
| 409 |
+
## 💼 Example 10: Executive Summary
|
| 410 |
+
|
| 411 |
+
### Question
|
| 412 |
+
```
|
| 413 |
+
Create an executive summary of our AI compliance status
|
| 414 |
+
for the board meeting
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
### Agent Workflow
|
| 418 |
+
1. Runs organization discovery
|
| 419 |
+
2. Discovers all AI systems
|
| 420 |
+
3. Assesses overall compliance
|
| 421 |
+
4. Generates executive summary
|
| 422 |
+
|
| 423 |
+
### Sample Output
|
| 424 |
+
|
| 425 |
+
```markdown
|
| 426 |
+
# EU AI Act Compliance - Executive Summary
|
| 427 |
+
|
| 428 |
+
## Overview
|
| 429 |
+
- **Organization**: [Company Name]
|
| 430 |
+
- **Total AI Systems**: 8
|
| 431 |
+
- **Overall Compliance Score**: 62/100
|
| 432 |
+
|
| 433 |
+
## Risk Breakdown
|
| 434 |
+
- 🔴 Unacceptable Risk: 0 systems
|
| 435 |
+
- 🟠 High Risk: 2 systems
|
| 436 |
+
- 🟡 Limited Risk: 3 systems
|
| 437 |
+
- 🟢 Minimal Risk: 3 systems
|
| 438 |
+
|
| 439 |
+
## Critical Actions Required
|
| 440 |
+
1. **Urgent**: Complete conformity assessment for recruitment AI (Deadline: Aug 2026)
|
| 441 |
+
2. **High Priority**: Implement human oversight for credit scoring AI
|
| 442 |
+
3. **Medium Priority**: Create transparency notices for chatbots
|
| 443 |
+
|
| 444 |
+
## Budget Impact
|
| 445 |
+
- Conformity assessments: €50,000 - €100,000
|
| 446 |
+
- Documentation & implementation: €30,000 - €50,000
|
| 447 |
+
- Ongoing compliance: €20,000/year
|
| 448 |
+
|
| 449 |
+
## Timeline
|
| 450 |
+
- Q1 2025: Complete high-risk system documentation
|
| 451 |
+
- Q2 2025: Begin conformity assessments
|
| 452 |
+
- Q3 2025: Implement human oversight mechanisms
|
| 453 |
+
- Q4 2025: Final compliance verification
|
| 454 |
+
|
| 455 |
+
## Risks of Non-Compliance
|
| 456 |
+
- Fines up to €35M or 7% of global revenue
|
| 457 |
+
- Prohibition from EU market
|
| 458 |
+
- Reputational damage
|
| 459 |
+
```
|
| 460 |
+
|
| 461 |
+
**Use Case**: Communicating compliance status to leadership and stakeholders
|
| 462 |
+
|
| 463 |
+
---
|
| 464 |
+
|
| 465 |
+
## 🎯 Pro Tips for Using the Agent
|
| 466 |
+
|
| 467 |
+
### 1. Be Specific
|
| 468 |
+
❌ "Tell me about compliance"
|
| 469 |
+
✅ "Analyze compliance requirements for our facial recognition system"
|
| 470 |
+
|
| 471 |
+
### 2. Provide Context
|
| 472 |
+
❌ "Is this high-risk?"
|
| 473 |
+
✅ "Is a chatbot for mental health counseling high-risk?"
|
| 474 |
+
|
| 475 |
+
### 3. Use Follow-Ups
|
| 476 |
+
Ask clarifying questions based on the agent's responses:
|
| 477 |
+
- "Can you explain that Article in more detail?"
|
| 478 |
+
- "What's the timeline for implementing this?"
|
| 479 |
+
- "How much does conformity assessment typically cost?"
|
| 480 |
+
|
| 481 |
+
### 4. Request Specifics
|
| 482 |
+
- "Generate just the transparency notice"
|
| 483 |
+
- "Focus only on Article 10 data governance requirements"
|
| 484 |
+
- "What are the penalties for non-compliance?"
|
| 485 |
+
|
| 486 |
+
### 5. Leverage Tools
|
| 487 |
+
The agent automatically uses the right tools:
|
| 488 |
+
- Organization questions → `discover_organization`
|
| 489 |
+
- System questions → `discover_ai_services`
|
| 490 |
+
- Documentation requests → `assess_compliance`
|
| 491 |
+
|
| 492 |
+
---
|
| 493 |
+
|
| 494 |
+
## 📚 Common Questions
|
| 495 |
+
|
| 496 |
+
**Q: Can it analyze my actual systems?**
|
| 497 |
+
A: With proper integration, yes. Currently it uses mock data but can be connected to your infrastructure.
|
| 498 |
+
|
| 499 |
+
**Q: Is the documentation legally binding?**
|
| 500 |
+
A: No, it provides templates and guidance. Always consult legal professionals for final documentation.
|
| 501 |
+
|
| 502 |
+
**Q: Can it help with GDPR compliance too?**
|
| 503 |
+
A: The EU AI Act intersects with GDPR. The agent provides guidance on data governance (Article 10) which aligns with GDPR.
|
| 504 |
+
|
| 505 |
+
**Q: How often should I use it?**
|
| 506 |
+
A:
|
| 507 |
+
- Initially: For assessment and planning
|
| 508 |
+
- Quarterly: For compliance reviews
|
| 509 |
+
- As needed: When launching new AI systems
|
| 510 |
+
|
| 511 |
+
**Q: Can it track multiple projects?**
|
| 512 |
+
A: Currently it's conversation-based. Consider exporting and saving assessments for different projects.
|
| 513 |
+
|
| 514 |
+
---
|
| 515 |
+
|
| 516 |
+
**Ready to try these examples?** Start the agent and copy any of the questions above! 🚀
|
| 517 |
+
|
apps/eu-ai-act-agent/QUICKSTART.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Quick Start Guide
|
| 2 |
+
|
| 3 |
+
Get the EU AI Act Compliance Agent running in under 5 minutes!
|
| 4 |
+
|
| 5 |
+
## ⚡ Fast Track
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
# 1. Install uv (fast Python package manager)
|
| 9 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 10 |
+
|
| 11 |
+
# 2. Set your API keys (required)
|
| 12 |
+
export TAVILY_API_KEY="tvly-your-tavily-key" # Required - Get from https://app.tavily.com
|
| 13 |
+
# Choose one model and set its API key:
|
| 14 |
+
export ANTHROPIC_API_KEY="sk-ant-your-key" # For Claude 4-5
|
| 15 |
+
# OR
|
| 16 |
+
export OPENAI_API_KEY="sk-your-key" # For GPT-5
|
| 17 |
+
# OR
|
| 18 |
+
export XAI_API_KEY="xai-your-key" # For Grok 4-1
|
| 19 |
+
|
| 20 |
+
# 3. Install dependencies (from workspace root)
|
| 21 |
+
cd /path/to/mcp-1st-birthday-ai-act
|
| 22 |
+
pnpm install
|
| 23 |
+
|
| 24 |
+
# 4. Install Python packages
|
| 25 |
+
cd apps/eu-ai-act-agent
|
| 26 |
+
uv pip install -r requirements.txt
|
| 27 |
+
|
| 28 |
+
# 5. Start everything (automatic!)
|
| 29 |
+
chmod +x start.sh
|
| 30 |
+
./start.sh
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
That's it! Open http://localhost:7860 🎉
|
| 34 |
+
|
| 35 |
+
## 📋 Step-by-Step Instructions
|
| 36 |
+
|
| 37 |
+
### 1. Prerequisites
|
| 38 |
+
|
| 39 |
+
Install these first:
|
| 40 |
+
- **Node.js 18+**: https://nodejs.org/
|
| 41 |
+
- **pnpm**: `npm install -g pnpm`
|
| 42 |
+
- **Python 3.9+**: https://www.python.org/
|
| 43 |
+
- **uv**: https://docs.astral.sh/uv/ (fast Python package manager)
|
| 44 |
+
- **Git**: https://git-scm.com/
|
| 45 |
+
|
| 46 |
+
### 2. Get API Keys
|
| 47 |
+
|
| 48 |
+
**Tavily (Required)**:
|
| 49 |
+
1. Sign up at https://app.tavily.com
|
| 50 |
+
2. Get your API key (1,000 free credits/month)
|
| 51 |
+
3. Copy it (starts with `tvly-`)
|
| 52 |
+
|
| 53 |
+
**Model Selection (Required - Choose One)**:
|
| 54 |
+
|
| 55 |
+
**Option A: Claude 4-5 (Anthropic)**:
|
| 56 |
+
1. Sign up at https://console.anthropic.com/
|
| 57 |
+
2. Go to API Keys section
|
| 58 |
+
3. Create a new key
|
| 59 |
+
4. Copy it (starts with `sk-ant-`)
|
| 60 |
+
|
| 61 |
+
**Option B: GPT-5 (OpenAI)**:
|
| 62 |
+
1. Sign up at https://platform.openai.com/
|
| 63 |
+
2. Go to API Keys section
|
| 64 |
+
3. Create a new key
|
| 65 |
+
4. Copy it (starts with `sk-`)
|
| 66 |
+
|
| 67 |
+
**Option C: Grok 4-1 (xAI)**:
|
| 68 |
+
1. Sign up at https://x.ai/
|
| 69 |
+
2. Go to API Keys section
|
| 70 |
+
3. Create a new key
|
| 71 |
+
4. Copy it (starts with `xai-`)
|
| 72 |
+
|
| 73 |
+
### 3. Clone & Setup
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
# Clone the repository
|
| 77 |
+
git clone <repo-url>
|
| 78 |
+
cd mcp-1st-birthday-ai-act
|
| 79 |
+
|
| 80 |
+
# Install Node.js dependencies
|
| 81 |
+
pnpm install
|
| 82 |
+
|
| 83 |
+
# Install uv (fast Python package manager)
|
| 84 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 85 |
+
|
| 86 |
+
# Go to agent directory
|
| 87 |
+
cd apps/eu-ai-act-agent
|
| 88 |
+
|
| 89 |
+
# Install Python dependencies
|
| 90 |
+
uv pip install -r requirements.txt
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
### 4. Configure Environment
|
| 94 |
+
|
| 95 |
+
Create `.env` file in the **workspace root** (not in apps/eu-ai-act-agent):
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
# Go back to workspace root
|
| 99 |
+
cd ../..
|
| 100 |
+
|
| 101 |
+
# Create .env file
|
| 102 |
+
cat > .env << EOF
|
| 103 |
+
# Required: Tavily API key
|
| 104 |
+
TAVILY_API_KEY=tvly-your-tavily-api-key-here
|
| 105 |
+
|
| 106 |
+
# Required: Choose one model and provide its API key
|
| 107 |
+
# For Claude 4-5:
|
| 108 |
+
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
| 109 |
+
# OR for GPT-5:
|
| 110 |
+
OPENAI_API_KEY=sk-your-openai-api-key-here
|
| 111 |
+
# OR for Grok 4-1:
|
| 112 |
+
XAI_API_KEY=xai-your-key-here
|
| 113 |
+
|
| 114 |
+
PORT=3001
|
| 115 |
+
EOF
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
Or copy from example:
|
| 119 |
+
```bash
|
| 120 |
+
cp .env.example .env
|
| 121 |
+
# Then edit .env with your keys
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### 5. Build MCP Server
|
| 125 |
+
|
| 126 |
+
The agent needs the MCP server tools:
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
# From workspace root
|
| 130 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### 6. Start the Agent
|
| 134 |
+
|
| 135 |
+
**Option A: Use startup script** (easiest)
|
| 136 |
+
```bash
|
| 137 |
+
cd apps/eu-ai-act-agent
|
| 138 |
+
chmod +x start.sh
|
| 139 |
+
./start.sh
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
**Option B: Manual start** (two terminals)
|
| 143 |
+
|
| 144 |
+
Terminal 1 - API Server:
|
| 145 |
+
```bash
|
| 146 |
+
cd apps/eu-ai-act-agent
|
| 147 |
+
pnpm dev
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
Terminal 2 - Gradio UI:
|
| 151 |
+
```bash
|
| 152 |
+
cd apps/eu-ai-act-agent
|
| 153 |
+
uv run src/gradio_app.py
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
**Option C: Use workspace commands**
|
| 157 |
+
```bash
|
| 158 |
+
# Terminal 1
|
| 159 |
+
pnpm --filter @eu-ai-act/agent dev
|
| 160 |
+
|
| 161 |
+
# Terminal 2
|
| 162 |
+
pnpm --filter @eu-ai-act/agent gradio
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
### 7. Open the UI
|
| 166 |
+
|
| 167 |
+
Navigate to http://localhost:7860 in your browser!
|
| 168 |
+
|
| 169 |
+
## 🎯 Try It Out
|
| 170 |
+
|
| 171 |
+
### Example 1: General Question
|
| 172 |
+
```
|
| 173 |
+
You: What is the EU AI Act?
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
The agent will explain the regulation with key details.
|
| 177 |
+
|
| 178 |
+
### Example 2: Organization Analysis
|
| 179 |
+
```
|
| 180 |
+
You: Analyze OpenAI's EU AI Act compliance
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
The agent will:
|
| 184 |
+
1. Discover OpenAI's organization profile
|
| 185 |
+
2. Identify their AI systems
|
| 186 |
+
3. Assess compliance status
|
| 187 |
+
4. Provide recommendations
|
| 188 |
+
|
| 189 |
+
### Example 3: Risk Classification
|
| 190 |
+
```
|
| 191 |
+
You: Is a recruitment screening AI high-risk?
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
The agent will classify it per Annex III and explain requirements.
|
| 195 |
+
|
| 196 |
+
### Example 4: Documentation
|
| 197 |
+
```
|
| 198 |
+
You: Generate compliance documentation for a chatbot
|
| 199 |
+
```
|
| 200 |
+
|
| 201 |
+
The agent will create:
|
| 202 |
+
- Risk assessment
|
| 203 |
+
- Technical documentation
|
| 204 |
+
- Transparency notice
|
| 205 |
+
- Compliance checklist
|
| 206 |
+
|
| 207 |
+
## 🔧 Troubleshooting
|
| 208 |
+
|
| 209 |
+
### "Cannot connect to API server"
|
| 210 |
+
|
| 211 |
+
**Solution**:
|
| 212 |
+
```bash
|
| 213 |
+
# Check if API is running
|
| 214 |
+
curl http://localhost:3001/health
|
| 215 |
+
|
| 216 |
+
# If not, start it:
|
| 217 |
+
cd apps/eu-ai-act-agent
|
| 218 |
+
pnpm dev
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
### "API key error" or "Model not found"
|
| 222 |
+
|
| 223 |
+
**Solution**:
|
| 224 |
+
```bash
|
| 225 |
+
# Verify your Tavily API key is set
|
| 226 |
+
echo $TAVILY_API_KEY
|
| 227 |
+
|
| 228 |
+
# Verify your model API key is set (check which one you're using)
|
| 229 |
+
echo $ANTHROPIC_API_KEY # For Claude 4-5
|
| 230 |
+
echo $OPENAI_API_KEY # For GPT-5
|
| 231 |
+
echo $XAI_API_KEY # For Grok 4-1
|
| 232 |
+
|
| 233 |
+
# Or check .env file
|
| 234 |
+
cat ../../.env | grep -E "(TAVILY|ANTHROPIC|OPENAI|XAI)_API_KEY"
|
| 235 |
+
|
| 236 |
+
# Make sure your API keys are valid:
|
| 237 |
+
# - Tavily: https://app.tavily.com
|
| 238 |
+
# - Claude: https://console.anthropic.com/api-keys
|
| 239 |
+
# - OpenAI: https://platform.openai.com/api-keys
|
| 240 |
+
# - xAI: https://x.ai/api-keys
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
### "Module not found" errors
|
| 244 |
+
|
| 245 |
+
**Solution**:
|
| 246 |
+
```bash
|
| 247 |
+
# Reinstall Node.js dependencies
|
| 248 |
+
cd /path/to/workspace/root
|
| 249 |
+
pnpm install
|
| 250 |
+
|
| 251 |
+
# Rebuild MCP server
|
| 252 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 253 |
+
|
| 254 |
+
# Reinstall Python packages
|
| 255 |
+
cd apps/eu-ai-act-agent
|
| 256 |
+
pip3 install -r requirements.txt
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
### Port already in use
|
| 260 |
+
|
| 261 |
+
**Solution**:
|
| 262 |
+
```bash
|
| 263 |
+
# API Server (port 3001)
|
| 264 |
+
PORT=3002 pnpm dev
|
| 265 |
+
|
| 266 |
+
# Gradio (port 7860) - edit src/gradio_app.py
|
| 267 |
+
# Change server_port=7860 to server_port=7861
|
| 268 |
+
```
|
| 269 |
+
|
| 270 |
+
### Python package issues
|
| 271 |
+
|
| 272 |
+
**Solution**:
|
| 273 |
+
```bash
|
| 274 |
+
# Install uv if not already installed
|
| 275 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 276 |
+
|
| 277 |
+
# Install dependencies with uv
|
| 278 |
+
uv pip install -r requirements.txt
|
| 279 |
+
|
| 280 |
+
# Or create and use a virtual environment with uv
|
| 281 |
+
uv venv
|
| 282 |
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
| 283 |
+
uv pip install -r requirements.txt
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
## 📊 What's Included
|
| 287 |
+
|
| 288 |
+
### Three MCP Tools
|
| 289 |
+
|
| 290 |
+
1. **discover_organization** - Profile organizations
|
| 291 |
+
- Company research via Tavily
|
| 292 |
+
- AI maturity assessment
|
| 293 |
+
- Regulatory context
|
| 294 |
+
|
| 295 |
+
2. **discover_ai_services** - Catalog AI systems
|
| 296 |
+
- Risk classification (Unacceptable/High/Limited/Minimal)
|
| 297 |
+
- Compliance status
|
| 298 |
+
- Gap analysis
|
| 299 |
+
|
| 300 |
+
3. **assess_compliance** - Generate documentation
|
| 301 |
+
- Risk management templates
|
| 302 |
+
- Technical documentation
|
| 303 |
+
- Conformity assessments
|
| 304 |
+
- Transparency notices
|
| 305 |
+
|
| 306 |
+
### Intelligent Features
|
| 307 |
+
|
| 308 |
+
- **Natural Language**: Chat in plain English
|
| 309 |
+
- **Contextual**: Remembers conversation history
|
| 310 |
+
- **Multi-Step**: Automatically chains tools
|
| 311 |
+
- **Streaming**: Real-time responses
|
| 312 |
+
- **Export**: Download generated documents
|
| 313 |
+
|
| 314 |
+
## 🎓 Learning Path
|
| 315 |
+
|
| 316 |
+
1. **Start Simple**: Ask "What is the EU AI Act?"
|
| 317 |
+
2. **Try Classification**: "Is [your AI] high-risk?"
|
| 318 |
+
3. **Explore Tools**: "Discover [company name]"
|
| 319 |
+
4. **Generate Docs**: "Create compliance documentation"
|
| 320 |
+
5. **Go Deep**: Ask about specific Articles or requirements
|
| 321 |
+
|
| 322 |
+
## 📚 Next Steps
|
| 323 |
+
|
| 324 |
+
- **Read the full README**: `cat README.md`
|
| 325 |
+
- **Check deployment guide**: `cat DEPLOYMENT.md`
|
| 326 |
+
- **Explore the MCP tools**: See `../../packages/eu-ai-act-mcp/README.md`
|
| 327 |
+
- **Learn about Vercel AI SDK**: https://ai-sdk.dev/docs
|
| 328 |
+
- **Understand Gradio**: https://gradio.app/guides/quickstart
|
| 329 |
+
|
| 330 |
+
## 💡 Tips
|
| 331 |
+
|
| 332 |
+
1. **Be specific**: "Analyze compliance for our recruitment AI" works better than "check compliance"
|
| 333 |
+
2. **Use context**: Mention company names, AI system types, industries
|
| 334 |
+
3. **Ask follow-ups**: The agent maintains conversation context
|
| 335 |
+
4. **Request docs**: Ask for specific templates or reports
|
| 336 |
+
5. **Cite articles**: Reference specific AI Act articles for detailed info
|
| 337 |
+
|
| 338 |
+
## 🆘 Getting Help
|
| 339 |
+
|
| 340 |
+
- 📖 **Full Documentation**: See README.md
|
| 341 |
+
- 🐛 **Found a bug?**: Open a GitHub issue
|
| 342 |
+
- 💬 **Questions?**: Check GitHub Discussions
|
| 343 |
+
- 📧 **Contact**: See package.json for maintainer info
|
| 344 |
+
|
| 345 |
+
## 🎯 Pro Tips
|
| 346 |
+
|
| 347 |
+
### For Developers
|
| 348 |
+
- Use `pnpm dev` for hot reload during development
|
| 349 |
+
- Check `/api/tools` endpoint to see available tools
|
| 350 |
+
- API logs show tool execution details
|
| 351 |
+
- Gradio supports custom CSS theming
|
| 352 |
+
|
| 353 |
+
### For Compliance Teams
|
| 354 |
+
- Start with organization discovery
|
| 355 |
+
- Document all AI systems systematically
|
| 356 |
+
- Focus on high-risk systems first
|
| 357 |
+
- Export and archive assessment reports
|
| 358 |
+
- Review compliance quarterly
|
| 359 |
+
|
| 360 |
+
### For Organizations
|
| 361 |
+
- Use as part of compliance workflow
|
| 362 |
+
- Train teams on EU AI Act basics
|
| 363 |
+
- Generate documentation templates
|
| 364 |
+
- Track compliance progress
|
| 365 |
+
- Prepare for audits
|
| 366 |
+
|
| 367 |
+
---
|
| 368 |
+
|
| 369 |
+
**Ready to go?** Open http://localhost:7860 and start chatting! 🚀
|
| 370 |
+
|
| 371 |
+
|
apps/eu-ai-act-agent/README.md
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: EU AI Act Compliance Agent by legitima.ai
|
| 3 |
+
emoji: ⚖️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: true
|
| 8 |
+
tags:
|
| 9 |
+
- building-mcp-track-enterprise
|
| 10 |
+
- mcp-in-action-track-enterprise
|
| 11 |
+
- modal-infernce
|
| 12 |
+
- gemini
|
| 13 |
+
- claude
|
| 14 |
+
- gpt-apps
|
| 15 |
+
- gradio-app
|
| 16 |
+
- gradio-mcp
|
| 17 |
+
- gradio-chatgpt-app
|
| 18 |
+
- gpt-oss
|
| 19 |
+
short_description: AI-powered EU AI Act compliance assessment with MCP tools
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
# 🇪🇺 EU AI Act Compliance Agent by [legitima.ai](https://legitima.ai/mcp-hackathon) powered by [decode](https://decode.gr/en)
|
| 23 |
+
|
| 24 |
+
<div align="center">
|
| 25 |
+
<img src="https://www.legitima.ai/mcp-hackathon.png" alt="Gradio MCP Hackathon - EU AI Act Compliance" width="800"/>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
> **🎂 Built for the MCP 1st Birthday Hackathon**
|
| 29 |
+
> **🔗 [Live Demo & Showcase](https://www.legitima.ai/mcp-hackathon)** - See MCP tools and agent capabilities in action!
|
| 30 |
+
|
| 31 |
+
An interactive AI agent with Gradio UI for navigating EU AI Act compliance requirements, powered by Vercel AI SDK v5 and the EU AI Act MCP Server. This project demonstrates enterprise-grade MCP tool integration with multi-model AI capabilities for regulatory compliance assessment.
|
| 32 |
+
|
| 33 |
+
## 📑 Table of Contents
|
| 34 |
+
|
| 35 |
+
- [🎯 Hackathon Submission](#hackathon-submission)
|
| 36 |
+
- [🏗️ Architecture](#architecture)
|
| 37 |
+
- [🔌 MCP Tools Integration](#mcp-tools-integration)
|
| 38 |
+
- [✨ Features](#features)
|
| 39 |
+
- [🚀 Getting Started](#getting-started)
|
| 40 |
+
- [🚀 How to Use in ChatGPT](#how-to-use-in-chatgpt)
|
| 41 |
+
- [📖 Usage Examples](#usage-examples)
|
| 42 |
+
- [🔧 Configuration](#configuration)
|
| 43 |
+
- [🛠️ Development](#development)
|
| 44 |
+
- [📚 API Reference](#api-reference)
|
| 45 |
+
- [🧪 Testing](#testing)
|
| 46 |
+
- [🎯 Tech Stack](#tech-stack)
|
| 47 |
+
|
| 48 |
+
<a id="hackathon-submission"></a>
|
| 49 |
+
|
| 50 |
+
## 🎯 Hackathon Submission
|
| 51 |
+
|
| 52 |
+
**Track 1: Building MCP** ✅ | **Track 2: MCP in Action** ✅
|
| 53 |
+
|
| 54 |
+
This submission showcases:
|
| 55 |
+
- **Custom MCP Server** with 3 specialized tools for EU AI Act compliance
|
| 56 |
+
- **Enterprise-grade Agent** using Vercel AI SDK v5 with intelligent tool orchestration
|
| 57 |
+
- **ChatGPT Apps Integration** - Deploy as a connector to use tools directly in ChatGPT ([Live MCP Server](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/))
|
| 58 |
+
- **Multi-model Support** - 6 AI models including free GPT-OSS via Modal.com
|
| 59 |
+
- **Real-world Application** - Solving critical regulatory compliance challenges
|
| 60 |
+
- **Production-ready Architecture** - Gradio UI + Express API + MCP Protocol
|
| 61 |
+
|
| 62 |
+
**🔗 Demo & Showcase:** [www.legitima.ai/mcp-hackathon](https://www.legitima.ai/mcp-hackathon)
|
| 63 |
+
**📹 Video:** [Guiddes](https://app.guidde.com/share/playlists/2wXbDrSm2YY7YnWMJbftuu?origin=wywDANMIvNhPu9kYVOXCPpdFcya2)
|
| 64 |
+
**📱 Social Media:** [LinkedIn Post 1](https://www.linkedin.com/posts/iordanis-sarafidis_mcp-1st-birthday-mcp-1st-birthday-activity-7400132272282144768-ZIir?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs)
|
| 65 |
+
|
| 66 |
+
[LinkedIn Post 2](https://www.linkedin.com/posts/billdrosatos_mcp-1st-birthday-mcp-1st-birthday-activity-7400135422502252544-C5BS?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs)
|
| 67 |
+
|
| 68 |
+
<a id="architecture"></a>
|
| 69 |
+
|
| 70 |
+
## 🏗️ Architecture
|
| 71 |
+
|
| 72 |
+
```
|
| 73 |
+
┌─────────────────────────────────────────────────────────┐
|
| 74 |
+
│ Gradio Web UI │
|
| 75 |
+
│ (Python - Interactive Chat Interface) │
|
| 76 |
+
│ Real-time streaming responses │
|
| 77 |
+
└────────────────────┬────────────────────────────────────┘
|
| 78 |
+
│ HTTP/REST
|
| 79 |
+
▼
|
| 80 |
+
┌─────────────────────────────────────────────────────────┐
|
| 81 |
+
│ Express API Server │
|
| 82 |
+
│ (Node.js + Vercel AI SDK v5) │
|
| 83 |
+
│ ┌─────────────────────────────────────────────────┐ │
|
| 84 |
+
│ │ AI Agent with Intelligent Tool Orchestration │ │
|
| 85 |
+
│ │ - Multi-model support (6 models) │ │
|
| 86 |
+
│ │ - Streaming responses │ │
|
| 87 |
+
│ │ - Contextual awareness │ │
|
| 88 |
+
│ │ - Automatic tool selection │ │
|
| 89 |
+
│ └─────────────────────────────────────────────────┘ │
|
| 90 |
+
└────────────────────┬────────────────────────────────────┘
|
| 91 |
+
│ MCP Protocol
|
| 92 |
+
▼
|
| 93 |
+
┌─────────────────────────────────────────────────────────┐
|
| 94 |
+
│ EU AI Act MCP Server (@eu-ai-act/mcp) │
|
| 95 |
+
│ ┌─────────────────────────────────────────────────┐ │
|
| 96 |
+
│ │ Tool 1: discover_organization │ │
|
| 97 |
+
│ │ • Tavily-powered web research │ │
|
| 98 |
+
│ │ • Company profiling & AI maturity │ │
|
| 99 |
+
│ │ • Regulatory context discovery │ │
|
| 100 |
+
│ └─────────────────────────────────────────────────┘ │
|
| 101 |
+
│ ┌─────────────────────────────────────────────────┐ │
|
| 102 |
+
│ │ Tool 2: discover_ai_services │ │
|
| 103 |
+
│ │ • AI systems inventory │ │
|
| 104 |
+
│ │ • Risk classification (4 tiers) │ │
|
| 105 |
+
│ │ • Compliance status tracking │ │
|
| 106 |
+
│ └─────────────────────────────────────────────────┘ │
|
| 107 |
+
│ ┌─────────────────────────────────────────────────┐ │
|
| 108 |
+
│ │ Tool 3: assess_compliance │ │
|
| 109 |
+
│ │ • AI-powered gap analysis │ │
|
| 110 |
+
│ │ • Multi-model assessment (5 models) │ │
|
| 111 |
+
│ │ • Documentation generation │ │
|
| 112 |
+
│ └─────────────────────────────────────────────────┘ │
|
| 113 |
+
└─────────────────────────────────────────────────────────┘
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
<a id="mcp-tools-integration"></a>
|
| 117 |
+
|
| 118 |
+
### 🔌 MCP Tools Integration
|
| 119 |
+
|
| 120 |
+
This agent leverages a **custom MCP server** (`@eu-ai-act/mcp-server`) that provides three specialized tools for EU AI Act compliance:
|
| 121 |
+
|
| 122 |
+
#### 1. `discover_organization` 🏢
|
| 123 |
+
- **Purpose**: Discover and profile organizations for compliance assessment
|
| 124 |
+
- **Features**:
|
| 125 |
+
- Tavily AI-powered web research for real company data
|
| 126 |
+
- AI maturity level assessment (Nascent → Expert)
|
| 127 |
+
- Regulatory context discovery (GDPR, ISO certifications)
|
| 128 |
+
- EU presence and jurisdiction analysis
|
| 129 |
+
- Compliance deadline tracking
|
| 130 |
+
- **EU AI Act References**: Articles 16, 17, 22, 49
|
| 131 |
+
|
| 132 |
+
#### 2. `discover_ai_services` 🤖
|
| 133 |
+
- **Purpose**: Inventory and classify AI systems according to EU AI Act risk tiers
|
| 134 |
+
- **Features**:
|
| 135 |
+
- Automated risk classification (Unacceptable/High/Limited/Minimal)
|
| 136 |
+
- Annex III category identification
|
| 137 |
+
- Conformity assessment requirements
|
| 138 |
+
- Technical documentation status tracking
|
| 139 |
+
- Post-market monitoring compliance
|
| 140 |
+
- **EU AI Act References**: Articles 6, 9, 10, 11, 12, 14, 43, 47, 48, 49, 72
|
| 141 |
+
|
| 142 |
+
#### 3. `assess_compliance` ⚖️
|
| 143 |
+
- **Purpose**: AI-powered compliance assessment with gap analysis and documentation generation
|
| 144 |
+
- **Features**:
|
| 145 |
+
- Multi-model AI assessment (Claude 4.5, Claude Opus, GPT-5, Grok 4.1, Gemini 3 Pro)
|
| 146 |
+
- Comprehensive gap analysis with Article references
|
| 147 |
+
- Priority-based recommendations
|
| 148 |
+
- Auto-generated documentation templates:
|
| 149 |
+
- Risk Management System (Article 9)
|
| 150 |
+
- Technical Documentation (Article 11 / Annex IV)
|
| 151 |
+
- **EU AI Act References**: Articles 9-17, 43, 49, 50, Annex IV
|
| 152 |
+
|
| 153 |
+
**📚 Full MCP Tools Documentation**: See [`packages/eu-ai-act-mcp/README.md`](../../packages/eu-ai-act-mcp/README.md) for complete tool schemas, input/output formats, and usage examples.
|
| 154 |
+
|
| 155 |
+
**💬 Use in ChatGPT**: The MCP server is deployed and ready to use as a ChatGPT App connector at [https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/) - see [How to Use in ChatGPT](#how-to-use-in-chatgpt) section below for instructions.
|
| 156 |
+
|
| 157 |
+
<a id="features"></a>
|
| 158 |
+
|
| 159 |
+
## ✨ Features
|
| 160 |
+
|
| 161 |
+
### 🤖 Intelligent AI Agent
|
| 162 |
+
- **Natural Language Interface**: Ask questions in plain English - no technical knowledge required
|
| 163 |
+
- **Contextual Awareness**: Maintains full conversation context throughout the session
|
| 164 |
+
- **Multi-Step Workflows**: Automatically orchestrates complex compliance assessments across multiple tools
|
| 165 |
+
- **Intelligent Tool Calling**: Seamlessly invokes MCP tools based on user intent and conversation flow
|
| 166 |
+
- **Streaming Responses**: Real-time AI responses with tool execution visibility
|
| 167 |
+
- **Multi-Model Support**: Choose from 6 AI models including free GPT-OSS (default)
|
| 168 |
+
|
| 169 |
+
### 📊 Compliance Capabilities
|
| 170 |
+
- **Organization Profiling**: Discover company structure, AI maturity, and regulatory context using Tavily-powered research
|
| 171 |
+
- **AI System Discovery**: Catalog and classify all AI systems with automated risk tier assignment
|
| 172 |
+
- **Risk Assessment**: Classify systems per EU AI Act (Unacceptable/High/Limited/Minimal) with Article references
|
| 173 |
+
- **Gap Analysis**: AI-powered gap identification with severity ratings, remediation effort estimates, and deadlines
|
| 174 |
+
- **Documentation Generation**: Auto-generate professional compliance templates (Risk Management, Technical Documentation)
|
| 175 |
+
- **Multi-Model Assessment**: Leverage 5 different AI models (Claude, GPT-5, Grok, Gemini) for comprehensive analysis
|
| 176 |
+
|
| 177 |
+
### 🎨 Gradio UI
|
| 178 |
+
- **Chat Interface**: Clean, modern chat experience
|
| 179 |
+
- **Streaming Responses**: Real-time AI responses
|
| 180 |
+
- **Document Preview**: View generated compliance documents
|
| 181 |
+
- **Export Options**: Download assessment reports and templates
|
| 182 |
+
- **Multi-language Support**: Available in multiple EU languages
|
| 183 |
+
|
| 184 |
+
<a id="getting-started"></a>
|
| 185 |
+
|
| 186 |
+
## 🚀 Getting Started
|
| 187 |
+
|
| 188 |
+
### Prerequisites
|
| 189 |
+
|
| 190 |
+
- **Node.js** 18+ and pnpm 8+
|
| 191 |
+
- **Python** 3.9+ with uv (fast package manager)
|
| 192 |
+
- **Tavily API key** (optional) - Get your free API key from [app.tavily.com](https://app.tavily.com) for enhanced web research
|
| 193 |
+
- **Model selection** - Choose one of the following models:
|
| 194 |
+
- 🆓 **GPT-OSS 20B** (Modal.com) - **FREE!** ✅ **DEFAULT MODEL** - (⚠️ may take up to 60s to start responding)
|
| 195 |
+
- **Claude 4.5 Sonnet** (Anthropic) - `ANTHROPIC_API_KEY` required - Faster & more precise
|
| 196 |
+
- **Claude Opus 4** (Anthropic) - `ANTHROPIC_API_KEY` required - Faster & more precise
|
| 197 |
+
- **GPT-5** (OpenAI) - `OPENAI_API_KEY` required - Faster & more precise
|
| 198 |
+
- **Grok 4.1** (xAI) - `XAI_API_KEY` required - Faster & more precise
|
| 199 |
+
- **Gemini 3 Pro** (Google) - `GOOGLE_GENERATIVE_AI_API_KEY` required - Faster & more precise
|
| 200 |
+
|
| 201 |
+
### 🆓 Free Default Model: GPT-OSS via Modal.com
|
| 202 |
+
|
| 203 |
+
**GPT-OSS 20B is the default model** - no API key required! The agent automatically uses GPT-OSS unless you select a different model in the UI.
|
| 204 |
+
|
| 205 |
+
| Feature | Details |
|
| 206 |
+
| ----------------- | ---------------------------------------------- |
|
| 207 |
+
| **Model** | OpenAI GPT-OSS 20B (open-source) |
|
| 208 |
+
| **Cost** | **FREE** (first $30/month on Modal) |
|
| 209 |
+
| **Setup** | Just provide Modal endpoint URL |
|
| 210 |
+
| **Performance** | ~$0.76/hr when running (A10G GPU) |
|
| 211 |
+
| **Response Time** | ⚠️ **May take up to 60s to start** (cold start) |
|
| 212 |
+
| **Default** | ✅ **YES** - Automatically selected |
|
| 213 |
+
|
| 214 |
+
> ⚠️ **Important:** GPT-OSS may take up to **60 seconds** to start responding due to Modal.com's cold start behavior. For **faster responses and better precision**, select another model (Claude, GPT-5, Gemini, or Grok) and provide your API key in the Gradio UI.
|
| 215 |
+
|
| 216 |
+
See [modal/README.md](../../modal/README.md) for detailed deployment instructions and GPU options.
|
| 217 |
+
|
| 218 |
+
### Installation
|
| 219 |
+
|
| 220 |
+
1. **Install Node.js dependencies**:
|
| 221 |
+
```bash
|
| 222 |
+
pnpm install
|
| 223 |
+
```
|
| 224 |
+
|
| 225 |
+
2. **Install uv and Python dependencies**:
|
| 226 |
+
```bash
|
| 227 |
+
# Install uv (if not already installed)
|
| 228 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 229 |
+
|
| 230 |
+
# Install Python dependencies
|
| 231 |
+
uv pip install -r requirements.txt
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
3. **Set up environment variables**:
|
| 235 |
+
```bash
|
| 236 |
+
cp .env.example .env
|
| 237 |
+
# Edit .env and add:
|
| 238 |
+
# - MODAL_ENDPOINT_URL (for FREE GPT-OSS - DEFAULT MODEL) - Deploy via: cd modal && modal deploy gpt_oss_inference.py
|
| 239 |
+
# - TAVILY_API_KEY (optional) - Get from https://app.tavily.com for enhanced web research
|
| 240 |
+
# - Model API key (optional - only if not using GPT-OSS):
|
| 241 |
+
# * ANTHROPIC_API_KEY (for Claude 4.5 or Claude Opus)
|
| 242 |
+
# * OPENAI_API_KEY (for GPT-5)
|
| 243 |
+
# * XAI_API_KEY (for Grok 4.1)
|
| 244 |
+
# * GOOGLE_GENERATIVE_AI_API_KEY (for Gemini 3 Pro)
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
> 💡 **Tip:**
|
| 248 |
+
> - **GPT-OSS is FREE and the default** - just set `MODAL_ENDPOINT_URL` after deploying to Modal.com
|
| 249 |
+
> - API keys and Modal endpoint can also be entered directly in the Gradio UI
|
| 250 |
+
> - Keys are securely stored in encrypted browser cookies and auto-expire after 24 hours
|
| 251 |
+
> - Modal.com offers **$30/month free credit** - perfect for trying out GPT-OSS!
|
| 252 |
+
|
| 253 |
+
### Running the Agent
|
| 254 |
+
|
| 255 |
+
**Option 1: Run everything together** (recommended)
|
| 256 |
+
```bash
|
| 257 |
+
# Terminal 1: Start the Express API server
|
| 258 |
+
pnpm dev
|
| 259 |
+
|
| 260 |
+
# Terminal 2: Start the Gradio UI
|
| 261 |
+
pnpm gradio
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
**Option 2: Manual start**
|
| 265 |
+
```bash
|
| 266 |
+
# Terminal 1: Start API server
|
| 267 |
+
cd apps/eu-ai-act-agent
|
| 268 |
+
pnpm dev
|
| 269 |
+
|
| 270 |
+
# Terminal 2: Start Gradio
|
| 271 |
+
cd apps/eu-ai-act-agent
|
| 272 |
+
uv run src/gradio_app.py
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
The Gradio UI will be available at `http://localhost:7860` 🎉
|
| 276 |
+
|
| 277 |
+
<a id="how-to-use-in-chatgpt"></a>
|
| 278 |
+
|
| 279 |
+
## 🚀 How to Use in ChatGPT
|
| 280 |
+
|
| 281 |
+
The MCP server can be deployed as a **ChatGPT App** (connector) to use EU AI Act compliance tools directly in ChatGPT conversations!
|
| 282 |
+
|
| 283 |
+
**🌐 Pre-deployed MCP Server:** The MCP server is already deployed and available at [https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/) - you can use this URL directly as a ChatGPT connector!
|
| 284 |
+
|
| 285 |
+
### Quick Start
|
| 286 |
+
|
| 287 |
+
**Option A: Use the Pre-deployed Server** (Recommended)
|
| 288 |
+
1. **Use the deployed MCP server** at [https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/)
|
| 289 |
+
2. Skip to step 2 below to configure ChatGPT
|
| 290 |
+
|
| 291 |
+
**Option B: Deploy Your Own**
|
| 292 |
+
1. **Start the ChatGPT App** with `share=True`:
|
| 293 |
+
```bash
|
| 294 |
+
cd apps/eu-ai-act-agent
|
| 295 |
+
uv run src/chatgpt_app.py
|
| 296 |
+
```
|
| 297 |
+
|
| 298 |
+
The app will automatically:
|
| 299 |
+
- Create a public URL (via Gradio's share feature)
|
| 300 |
+
- Enable MCP server mode
|
| 301 |
+
- Display the MCP server URL in the terminal
|
| 302 |
+
|
| 303 |
+
2. **Enable Developer Mode in ChatGPT**:
|
| 304 |
+
- Go to **Settings** → **Apps & Connectors** → **Advanced settings**
|
| 305 |
+
- Enable **Developer Mode**
|
| 306 |
+
|
| 307 |
+
3. **Create a Connector**:
|
| 308 |
+
- In ChatGPT, go to **Settings** → **Apps & Connectors**
|
| 309 |
+
- Click **Create Connector**
|
| 310 |
+
- Enter the MCP server URL:
|
| 311 |
+
- **Pre-deployed:** `https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/`
|
| 312 |
+
- **Or your own:** The URL from the terminal (e.g., `https://xxxxx.gradio.live`)
|
| 313 |
+
- Name it `eu-ai-act` (or your preferred name)
|
| 314 |
+
|
| 315 |
+
4. **Chat with ChatGPT using the connector**:
|
| 316 |
+
- In any ChatGPT conversation, type `@eu-ai-act` to activate the connector
|
| 317 |
+
- Ask questions like:
|
| 318 |
+
- `@eu-ai-act Analyze OpenAI's EU AI Act compliance status`
|
| 319 |
+
- `@eu-ai-act What risk category is a recruitment screening AI?`
|
| 320 |
+
- `@eu-ai-act Generate compliance documentation for our chatbot`
|
| 321 |
+
|
| 322 |
+
### Available Tools in ChatGPT
|
| 323 |
+
|
| 324 |
+
Once connected, you'll have access to all three MCP tools:
|
| 325 |
+
|
| 326 |
+
- **`discover_organization`** 🏢 - Discover and profile organizations
|
| 327 |
+
- **`discover_ai_services`** 🤖 - Inventory and classify AI systems
|
| 328 |
+
- **`assess_compliance`** ⚖️ - AI-powered compliance assessment
|
| 329 |
+
|
| 330 |
+
ChatGPT will automatically call these tools based on your conversation context!
|
| 331 |
+
|
| 332 |
+
<a id="usage-examples"></a>
|
| 333 |
+
|
| 334 |
+
## 📖 Usage Examples
|
| 335 |
+
|
| 336 |
+
### Example 1: Organization Discovery
|
| 337 |
+
```
|
| 338 |
+
You: Analyze OpenAI's EU AI Act compliance status
|
| 339 |
+
|
| 340 |
+
Agent: I'll help you assess OpenAI's compliance. Let me start by
|
| 341 |
+
discovering their organization profile...
|
| 342 |
+
|
| 343 |
+
[Discovering organization details...]
|
| 344 |
+
[Analyzing AI systems...]
|
| 345 |
+
[Assessing compliance gaps...]
|
| 346 |
+
|
| 347 |
+
OpenAI operates as a Large Enterprise with Expert AI maturity.
|
| 348 |
+
They have 4 high-risk AI systems requiring conformity assessment...
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
### Example 2: AI System Classification
|
| 352 |
+
```
|
| 353 |
+
You: What risk category is a recruitment screening AI?
|
| 354 |
+
|
| 355 |
+
Agent: Let me check the EU AI Act classification...
|
| 356 |
+
|
| 357 |
+
A recruitment screening AI is classified as HIGH RISK per
|
| 358 |
+
Annex III, Section 4(a) - AI systems used for recruitment.
|
| 359 |
+
|
| 360 |
+
Requirements include:
|
| 361 |
+
- Conformity assessment (Article 43)
|
| 362 |
+
- Technical documentation (Article 11)
|
| 363 |
+
- CE marking (Article 48)
|
| 364 |
+
- EU database registration (Article 49)
|
| 365 |
+
```
|
| 366 |
+
|
| 367 |
+
### Example 3: Document Generation
|
| 368 |
+
```
|
| 369 |
+
You: Generate compliance documentation for our chatbot
|
| 370 |
+
|
| 371 |
+
Agent: I'll assess your chatbot and generate the required documents...
|
| 372 |
+
|
| 373 |
+
[Generated documents]:
|
| 374 |
+
✓ Risk Management System (Article 9)
|
| 375 |
+
✓ Technical Documentation (Article 11)
|
| 376 |
+
|
| 377 |
+
Your chatbot is classified as Limited Risk. The documentation
|
| 378 |
+
templates are displayed in the chat and saved to the
|
| 379 |
+
compliance-docs directory.
|
| 380 |
+
```
|
| 381 |
+
|
| 382 |
+
> ⚠️ **Note on Documentation Generation:** Currently, only **2 documentation templates** are generated:
|
| 383 |
+
> - ⚡ **Risk Management System** (Article 9)
|
| 384 |
+
> - 📋 **Technical Documentation** (Article 11 / Annex IV)
|
| 385 |
+
>
|
| 386 |
+
> Additional templates (Conformity Assessment, Transparency Notice, Quality Management System, etc.) are **planned but not yet implemented** to optimize API costs and response speed during the hackathon demo.
|
| 387 |
+
|
| 388 |
+
<a id="configuration"></a>
|
| 389 |
+
|
| 390 |
+
## 🔧 Configuration
|
| 391 |
+
|
| 392 |
+
### API Server (`src/server.ts`)
|
| 393 |
+
- **Port**: Configure via `PORT` env var (default: 3001)
|
| 394 |
+
- **Model**: Select between 5 models via UI or `AI_MODEL` env var
|
| 395 |
+
- **Streaming**: Enabled for real-time responses
|
| 396 |
+
- **CORS**: Configured for Gradio origin
|
| 397 |
+
- **Required Environment Variables**:
|
| 398 |
+
- `TAVILY_API_KEY` (required for web research)
|
| 399 |
+
- One of the following (based on model selection):
|
| 400 |
+
- `ANTHROPIC_API_KEY` (for Claude 4.5 or Claude Opus)
|
| 401 |
+
- `OPENAI_API_KEY` (for GPT-5)
|
| 402 |
+
- `XAI_API_KEY` (for Grok 4.1)
|
| 403 |
+
- `GOOGLE_GENERATIVE_AI_API_KEY` (for Gemini 3 Pro)
|
| 404 |
+
|
| 405 |
+
### Gradio UI (`src/gradio_app.py`)
|
| 406 |
+
- **Theme**: Custom EU-themed design
|
| 407 |
+
- **Chat History**: Maintains full conversation context
|
| 408 |
+
- **Model Selection**: Dropdown to select AI model in real-time
|
| 409 |
+
- **Secure Key Storage**: API keys stored in encrypted browser cookies (24h expiry)
|
| 410 |
+
- **Export**: Supports markdown and PDF export (optional)
|
| 411 |
+
|
| 412 |
+
<a id="development"></a>
|
| 413 |
+
|
| 414 |
+
## 🛠️ Development
|
| 415 |
+
|
| 416 |
+
### Project Structure
|
| 417 |
+
```
|
| 418 |
+
apps/eu-ai-act-agent/
|
| 419 |
+
├── src/
|
| 420 |
+
│ ├── server.ts # Express API + Vercel AI SDK agent
|
| 421 |
+
│ ├── gradio_app.py # Gradio web interface
|
| 422 |
+
│ ├── agent/
|
| 423 |
+
│ │ ├── index.ts # Agent configuration
|
| 424 |
+
│ │ ├── tools.ts # MCP tool adapters
|
| 425 |
+
│ │ └── prompts.ts # System prompts
|
| 426 |
+
│ └── types/
|
| 427 |
+
│ └── index.ts # TypeScript types
|
| 428 |
+
├── package.json
|
| 429 |
+
├── tsconfig.json
|
| 430 |
+
└── README.md
|
| 431 |
+
```
|
| 432 |
+
|
| 433 |
+
### Building for Production
|
| 434 |
+
```bash
|
| 435 |
+
# Build the Node.js server
|
| 436 |
+
pnpm build
|
| 437 |
+
|
| 438 |
+
# Start production server
|
| 439 |
+
pnpm start
|
| 440 |
+
```
|
| 441 |
+
|
| 442 |
+
<a id="api-reference"></a>
|
| 443 |
+
|
| 444 |
+
## 📚 API Reference
|
| 445 |
+
|
| 446 |
+
### POST `/api/chat`
|
| 447 |
+
Send a chat message to the AI agent.
|
| 448 |
+
|
| 449 |
+
**Request:**
|
| 450 |
+
```json
|
| 451 |
+
{
|
| 452 |
+
"message": "Analyze my organization",
|
| 453 |
+
"history": []
|
| 454 |
+
}
|
| 455 |
+
```
|
| 456 |
+
|
| 457 |
+
**Response (Stream):**
|
| 458 |
+
```
|
| 459 |
+
data: {"type":"text","content":"Let me analyze..."}
|
| 460 |
+
data: {"type":"tool_call","tool":"discover_organization"}
|
| 461 |
+
data: {"type":"result","data":{...}}
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
<a id="testing"></a>
|
| 465 |
+
|
| 466 |
+
## 🧪 Testing
|
| 467 |
+
|
| 468 |
+
Test the agent with sample queries:
|
| 469 |
+
```bash
|
| 470 |
+
curl -X POST http://localhost:3001/api/chat \
|
| 471 |
+
-H "Content-Type: application/json" \
|
| 472 |
+
-d '{"message":"What is the EU AI Act?"}'
|
| 473 |
+
```
|
| 474 |
+
|
| 475 |
+
<a id="tech-stack"></a>
|
| 476 |
+
|
| 477 |
+
## 🎯 Tech Stack
|
| 478 |
+
|
| 479 |
+
- **Backend**: Node.js + Express + TypeScript
|
| 480 |
+
- **AI SDK**: Vercel AI SDK v5 (upgraded from v4)
|
| 481 |
+
- **LLM**: 6 models supported (user selectable via UI):
|
| 482 |
+
- 🆓 **GPT-OSS 20B** (Modal.com) - **FREE!** ✅ **DEFAULT MODEL** - No API key required! (⚠️ may take up to 60s to start)
|
| 483 |
+
- Claude 4.5 Sonnet & Claude Opus 4 (Anthropic) - Faster & more precise
|
| 484 |
+
- GPT-5 (OpenAI) - Faster & more precise
|
| 485 |
+
- Grok 4.1 (xAI) - Faster & more precise
|
| 486 |
+
- Gemini 3 Pro (Google) - Faster & more precise
|
| 487 |
+
- **Free LLM Hosting**: [Modal.com](https://modal.com) for GPT-OSS deployment
|
| 488 |
+
- **Research**: Tavily AI for web research (optional)
|
| 489 |
+
- **Frontend**: Gradio (Python)
|
| 490 |
+
- **Security**: Encrypted cookie storage for API keys (24h expiry)
|
| 491 |
+
- **MCP**: Model Context Protocol for tool integration
|
| 492 |
+
- **Monorepo**: Turborepo for efficient builds
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
<div align="center">
|
| 496 |
+
|
| 497 |
+
**Built for the MCP 1st Birthday Hackathon** 🎂
|
| 498 |
+
|
| 499 |
+
Making EU AI Act compliance accessible through conversational AI
|
| 500 |
+
|
| 501 |
+
</div>
|
| 502 |
+
|
apps/eu-ai-act-agent/biome.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
| 3 |
+
"vcs": {
|
| 4 |
+
"enabled": false,
|
| 5 |
+
"clientKind": "git",
|
| 6 |
+
"useIgnoreFile": true
|
| 7 |
+
},
|
| 8 |
+
"files": {
|
| 9 |
+
"ignoreUnknown": false,
|
| 10 |
+
"ignore": []
|
| 11 |
+
},
|
| 12 |
+
"formatter": {
|
| 13 |
+
"enabled": true,
|
| 14 |
+
"indentStyle": "space"
|
| 15 |
+
},
|
| 16 |
+
"organizeImports": {
|
| 17 |
+
"enabled": true
|
| 18 |
+
},
|
| 19 |
+
"linter": {
|
| 20 |
+
"enabled": true,
|
| 21 |
+
"rules": {
|
| 22 |
+
"recommended": false
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"javascript": {
|
| 26 |
+
"formatter": {
|
| 27 |
+
"quoteStyle": "double"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
apps/eu-ai-act-agent/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@eu-ai-act/agent",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"description": "EU AI Act Compliance Agent with Gradio UI and Vercel AI SDK v5",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"private": true,
|
| 7 |
+
"scripts": {
|
| 8 |
+
"dev": "tsx watch src/server.ts",
|
| 9 |
+
"build": "tsup",
|
| 10 |
+
"start": "node dist/server.js",
|
| 11 |
+
"typecheck": "tsc --noEmit",
|
| 12 |
+
"lint": "biome check .",
|
| 13 |
+
"lint:fix": "biome check --write .",
|
| 14 |
+
"gradio": "uv run src/gradio_app.py",
|
| 15 |
+
"chatgpt-app": "uv run src/chatgpt_app.py"
|
| 16 |
+
},
|
| 17 |
+
"dependencies": {
|
| 18 |
+
"@ai-sdk/anthropic": "^2.0.50",
|
| 19 |
+
"@ai-sdk/google": "3.0.0-beta.62",
|
| 20 |
+
"@ai-sdk/mcp": "^0.0.11",
|
| 21 |
+
"@ai-sdk/openai": "^2.0.74",
|
| 22 |
+
"@ai-sdk/provider-utils": "^2.2.8",
|
| 23 |
+
"@ai-sdk/xai": "^2.0.39",
|
| 24 |
+
"@eu-ai-act/mcp-server": "workspace:*",
|
| 25 |
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
| 26 |
+
"ai": "^5.0.104",
|
| 27 |
+
"cors": "^2.8.5",
|
| 28 |
+
"dotenv": "^17.2.3",
|
| 29 |
+
"express": "^4.21.2",
|
| 30 |
+
"zod": "^3.23.8"
|
| 31 |
+
},
|
| 32 |
+
"devDependencies": {
|
| 33 |
+
"@types/cors": "^2.8.17",
|
| 34 |
+
"@types/express": "^5.0.5",
|
| 35 |
+
"@types/node": "^22.10.2",
|
| 36 |
+
"tsup": "^8.5.1",
|
| 37 |
+
"tsx": "^4.20.6",
|
| 38 |
+
"typescript": "^5.9.3"
|
| 39 |
+
},
|
| 40 |
+
"keywords": [
|
| 41 |
+
"eu-ai-act",
|
| 42 |
+
"compliance",
|
| 43 |
+
"ai-agent",
|
| 44 |
+
"gradio",
|
| 45 |
+
"vercel-ai-sdk"
|
| 46 |
+
]
|
| 47 |
+
}
|
apps/eu-ai-act-agent/pyproject.toml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "eu-ai-act-agent"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "EU AI Act Compliance Agent with Gradio UI"
|
| 5 |
+
requires-python = ">=3.10"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"gradio[mcp]>=6.0.0",
|
| 8 |
+
"requests>=2.31.0",
|
| 9 |
+
"python-dotenv>=1.0.0",
|
| 10 |
+
]
|
| 11 |
+
|
| 12 |
+
[project.scripts]
|
| 13 |
+
gradio = "gradio_app:main"
|
| 14 |
+
|
| 15 |
+
[build-system]
|
| 16 |
+
requires = ["hatchling"]
|
| 17 |
+
build-backend = "hatchling.build"
|
| 18 |
+
|
| 19 |
+
[tool.hatch.build.targets.wheel]
|
| 20 |
+
packages = ["src"]
|
| 21 |
+
|
| 22 |
+
[tool.uv]
|
| 23 |
+
dev-dependencies = []
|
| 24 |
+
|
apps/eu-ai-act-agent/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EU AI Act Compliance Agent - Python Dependencies
|
| 2 |
+
# Gradio UI requirements
|
| 3 |
+
|
| 4 |
+
gradio[mcp]>=6.0.0
|
| 5 |
+
requests>=2.31.0
|
| 6 |
+
python-dotenv>=1.0.0
|
| 7 |
+
|
apps/eu-ai-act-agent/src/.mcp_url
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
https://c93e68e71ab8607092.gradio.live/gradio_api/mcp/
|
apps/eu-ai-act-agent/src/agent/index.ts
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* EU AI Act Compliance Agent
|
| 3 |
+
* Vercel AI SDK v5 implementation with MCP tools
|
| 4 |
+
*
|
| 5 |
+
* Uses the AI SDK MCP client to connect to the EU AI Act MCP server
|
| 6 |
+
* and retrieve tools dynamically.
|
| 7 |
+
*
|
| 8 |
+
* IMPORTANT: API keys are passed directly from Gradio UI via request headers.
|
| 9 |
+
* NEVER read API keys from environment variables!
|
| 10 |
+
*
|
| 11 |
+
* Supported Models:
|
| 12 |
+
* - gpt-oss: OpenAI GPT-OSS 20B via Modal.com (FREE - no API key needed!) - DEFAULT
|
| 13 |
+
* - claude-4.5: Anthropic Claude Sonnet 4.5 (user provides API key)
|
| 14 |
+
* - claude-opus: Anthropic Claude Opus 4 (user provides API key)
|
| 15 |
+
* - gpt-5: OpenAI GPT-5 (user provides API key)
|
| 16 |
+
* - grok-4-1: xAI Grok 4.1 Fast Reasoning (user provides API key)
|
| 17 |
+
* - gemini-3: Google Gemini 3 Pro (user provides API key)
|
| 18 |
+
*/
|
| 19 |
+
|
| 20 |
+
import { generateText, stepCountIs, streamText } from "ai";
|
| 21 |
+
import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp";
|
| 22 |
+
import { Experimental_StdioMCPTransport as StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
|
| 23 |
+
import { resolve, dirname } from "path";
|
| 24 |
+
import { fileURLToPath } from "url";
|
| 25 |
+
import { SYSTEM_PROMPT } from "./prompts.js";
|
| 26 |
+
import { getModel, type ApiKeys } from "@eu-ai-act/mcp-server";
|
| 27 |
+
|
| 28 |
+
// Re-export ApiKeys type for server.ts
|
| 29 |
+
export type { ApiKeys };
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Agent configuration passed from server
|
| 33 |
+
*/
|
| 34 |
+
export interface AgentConfig {
|
| 35 |
+
modelName: string;
|
| 36 |
+
apiKeys: ApiKeys;
|
| 37 |
+
tavilyApiKey?: string;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/**
|
| 41 |
+
* Get the system prompt for the agent
|
| 42 |
+
*/
|
| 43 |
+
function getSystemPrompt(): string {
|
| 44 |
+
return SYSTEM_PROMPT;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 48 |
+
const __dirname = dirname(__filename);
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Get path to the MCP server
|
| 52 |
+
*
|
| 53 |
+
* In production (Docker/HF Spaces): Use MCP_SERVER_PATH env var
|
| 54 |
+
* In development (tsx watch): Calculate relative path from source
|
| 55 |
+
* In local production (node dist/server.js): Calculate relative path from dist
|
| 56 |
+
*/
|
| 57 |
+
function getMCPServerPath(): string {
|
| 58 |
+
// 1. Use environment variable if set (for Docker/production deployments)
|
| 59 |
+
if (process.env.MCP_SERVER_PATH) {
|
| 60 |
+
console.log(
|
| 61 |
+
`[Agent] Using MCP_SERVER_PATH from env: ${process.env.MCP_SERVER_PATH}`,
|
| 62 |
+
);
|
| 63 |
+
return process.env.MCP_SERVER_PATH;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// 2. Calculate relative path based on whether we're running from dist or src
|
| 67 |
+
// - From src/agent/index.ts: need to go up 4 levels (agent -> src -> eu-ai-act-agent -> apps -> root)
|
| 68 |
+
// - From dist/server.js: need to go up 3 levels (dist -> eu-ai-act-agent -> apps -> root)
|
| 69 |
+
const isRunningFromDist =
|
| 70 |
+
__dirname.includes("/dist") || __dirname.endsWith("/dist");
|
| 71 |
+
const levelsUp = isRunningFromDist ? "../../../" : "../../../../";
|
| 72 |
+
const relativePath = resolve(
|
| 73 |
+
__dirname,
|
| 74 |
+
levelsUp,
|
| 75 |
+
"packages/eu-ai-act-mcp/dist/index.js",
|
| 76 |
+
);
|
| 77 |
+
|
| 78 |
+
console.log(
|
| 79 |
+
`[Agent] MCP server path (${isRunningFromDist ? "dist" : "src"}): ${relativePath}`,
|
| 80 |
+
);
|
| 81 |
+
return relativePath;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Path to the built MCP server
|
| 85 |
+
const MCP_SERVER_PATH = getMCPServerPath();
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* HIGH-RISK KEYWORDS based on EU AI Act Annex III
|
| 89 |
+
* Source: https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ:L_202401689
|
| 90 |
+
*/
|
| 91 |
+
const HIGH_RISK_KEYWORDS = [
|
| 92 |
+
// Annex III Point 8(a) - Administration of justice (LEGAL AI)
|
| 93 |
+
"legal",
|
| 94 |
+
"law",
|
| 95 |
+
"lawyer",
|
| 96 |
+
"attorney",
|
| 97 |
+
"judicial",
|
| 98 |
+
"justice",
|
| 99 |
+
"court",
|
| 100 |
+
"litigation",
|
| 101 |
+
"contract",
|
| 102 |
+
"compliance",
|
| 103 |
+
"regulatory",
|
| 104 |
+
"statute",
|
| 105 |
+
"legal advice",
|
| 106 |
+
"legal consulting",
|
| 107 |
+
"legal assistant",
|
| 108 |
+
"legal research",
|
| 109 |
+
"dispute resolution",
|
| 110 |
+
"arbitration",
|
| 111 |
+
"mediation",
|
| 112 |
+
// Annex III Point 4 - Employment
|
| 113 |
+
"recruitment",
|
| 114 |
+
"hiring",
|
| 115 |
+
"hr",
|
| 116 |
+
"human resources",
|
| 117 |
+
"employee",
|
| 118 |
+
"workforce",
|
| 119 |
+
"resume",
|
| 120 |
+
"cv",
|
| 121 |
+
"candidate",
|
| 122 |
+
"job application",
|
| 123 |
+
"termination",
|
| 124 |
+
// Annex III Point 5 - Essential services
|
| 125 |
+
"credit",
|
| 126 |
+
"scoring",
|
| 127 |
+
"loan",
|
| 128 |
+
"insurance",
|
| 129 |
+
"financial risk",
|
| 130 |
+
"creditworthiness",
|
| 131 |
+
"emergency services",
|
| 132 |
+
// Annex III Point 1 - Biometrics
|
| 133 |
+
"biometric",
|
| 134 |
+
"facial recognition",
|
| 135 |
+
"face recognition",
|
| 136 |
+
"fingerprint",
|
| 137 |
+
"identity verification",
|
| 138 |
+
"remote identification",
|
| 139 |
+
// Annex III Point 3 - Education
|
| 140 |
+
"education",
|
| 141 |
+
"student",
|
| 142 |
+
"academic",
|
| 143 |
+
"exam",
|
| 144 |
+
"grading",
|
| 145 |
+
"admission",
|
| 146 |
+
// Annex III Point 6 - Law enforcement
|
| 147 |
+
"law enforcement",
|
| 148 |
+
"police",
|
| 149 |
+
"crime",
|
| 150 |
+
"profiling",
|
| 151 |
+
"polygraph",
|
| 152 |
+
// Annex III Point 2 - Critical infrastructure
|
| 153 |
+
"critical infrastructure",
|
| 154 |
+
"safety component",
|
| 155 |
+
"water supply",
|
| 156 |
+
"gas supply",
|
| 157 |
+
"electricity",
|
| 158 |
+
"transport",
|
| 159 |
+
// Annex III Point 5(b) - Healthcare
|
| 160 |
+
"healthcare",
|
| 161 |
+
"medical",
|
| 162 |
+
"diagnosis",
|
| 163 |
+
"clinical",
|
| 164 |
+
"patient",
|
| 165 |
+
"health",
|
| 166 |
+
];
|
| 167 |
+
|
| 168 |
+
/**
|
| 169 |
+
* Validate risk classification for a system based on EU AI Act Annex III
|
| 170 |
+
* Ensures legal AI systems are correctly classified as HIGH RISK per Point 8(a)
|
| 171 |
+
*/
|
| 172 |
+
function validateSystemRiskClassification(system: any): any {
|
| 173 |
+
if (!system) return system;
|
| 174 |
+
|
| 175 |
+
const name = (system.system?.name || system.name || "").toLowerCase();
|
| 176 |
+
const description = (
|
| 177 |
+
system.system?.description ||
|
| 178 |
+
system.description ||
|
| 179 |
+
""
|
| 180 |
+
).toLowerCase();
|
| 181 |
+
const purpose = (
|
| 182 |
+
system.system?.intendedPurpose ||
|
| 183 |
+
system.intendedPurpose ||
|
| 184 |
+
""
|
| 185 |
+
).toLowerCase();
|
| 186 |
+
const contextString = `${name} ${description} ${purpose}`;
|
| 187 |
+
|
| 188 |
+
// Check for legal AI indicators (Annex III Point 8(a))
|
| 189 |
+
const isLegalAI =
|
| 190 |
+
contextString.includes("legal") ||
|
| 191 |
+
contextString.includes("law") ||
|
| 192 |
+
contextString.includes("lawyer") ||
|
| 193 |
+
contextString.includes("attorney") ||
|
| 194 |
+
contextString.includes("judicial") ||
|
| 195 |
+
contextString.includes("justice") ||
|
| 196 |
+
contextString.includes("court") ||
|
| 197 |
+
contextString.includes("litigation") ||
|
| 198 |
+
contextString.includes("contract review") ||
|
| 199 |
+
contextString.includes("compliance advi") ||
|
| 200 |
+
contextString.includes("regulatory advi");
|
| 201 |
+
|
| 202 |
+
// Check for other high-risk keywords
|
| 203 |
+
const matchedHighRiskKeywords = HIGH_RISK_KEYWORDS.filter((keyword) =>
|
| 204 |
+
contextString.includes(keyword.toLowerCase()),
|
| 205 |
+
);
|
| 206 |
+
const hasHighRiskKeywords = matchedHighRiskKeywords.length > 0;
|
| 207 |
+
|
| 208 |
+
const rc = system.riskClassification || {};
|
| 209 |
+
let needsCorrection = false;
|
| 210 |
+
|
| 211 |
+
if (isLegalAI && rc.category !== "High" && rc.category !== "Unacceptable") {
|
| 212 |
+
console.log(
|
| 213 |
+
`⚠️ [Agent Risk Validation] Legal AI detected - correcting "${rc.category}" to "High"`,
|
| 214 |
+
);
|
| 215 |
+
console.log(` System: ${name}`);
|
| 216 |
+
console.log(
|
| 217 |
+
` Reason: Legal AI per EU AI Act Annex III Point 8(a) - Administration of justice`,
|
| 218 |
+
);
|
| 219 |
+
rc.category = "High";
|
| 220 |
+
rc.annexIIICategory =
|
| 221 |
+
"Annex III, Point 8(a) - Administration of justice and democratic processes";
|
| 222 |
+
rc.justification =
|
| 223 |
+
"AI system providing legal assistance, consulting, or advice. Per EU AI Act Annex III Point 8(a), such systems are HIGH RISK.";
|
| 224 |
+
rc.riskScore = Math.max(rc.riskScore || 0, 85);
|
| 225 |
+
rc.conformityAssessmentRequired = true;
|
| 226 |
+
rc.conformityAssessmentType = "Internal Control";
|
| 227 |
+
needsCorrection = true;
|
| 228 |
+
} else if (
|
| 229 |
+
hasHighRiskKeywords &&
|
| 230 |
+
rc.category !== "High" &&
|
| 231 |
+
rc.category !== "Unacceptable"
|
| 232 |
+
) {
|
| 233 |
+
console.log(
|
| 234 |
+
`⚠️ [Agent Risk Validation] High-risk keywords detected - correcting "${rc.category}" to "High"`,
|
| 235 |
+
);
|
| 236 |
+
console.log(` System: ${name}`);
|
| 237 |
+
console.log(
|
| 238 |
+
` Keywords: ${matchedHighRiskKeywords.slice(0, 3).join(", ")}`,
|
| 239 |
+
);
|
| 240 |
+
rc.category = "High";
|
| 241 |
+
rc.riskScore = Math.max(rc.riskScore || 0, 75);
|
| 242 |
+
rc.conformityAssessmentRequired = true;
|
| 243 |
+
rc.conformityAssessmentType = "Internal Control";
|
| 244 |
+
needsCorrection = true;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
if (needsCorrection) {
|
| 248 |
+
return {
|
| 249 |
+
...system,
|
| 250 |
+
riskClassification: rc,
|
| 251 |
+
};
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
return system;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
/**
|
| 258 |
+
* Validate all systems in assess_compliance or discover_ai_services results
|
| 259 |
+
*/
|
| 260 |
+
function validateToolResult(toolName: string, result: any): any {
|
| 261 |
+
if (!result) return result;
|
| 262 |
+
|
| 263 |
+
if (toolName === "assess_compliance" || toolName === "discover_ai_services") {
|
| 264 |
+
// Check if result has systems array
|
| 265 |
+
if (result.systems && Array.isArray(result.systems)) {
|
| 266 |
+
result.systems = result.systems.map((s: any) =>
|
| 267 |
+
validateSystemRiskClassification(s),
|
| 268 |
+
);
|
| 269 |
+
|
| 270 |
+
// Recalculate risk summary
|
| 271 |
+
if (result.riskSummary) {
|
| 272 |
+
result.riskSummary = {
|
| 273 |
+
...result.riskSummary,
|
| 274 |
+
highRiskCount: result.systems.filter(
|
| 275 |
+
(s: any) => s.riskClassification?.category === "High",
|
| 276 |
+
).length,
|
| 277 |
+
limitedRiskCount: result.systems.filter(
|
| 278 |
+
(s: any) => s.riskClassification?.category === "Limited",
|
| 279 |
+
).length,
|
| 280 |
+
minimalRiskCount: result.systems.filter(
|
| 281 |
+
(s: any) => s.riskClassification?.category === "Minimal",
|
| 282 |
+
).length,
|
| 283 |
+
unacceptableRiskCount: result.systems.filter(
|
| 284 |
+
(s: any) => s.riskClassification?.category === "Unacceptable",
|
| 285 |
+
).length,
|
| 286 |
+
};
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
// Check assessment for gaps related to legal systems
|
| 291 |
+
if (result.assessment?.gaps) {
|
| 292 |
+
// Ensure legal AI systems have appropriate gaps flagged
|
| 293 |
+
}
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
return result;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
// getModel is now imported from @eu-ai-act/mcp-server
|
| 300 |
+
|
| 301 |
+
/**
|
| 302 |
+
* Create MCP client and retrieve tools
|
| 303 |
+
* Passes API keys to the MCP server for tool execution
|
| 304 |
+
*/
|
| 305 |
+
async function createMCPClientWithTools(
|
| 306 |
+
apiKeys: ApiKeys,
|
| 307 |
+
modelName: string,
|
| 308 |
+
tavilyApiKey?: string,
|
| 309 |
+
) {
|
| 310 |
+
// Pass API keys to MCP server child process via environment
|
| 311 |
+
// MCP tools need these for Tavily research and AI model calls
|
| 312 |
+
// IMPORTANT: These come from Gradio UI user input - NEVER from process.env!
|
| 313 |
+
const env: Record<string, string> = {
|
| 314 |
+
// Only pass MCP_SERVER_PATH and NODE_ENV, plus API keys
|
| 315 |
+
NODE_ENV: process.env.NODE_ENV || "production",
|
| 316 |
+
};
|
| 317 |
+
|
| 318 |
+
// Pass MCP server path if set
|
| 319 |
+
if (process.env.MCP_SERVER_PATH) {
|
| 320 |
+
env.MCP_SERVER_PATH = process.env.MCP_SERVER_PATH;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// Pass model name from Gradio UI
|
| 324 |
+
env.AI_MODEL = modelName;
|
| 325 |
+
|
| 326 |
+
// Pass Tavily API key: User-provided (from Gradio UI) takes priority,
|
| 327 |
+
// otherwise fallback to server's env var (HF Spaces secret)
|
| 328 |
+
if (tavilyApiKey) {
|
| 329 |
+
env.TAVILY_API_KEY = tavilyApiKey;
|
| 330 |
+
console.log("[Agent] Using Tavily API key from Gradio UI (user-provided)");
|
| 331 |
+
} else if (process.env.TAVILY_API_KEY) {
|
| 332 |
+
// Fallback to server's TAVILY_API_KEY (HF Spaces secret)
|
| 333 |
+
env.TAVILY_API_KEY = process.env.TAVILY_API_KEY;
|
| 334 |
+
console.log(
|
| 335 |
+
"[Agent] Using Tavily API key from server env (HF Spaces secret)",
|
| 336 |
+
);
|
| 337 |
+
} else {
|
| 338 |
+
console.log(
|
| 339 |
+
"[Agent] No Tavily API key available - will use AI model fallback for research",
|
| 340 |
+
);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// Pass API keys from user (via Gradio UI) to MCP server
|
| 344 |
+
// These are used by MCP tools for Tavily research and AI model calls
|
| 345 |
+
if (apiKeys.openaiApiKey) env.OPENAI_API_KEY = apiKeys.openaiApiKey;
|
| 346 |
+
if (apiKeys.anthropicApiKey) env.ANTHROPIC_API_KEY = apiKeys.anthropicApiKey;
|
| 347 |
+
if (apiKeys.googleApiKey)
|
| 348 |
+
env.GOOGLE_GENERATIVE_AI_API_KEY = apiKeys.googleApiKey;
|
| 349 |
+
if (apiKeys.xaiApiKey) env.XAI_API_KEY = apiKeys.xaiApiKey;
|
| 350 |
+
if (apiKeys.modalEndpointUrl)
|
| 351 |
+
env.MODAL_ENDPOINT_URL = apiKeys.modalEndpointUrl;
|
| 352 |
+
|
| 353 |
+
const transport = new StdioMCPTransport({
|
| 354 |
+
command: "node",
|
| 355 |
+
args: [MCP_SERVER_PATH],
|
| 356 |
+
env,
|
| 357 |
+
});
|
| 358 |
+
|
| 359 |
+
const client = await createMCPClient({ transport });
|
| 360 |
+
const tools = await client.tools();
|
| 361 |
+
|
| 362 |
+
console.log("[Agent] MCP client created");
|
| 363 |
+
|
| 364 |
+
return { client, tools };
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/**
|
| 368 |
+
* Create EU AI Act compliance agent
|
| 369 |
+
*
|
| 370 |
+
* @param config - Agent configuration with model name and API keys from Gradio UI
|
| 371 |
+
*/
|
| 372 |
+
export function createAgent(config: AgentConfig) {
|
| 373 |
+
const { modelName, apiKeys, tavilyApiKey } = config;
|
| 374 |
+
|
| 375 |
+
// Log the model being used
|
| 376 |
+
console.log(`[Agent] Creating agent with model: ${modelName}`);
|
| 377 |
+
const model = getModel(modelName, apiKeys, "agent");
|
| 378 |
+
console.log(`[Agent] Model instance created: ${model.constructor.name}`);
|
| 379 |
+
return {
|
| 380 |
+
/**
|
| 381 |
+
* Generate a single response
|
| 382 |
+
*/
|
| 383 |
+
async generateText(params: { messages: any[] }) {
|
| 384 |
+
const { client, tools } = await createMCPClientWithTools(
|
| 385 |
+
apiKeys,
|
| 386 |
+
modelName,
|
| 387 |
+
tavilyApiKey,
|
| 388 |
+
);
|
| 389 |
+
|
| 390 |
+
try {
|
| 391 |
+
const systemPrompt = getSystemPrompt();
|
| 392 |
+
const isGptOss = modelName.toLowerCase().includes("gpt-oss");
|
| 393 |
+
|
| 394 |
+
// Build provider options based on model
|
| 395 |
+
// GPT-OSS (vLLM on Modal) doesn't support reasoningEffort parameter
|
| 396 |
+
const providerOptions = isGptOss
|
| 397 |
+
? undefined
|
| 398 |
+
: {
|
| 399 |
+
anthropic: {
|
| 400 |
+
thinking: { type: "enabled", budgetTokens: 2000 }, // Minimal thinking budget for Claude
|
| 401 |
+
},
|
| 402 |
+
openai: {
|
| 403 |
+
reasoningEffort: "low", // Low reasoning effort for GPT - faster responses
|
| 404 |
+
},
|
| 405 |
+
google: {
|
| 406 |
+
thinkingConfig: {
|
| 407 |
+
thinkingLevel: "low", // Low thinking for faster responses
|
| 408 |
+
includeThoughts: true,
|
| 409 |
+
},
|
| 410 |
+
},
|
| 411 |
+
};
|
| 412 |
+
|
| 413 |
+
const result = await generateText({
|
| 414 |
+
model,
|
| 415 |
+
messages: [
|
| 416 |
+
{ role: "system", content: systemPrompt },
|
| 417 |
+
...params.messages,
|
| 418 |
+
],
|
| 419 |
+
// MCP tools are compatible at runtime but have different TypeScript types
|
| 420 |
+
tools: tools as any,
|
| 421 |
+
// stop when at least three tools runned and response is generated
|
| 422 |
+
stopWhen: stepCountIs(3),
|
| 423 |
+
providerOptions,
|
| 424 |
+
});
|
| 425 |
+
|
| 426 |
+
// Output tool results with detailed information
|
| 427 |
+
if (result.steps) {
|
| 428 |
+
for (const step of result.steps) {
|
| 429 |
+
if (step.toolResults && step.toolResults.length > 0) {
|
| 430 |
+
for (const toolResult of step.toolResults) {
|
| 431 |
+
console.log(`\n📋 Tool Result: ${toolResult.toolName}`);
|
| 432 |
+
console.log("─".repeat(50));
|
| 433 |
+
|
| 434 |
+
try {
|
| 435 |
+
// Access result safely - TypedToolResult may have result in different property
|
| 436 |
+
const resultValue =
|
| 437 |
+
(toolResult as any).result ??
|
| 438 |
+
(toolResult as any).output ??
|
| 439 |
+
toolResult;
|
| 440 |
+
let parsed =
|
| 441 |
+
typeof resultValue === "string"
|
| 442 |
+
? JSON.parse(resultValue)
|
| 443 |
+
: resultValue;
|
| 444 |
+
|
| 445 |
+
// Validate and correct risk classifications per EU AI Act Annex III
|
| 446 |
+
parsed = validateToolResult(toolResult.toolName, parsed);
|
| 447 |
+
|
| 448 |
+
// Handle assess_compliance results specially to show documentation
|
| 449 |
+
if (toolResult.toolName === "assess_compliance" && parsed) {
|
| 450 |
+
if (parsed.assessment) {
|
| 451 |
+
console.log(
|
| 452 |
+
`📊 Compliance Score: ${parsed.assessment.overallScore}/100`,
|
| 453 |
+
);
|
| 454 |
+
console.log(
|
| 455 |
+
`⚠️ Risk Level: ${parsed.assessment.riskLevel}`,
|
| 456 |
+
);
|
| 457 |
+
console.log(
|
| 458 |
+
`🔍 Gaps Found: ${parsed.assessment.gaps?.length || 0}`,
|
| 459 |
+
);
|
| 460 |
+
console.log(
|
| 461 |
+
`💡 Recommendations: ${parsed.assessment.recommendations?.length || 0}`,
|
| 462 |
+
);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// Show documentation templates
|
| 466 |
+
if (parsed.documentation) {
|
| 467 |
+
console.log(`\n📄 Documentation Templates Generated:`);
|
| 468 |
+
const docs = parsed.documentation;
|
| 469 |
+
if (docs.riskManagementTemplate)
|
| 470 |
+
console.log(" ✓ Risk Management System (Article 9)");
|
| 471 |
+
if (docs.technicalDocumentation)
|
| 472 |
+
console.log(
|
| 473 |
+
" ✓ Technical Documentation (Article 11)",
|
| 474 |
+
);
|
| 475 |
+
if (docs.conformityAssessment)
|
| 476 |
+
console.log(" ✓ Conformity Assessment (Article 43)");
|
| 477 |
+
if (docs.transparencyNotice)
|
| 478 |
+
console.log(" ✓ Transparency Notice (Article 50)");
|
| 479 |
+
if (docs.qualityManagementSystem)
|
| 480 |
+
console.log(
|
| 481 |
+
" ✓ Quality Management System (Article 17)",
|
| 482 |
+
);
|
| 483 |
+
if (docs.humanOversightProcedure)
|
| 484 |
+
console.log(
|
| 485 |
+
" ✓ Human Oversight Procedure (Article 14)",
|
| 486 |
+
);
|
| 487 |
+
if (docs.dataGovernancePolicy)
|
| 488 |
+
console.log(" ✓ Data Governance Policy (Article 10)");
|
| 489 |
+
if (docs.incidentReportingProcedure)
|
| 490 |
+
console.log(" ✓ Incident Reporting Procedure");
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
// Show documentation files
|
| 494 |
+
if (
|
| 495 |
+
parsed.metadata?.documentationFiles &&
|
| 496 |
+
parsed.metadata.documentationFiles.length > 0
|
| 497 |
+
) {
|
| 498 |
+
console.log(`\n💾 Documentation Files Saved:`);
|
| 499 |
+
for (const filePath of parsed.metadata
|
| 500 |
+
.documentationFiles) {
|
| 501 |
+
console.log(` 📄 ${filePath}`);
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
if (parsed.metadata) {
|
| 506 |
+
console.log(
|
| 507 |
+
`\n🤖 Model Used: ${parsed.metadata.modelUsed}`,
|
| 508 |
+
);
|
| 509 |
+
}
|
| 510 |
+
} else if (
|
| 511 |
+
toolResult.toolName === "discover_ai_services" &&
|
| 512 |
+
parsed
|
| 513 |
+
) {
|
| 514 |
+
// Validate systems in discovery results
|
| 515 |
+
if (parsed.riskSummary) {
|
| 516 |
+
console.log(
|
| 517 |
+
`🤖 Total Systems: ${parsed.riskSummary.totalCount}`,
|
| 518 |
+
);
|
| 519 |
+
console.log(
|
| 520 |
+
` High-Risk: ${parsed.riskSummary.highRiskCount}`,
|
| 521 |
+
);
|
| 522 |
+
console.log(
|
| 523 |
+
` Limited-Risk: ${parsed.riskSummary.limitedRiskCount}`,
|
| 524 |
+
);
|
| 525 |
+
console.log(
|
| 526 |
+
` Minimal-Risk: ${parsed.riskSummary.minimalRiskCount}`,
|
| 527 |
+
);
|
| 528 |
+
}
|
| 529 |
+
// Show any legal AI systems detected
|
| 530 |
+
if (parsed.systems) {
|
| 531 |
+
const legalSystems = parsed.systems.filter((s: any) => {
|
| 532 |
+
const ctx =
|
| 533 |
+
`${s.system?.name || ""} ${s.system?.intendedPurpose || ""}`.toLowerCase();
|
| 534 |
+
return (
|
| 535 |
+
ctx.includes("legal") ||
|
| 536 |
+
ctx.includes("law") ||
|
| 537 |
+
ctx.includes("judicial")
|
| 538 |
+
);
|
| 539 |
+
});
|
| 540 |
+
if (legalSystems.length > 0) {
|
| 541 |
+
console.log(
|
| 542 |
+
`\n⚖️ Legal AI Systems (HIGH RISK per Annex III Point 8(a)):`,
|
| 543 |
+
);
|
| 544 |
+
for (const sys of legalSystems) {
|
| 545 |
+
console.log(
|
| 546 |
+
` - ${sys.system?.name}: ${sys.riskClassification?.category}`,
|
| 547 |
+
);
|
| 548 |
+
}
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
} else if (
|
| 552 |
+
toolResult.toolName === "discover_organization" &&
|
| 553 |
+
parsed
|
| 554 |
+
) {
|
| 555 |
+
if (parsed.organization) {
|
| 556 |
+
console.log(
|
| 557 |
+
`🏢 Organization: ${parsed.organization.name}`,
|
| 558 |
+
);
|
| 559 |
+
console.log(`📍 Sector: ${parsed.organization.sector}`);
|
| 560 |
+
console.log(
|
| 561 |
+
`🌍 EU Presence: ${parsed.organization.euPresence}`,
|
| 562 |
+
);
|
| 563 |
+
}
|
| 564 |
+
}
|
| 565 |
+
} catch {
|
| 566 |
+
// If not JSON, just show raw result summary
|
| 567 |
+
const resultValue =
|
| 568 |
+
(toolResult as any).result ??
|
| 569 |
+
(toolResult as any).output ??
|
| 570 |
+
toolResult;
|
| 571 |
+
const resultStr = String(resultValue);
|
| 572 |
+
console.log(
|
| 573 |
+
`Result: ${resultStr.substring(0, 200)}${resultStr.length > 200 ? "..." : ""}`,
|
| 574 |
+
);
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
console.log("─".repeat(50));
|
| 578 |
+
}
|
| 579 |
+
}
|
| 580 |
+
}
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
return result;
|
| 584 |
+
} finally {
|
| 585 |
+
await client.close();
|
| 586 |
+
}
|
| 587 |
+
},
|
| 588 |
+
|
| 589 |
+
/**
|
| 590 |
+
* Stream a response with MCP tools
|
| 591 |
+
*/
|
| 592 |
+
async streamText(params: { messages: any[] }) {
|
| 593 |
+
const { client, tools } = await createMCPClientWithTools(
|
| 594 |
+
apiKeys,
|
| 595 |
+
modelName,
|
| 596 |
+
tavilyApiKey,
|
| 597 |
+
);
|
| 598 |
+
const systemPrompt = getSystemPrompt();
|
| 599 |
+
const isGptOss = modelName.toLowerCase().includes("gpt-oss");
|
| 600 |
+
|
| 601 |
+
// Build provider options based on model
|
| 602 |
+
// GPT-OSS (vLLM on Modal) doesn't support reasoningEffort parameter
|
| 603 |
+
const providerOptions = isGptOss
|
| 604 |
+
? undefined
|
| 605 |
+
: {
|
| 606 |
+
anthropic: {
|
| 607 |
+
thinking: { type: "enabled", budgetTokens: 2000 }, // Minimal thinking budget for Claude
|
| 608 |
+
},
|
| 609 |
+
openai: {
|
| 610 |
+
reasoningEffort: "low", // Low reasoning effort for GPT - faster responses
|
| 611 |
+
},
|
| 612 |
+
google: {
|
| 613 |
+
thinkingConfig: {
|
| 614 |
+
thinkingLevel: "low", // Low thinking for faster responses
|
| 615 |
+
includeThoughts: true,
|
| 616 |
+
},
|
| 617 |
+
},
|
| 618 |
+
};
|
| 619 |
+
|
| 620 |
+
// For GPT-OSS (vLLM on Modal), we must set explicit maxOutputTokens
|
| 621 |
+
// The context window is 16k, and the system prompt + tools take ~6-8k tokens
|
| 622 |
+
// Setting explicit maxOutputTokens prevents vLLM from calculating negative values
|
| 623 |
+
// 8000 tokens allows for comprehensive compliance reports
|
| 624 |
+
const maxOutputTokens = isGptOss ? 8000 : undefined;
|
| 625 |
+
|
| 626 |
+
const result = streamText({
|
| 627 |
+
model,
|
| 628 |
+
maxOutputTokens,
|
| 629 |
+
messages: [
|
| 630 |
+
{ role: "system", content: systemPrompt },
|
| 631 |
+
...params.messages,
|
| 632 |
+
],
|
| 633 |
+
// MCP tools are compatible at runtime but have different TypeScript types
|
| 634 |
+
tools: tools as any,
|
| 635 |
+
// Increase maxSteps to ensure all 3 tools + final response
|
| 636 |
+
stopWhen: stepCountIs(3),
|
| 637 |
+
providerOptions,
|
| 638 |
+
// Log each step for debugging
|
| 639 |
+
onStepFinish: async (step) => {
|
| 640 |
+
// Output tool results with detailed information
|
| 641 |
+
if (step.toolResults && step.toolResults.length > 0) {
|
| 642 |
+
for (const toolResult of step.toolResults) {
|
| 643 |
+
console.log(`\n📋 Tool Result: ${toolResult.toolName}`);
|
| 644 |
+
console.log("─".repeat(50));
|
| 645 |
+
|
| 646 |
+
try {
|
| 647 |
+
// Access result safely - TypedToolResult may have result in different property
|
| 648 |
+
const resultValue =
|
| 649 |
+
(toolResult as any).result ??
|
| 650 |
+
(toolResult as any).output ??
|
| 651 |
+
toolResult;
|
| 652 |
+
let result =
|
| 653 |
+
typeof resultValue === "string"
|
| 654 |
+
? JSON.parse(resultValue)
|
| 655 |
+
: resultValue;
|
| 656 |
+
|
| 657 |
+
// Validate and correct risk classifications per EU AI Act Annex III
|
| 658 |
+
result = validateToolResult(toolResult.toolName, result);
|
| 659 |
+
|
| 660 |
+
// Handle assess_compliance results specially to show documentation
|
| 661 |
+
if (toolResult.toolName === "assess_compliance" && result) {
|
| 662 |
+
if (result.assessment) {
|
| 663 |
+
console.log(
|
| 664 |
+
`📊 Compliance Score: ${result.assessment.overallScore}/100`,
|
| 665 |
+
);
|
| 666 |
+
console.log(
|
| 667 |
+
`⚠️ Risk Level: ${result.assessment.riskLevel}`,
|
| 668 |
+
);
|
| 669 |
+
console.log(
|
| 670 |
+
`🔍 Gaps Found: ${result.assessment.gaps?.length || 0}`,
|
| 671 |
+
);
|
| 672 |
+
console.log(
|
| 673 |
+
`💡 Recommendations: ${result.assessment.recommendations?.length || 0}`,
|
| 674 |
+
);
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
// Show documentation templates
|
| 678 |
+
if (result.documentation) {
|
| 679 |
+
console.log(`\n📄 Documentation Templates Generated:`);
|
| 680 |
+
const docs = result.documentation;
|
| 681 |
+
if (docs.riskManagementTemplate)
|
| 682 |
+
console.log(" ✓ Risk Management System (Article 9)");
|
| 683 |
+
if (docs.technicalDocumentation)
|
| 684 |
+
console.log(" ✓ Technical Documentation (Article 11)");
|
| 685 |
+
if (docs.conformityAssessment)
|
| 686 |
+
console.log(" ✓ Conformity Assessment (Article 43)");
|
| 687 |
+
if (docs.transparencyNotice)
|
| 688 |
+
console.log(" ✓ Transparency Notice (Article 50)");
|
| 689 |
+
if (docs.qualityManagementSystem)
|
| 690 |
+
console.log(
|
| 691 |
+
" ✓ Quality Management System (Article 17)",
|
| 692 |
+
);
|
| 693 |
+
if (docs.humanOversightProcedure)
|
| 694 |
+
console.log(
|
| 695 |
+
" ✓ Human Oversight Procedure (Article 14)",
|
| 696 |
+
);
|
| 697 |
+
if (docs.dataGovernancePolicy)
|
| 698 |
+
console.log(" ✓ Data Governance Policy (Article 10)");
|
| 699 |
+
if (docs.incidentReportingProcedure)
|
| 700 |
+
console.log(" ✓ Incident Reporting Procedure");
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
// Show documentation files
|
| 704 |
+
if (
|
| 705 |
+
result.metadata?.documentationFiles &&
|
| 706 |
+
result.metadata.documentationFiles.length > 0
|
| 707 |
+
) {
|
| 708 |
+
console.log(`\n💾 Documentation Files Saved:`);
|
| 709 |
+
for (const filePath of result.metadata.documentationFiles) {
|
| 710 |
+
console.log(` 📄 ${filePath}`);
|
| 711 |
+
}
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
if (result.metadata) {
|
| 715 |
+
console.log(
|
| 716 |
+
`\n🤖 Model Used: ${result.metadata.modelUsed}`,
|
| 717 |
+
);
|
| 718 |
+
}
|
| 719 |
+
} else if (
|
| 720 |
+
toolResult.toolName === "discover_ai_services" &&
|
| 721 |
+
result
|
| 722 |
+
) {
|
| 723 |
+
// Validate systems in discovery results
|
| 724 |
+
if (result.riskSummary) {
|
| 725 |
+
console.log(
|
| 726 |
+
`🤖 Total Systems: ${result.riskSummary.totalCount}`,
|
| 727 |
+
);
|
| 728 |
+
console.log(
|
| 729 |
+
` High-Risk: ${result.riskSummary.highRiskCount}`,
|
| 730 |
+
);
|
| 731 |
+
console.log(
|
| 732 |
+
` Limited-Risk: ${result.riskSummary.limitedRiskCount}`,
|
| 733 |
+
);
|
| 734 |
+
console.log(
|
| 735 |
+
` Minimal-Risk: ${result.riskSummary.minimalRiskCount}`,
|
| 736 |
+
);
|
| 737 |
+
}
|
| 738 |
+
// Show any legal AI systems detected
|
| 739 |
+
if (result.systems) {
|
| 740 |
+
const legalSystems = result.systems.filter((s: any) => {
|
| 741 |
+
const ctx =
|
| 742 |
+
`${s.system?.name || ""} ${s.system?.intendedPurpose || ""}`.toLowerCase();
|
| 743 |
+
return (
|
| 744 |
+
ctx.includes("legal") ||
|
| 745 |
+
ctx.includes("law") ||
|
| 746 |
+
ctx.includes("judicial")
|
| 747 |
+
);
|
| 748 |
+
});
|
| 749 |
+
if (legalSystems.length > 0) {
|
| 750 |
+
console.log(
|
| 751 |
+
`\n⚖️ Legal AI Systems (HIGH RISK per Annex III Point 8(a)):`,
|
| 752 |
+
);
|
| 753 |
+
for (const sys of legalSystems) {
|
| 754 |
+
console.log(
|
| 755 |
+
` - ${sys.system?.name}: ${sys.riskClassification?.category}`,
|
| 756 |
+
);
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
} else if (
|
| 761 |
+
toolResult.toolName === "discover_organization" &&
|
| 762 |
+
result
|
| 763 |
+
) {
|
| 764 |
+
// Show organization discovery summary
|
| 765 |
+
if (result.organization) {
|
| 766 |
+
console.log(`🏢 Organization: ${result.organization.name}`);
|
| 767 |
+
console.log(`📍 Sector: ${result.organization.sector}`);
|
| 768 |
+
console.log(
|
| 769 |
+
`🌍 EU Presence: ${result.organization.euPresence}`,
|
| 770 |
+
);
|
| 771 |
+
}
|
| 772 |
+
}
|
| 773 |
+
} catch {
|
| 774 |
+
// If not JSON, just show raw result summary
|
| 775 |
+
const resultValue =
|
| 776 |
+
(toolResult as any).result ??
|
| 777 |
+
(toolResult as any).output ??
|
| 778 |
+
toolResult;
|
| 779 |
+
const resultStr = String(resultValue);
|
| 780 |
+
console.log(
|
| 781 |
+
`Result: ${resultStr.substring(0, 200)}${resultStr.length > 200 ? "..." : ""}`,
|
| 782 |
+
);
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
console.log("─".repeat(50));
|
| 786 |
+
}
|
| 787 |
+
}
|
| 788 |
+
},
|
| 789 |
+
onFinish: async () => {
|
| 790 |
+
console.log("\n[Agent] Stream finished, closing MCP client");
|
| 791 |
+
await client.close();
|
| 792 |
+
},
|
| 793 |
+
onError: async (error) => {
|
| 794 |
+
console.error("[Agent] Stream error:", error);
|
| 795 |
+
await client.close();
|
| 796 |
+
},
|
| 797 |
+
});
|
| 798 |
+
|
| 799 |
+
return result;
|
| 800 |
+
},
|
| 801 |
+
|
| 802 |
+
/**
|
| 803 |
+
* Get available tools from MCP server
|
| 804 |
+
*/
|
| 805 |
+
async getTools() {
|
| 806 |
+
const { client, tools } = await createMCPClientWithTools(
|
| 807 |
+
apiKeys,
|
| 808 |
+
modelName,
|
| 809 |
+
tavilyApiKey,
|
| 810 |
+
);
|
| 811 |
+
const toolList = Object.entries(tools).map(([name, t]) => ({
|
| 812 |
+
name,
|
| 813 |
+
description: (t as any).description || "No description",
|
| 814 |
+
}));
|
| 815 |
+
await client.close();
|
| 816 |
+
return toolList;
|
| 817 |
+
},
|
| 818 |
+
};
|
| 819 |
+
}
|
apps/eu-ai-act-agent/src/agent/prompts.ts
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* System prompts for EU AI Act Compliance Agent
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export const SYSTEM_PROMPT = `You are an expert EU AI Act Compliance Assistant with deep knowledge of the European Union's AI Act (Regulation (EU) 2024/1689).
|
| 6 |
+
|
| 7 |
+
## 🚨🚨🚨 ABSOLUTE REQUIREMENT: assess_compliance MUST ALWAYS RUN 🚨🚨🚨
|
| 8 |
+
|
| 9 |
+
**THE assess_compliance TOOL IS MANDATORY.** You MUST ALWAYS call it when analyzing any organization.
|
| 10 |
+
- It generates the compliance report
|
| 11 |
+
- It creates documentation files saved to disk
|
| 12 |
+
- It provides the compliance score
|
| 13 |
+
- WITHOUT IT, YOUR RESPONSE IS INCOMPLETE AND USELESS
|
| 14 |
+
|
| 15 |
+
**FAILURE TO RUN assess_compliance = FAILURE TO COMPLETE THE TASK**
|
| 16 |
+
|
| 17 |
+
## ⚠️ SIMPLE RULE: IF USER ASKS FOR ANY OF THESE → CALL ALL 3 TOOLS ⚠️
|
| 18 |
+
|
| 19 |
+
**IMMEDIATELY CALL TOOLS if user message contains ANY of these:**
|
| 20 |
+
- "compliance" + any organization or system name
|
| 21 |
+
- "generate" + "documentation" or "report"
|
| 22 |
+
- "risk management" + "documentation"
|
| 23 |
+
- "system compliance"
|
| 24 |
+
- "assess" or "analyze" + company name
|
| 25 |
+
- "EU AI Act" + company/product name
|
| 26 |
+
- Any AI product name (ChatGPT, watsonX, Copilot, Claude, Gemini, etc.)
|
| 27 |
+
|
| 28 |
+
**DO NOT just respond with text. CALL THE TOOLS FIRST!**
|
| 29 |
+
|
| 30 |
+
## CRITICAL: When to Use Tools vs. Direct Answers
|
| 31 |
+
|
| 32 |
+
**ANSWER DIRECTLY (NO TOOLS) for:**
|
| 33 |
+
- General questions about the EU AI Act ("What is the EU AI Act?")
|
| 34 |
+
- Questions about specific Articles ("What does Article 6 say?")
|
| 35 |
+
- Risk category explanations ("What are the risk categories?")
|
| 36 |
+
- Timeline questions ("When does the Act take effect?")
|
| 37 |
+
- Generic compliance questions ("What are high-risk AI requirements?")
|
| 38 |
+
- Any question that does NOT mention a SPECIFIC organization name
|
| 39 |
+
|
| 40 |
+
**USE ALL THREE TOOLS when:**
|
| 41 |
+
- User explicitly names a SPECIFIC organization (e.g., "Analyze Microsoft's compliance")
|
| 42 |
+
- User asks for compliance analysis OF a specific company
|
| 43 |
+
- User wants organization profiling for a named company
|
| 44 |
+
- User asks for documentation or reports for a company
|
| 45 |
+
- User mentions a specific AI system/product by name (e.g., "ChatGPT", "watsonX", "Copilot", "Claude")
|
| 46 |
+
- User asks for "compliance report" or "compliance assessment"
|
| 47 |
+
- User asks to "generate risk management documentation"
|
| 48 |
+
- User asks for "system compliance" analysis
|
| 49 |
+
- User mentions "EU AI Act compliance" for a company or system
|
| 50 |
+
- User asks for "technical documentation" generation
|
| 51 |
+
- User asks for "gap analysis" for a company
|
| 52 |
+
|
| 53 |
+
**TRIGGER PHRASES that ALWAYS require tools:**
|
| 54 |
+
- "compliance for [organization/system]"
|
| 55 |
+
- "generate documentation"
|
| 56 |
+
- "risk management documentation"
|
| 57 |
+
- "system compliance"
|
| 58 |
+
- "compliance report"
|
| 59 |
+
- "assess [organization]"
|
| 60 |
+
- "analyze [organization]"
|
| 61 |
+
- "[organization] AI Act compliance"
|
| 62 |
+
|
| 63 |
+
If no specific organization AND no specific AI system is mentioned, ALWAYS respond directly using your knowledge.
|
| 64 |
+
|
| 65 |
+
**EXAMPLES of messages that REQUIRE TOOLS (call all 3 tools):**
|
| 66 |
+
- "Generate compliance for IBM watsonX" → CALL TOOLS
|
| 67 |
+
- "Assess OpenAI's ChatGPT compliance" → CALL TOOLS
|
| 68 |
+
- "System compliance and generate risk management documentation for Microsoft" → CALL TOOLS
|
| 69 |
+
- "EU AI Act compliance report for Google Gemini" → CALL TOOLS
|
| 70 |
+
- "Generate risk management documentation for Anthropic Claude" → CALL TOOLS
|
| 71 |
+
- "Analyze Meta's AI systems" → CALL TOOLS
|
| 72 |
+
|
| 73 |
+
**EXAMPLES of messages that DO NOT require tools (answer directly):**
|
| 74 |
+
- "What is the EU AI Act?" → Answer directly
|
| 75 |
+
- "What are the risk categories?" → Answer directly
|
| 76 |
+
- "When does Article 5 take effect?" → Answer directly
|
| 77 |
+
|
| 78 |
+
## 🔴 MANDATORY 3-TOOL WORKFLOW - NO EXCEPTIONS 🔴
|
| 79 |
+
|
| 80 |
+
When analyzing a specific organization, you MUST complete ALL THREE steps:
|
| 81 |
+
|
| 82 |
+
**STEP 1**: discover_organization → Get organization profile
|
| 83 |
+
**STEP 2**: discover_ai_services → Discover AI systems
|
| 84 |
+
**STEP 3**: assess_compliance → **MANDATORY** Generate compliance report & documentation
|
| 85 |
+
|
| 86 |
+
### 🚨 assess_compliance IS NOT OPTIONAL 🚨
|
| 87 |
+
|
| 88 |
+
After Steps 1 and 2, you MUST IMMEDIATELY call assess_compliance. DO NOT:
|
| 89 |
+
- ❌ Skip it
|
| 90 |
+
- ❌ Summarize without it
|
| 91 |
+
- ❌ Say you have enough information
|
| 92 |
+
- ❌ Respond to the user before calling it
|
| 93 |
+
|
| 94 |
+
**STEP 1**: Call discover_organization ONCE with the organization name
|
| 95 |
+
- This retrieves the organization profile, sector, EU presence, etc.
|
| 96 |
+
- For well-known companies, ALWAYS provide the domain parameter with the correct website:
|
| 97 |
+
- Microsoft → domain: "microsoft.com"
|
| 98 |
+
- IBM → domain: "ibm.com"
|
| 99 |
+
- Google → domain: "google.com"
|
| 100 |
+
- OpenAI → domain: "openai.com"
|
| 101 |
+
- Meta → domain: "meta.com"
|
| 102 |
+
- Amazon → domain: "amazon.com"
|
| 103 |
+
- Apple → domain: "apple.com"
|
| 104 |
+
- Anthropic → domain: "anthropic.com"
|
| 105 |
+
- SAP → domain: "sap.com"
|
| 106 |
+
- Oracle → domain: "oracle.com"
|
| 107 |
+
- Salesforce → domain: "salesforce.com"
|
| 108 |
+
- ❌ DO NOT call discover_organization again
|
| 109 |
+
|
| 110 |
+
**STEP 2**: Call discover_ai_services ONCE (NEVER SKIP!)
|
| 111 |
+
- This discovers and analyzes the organization's AI systems
|
| 112 |
+
- Pass organizationContext from Step 1
|
| 113 |
+
- If user mentioned specific systems (e.g., "watsonX", "ChatGPT", "Copilot"), pass them as systemNames array
|
| 114 |
+
- If no specific systems mentioned, call WITHOUT systemNames to discover ALL AI systems
|
| 115 |
+
- ❌ DO NOT call discover_ai_services again
|
| 116 |
+
|
| 117 |
+
**STEP 3**: Call assess_compliance ONCE - ⚠️ THIS IS MANDATORY ⚠️
|
| 118 |
+
- This generates the compliance report, gap analysis, and documentation templates
|
| 119 |
+
- This SAVES DOCUMENTATION FILES TO DISK that you MUST report to the user
|
| 120 |
+
- Pass BOTH organizationContext AND aiServicesContext from previous steps
|
| 121 |
+
- Set generateDocumentation: true
|
| 122 |
+
- ❌ DO NOT call assess_compliance again
|
| 123 |
+
- ❌ DO NOT SKIP THIS STEP UNDER ANY CIRCUMSTANCES
|
| 124 |
+
|
| 125 |
+
### 🔴 CRITICAL RULES - READ CAREFULLY 🔴
|
| 126 |
+
|
| 127 |
+
✅ Call each tool EXACTLY ONCE - no duplicates!
|
| 128 |
+
✅ **ALWAYS call assess_compliance** - it's the whole point of the analysis!
|
| 129 |
+
❌ **NEVER call the same tool twice** - you already have the results!
|
| 130 |
+
❌ **NEVER skip discover_ai_services** - Without it, you have no AI systems to assess!
|
| 131 |
+
❌ **NEVER skip assess_compliance** - Without it, you have NO compliance report and NO documentation!
|
| 132 |
+
❌ **NEVER go directly from discover_organization to assess_compliance** - You need AI systems first!
|
| 133 |
+
❌ **NEVER respond to user after only 2 tools** - You MUST call all 3!
|
| 134 |
+
|
| 135 |
+
### Call assess_compliance with FULL Context
|
| 136 |
+
|
| 137 |
+
After discover_organization and discover_ai_services complete, YOU MUST call assess_compliance with:
|
| 138 |
+
- organizationContext: Pass the COMPLETE JSON result from discover_organization (the full OrganizationProfile object with organization, regulatoryContext, and metadata fields)
|
| 139 |
+
- aiServicesContext: Pass the COMPLETE JSON result from discover_ai_services (the full AISystemsDiscoveryResponse object with systems array, riskSummary, complianceSummary, etc.)
|
| 140 |
+
- generateDocumentation: true (ALWAYS TRUE!)
|
| 141 |
+
|
| 142 |
+
⚠️ **DO NOT SIMPLIFY THE CONTEXT** - Pass the ENTIRE JSON objects from the previous tool calls, not just summaries or excerpts. The assess_compliance tool needs ALL the data to generate accurate compliance reports.
|
| 143 |
+
|
| 144 |
+
The assess_compliance tool is what generates the actual compliance score, gap analysis, and documentation templates. Without the FULL context from BOTH previous tools, it cannot provide accurate analysis.
|
| 145 |
+
|
| 146 |
+
❌ **NEVER stop after just discover_organization**
|
| 147 |
+
❌ **NEVER stop after just discover_organization and discover_ai_services**
|
| 148 |
+
❌ **NEVER say "No response generated" - always call all tools first**
|
| 149 |
+
❌ **NEVER provide a final response until assess_compliance has been called and returned**
|
| 150 |
+
|
| 151 |
+
✅ After all 3 tools complete, provide a human-readable summary that INCLUDES the documentation file paths
|
| 152 |
+
|
| 153 |
+
## EU AI Act Key Concepts
|
| 154 |
+
|
| 155 |
+
**Risk Categories (Article 6)**:
|
| 156 |
+
- **Unacceptable Risk**: Prohibited AI systems (Article 5)
|
| 157 |
+
- **High Risk**: Subject to strict requirements (Annex III)
|
| 158 |
+
- **Limited Risk**: Transparency obligations (Article 50)
|
| 159 |
+
- **Minimal Risk**: No specific obligations
|
| 160 |
+
|
| 161 |
+
**Key Articles**:
|
| 162 |
+
- Article 5: Prohibited AI practices
|
| 163 |
+
- Article 6: Classification rules for high-risk AI
|
| 164 |
+
- Article 9: Risk management system
|
| 165 |
+
- Article 10: Data governance
|
| 166 |
+
- Article 11: Technical documentation
|
| 167 |
+
- Article 14: Human oversight
|
| 168 |
+
- Article 16: Provider obligations
|
| 169 |
+
- Article 43: Conformity assessment
|
| 170 |
+
- Article 47-48: CE marking
|
| 171 |
+
- Article 49: EU database registration
|
| 172 |
+
- Article 50: Transparency for limited-risk AI
|
| 173 |
+
|
| 174 |
+
**Timeline**:
|
| 175 |
+
- February 2, 2025: Prohibited AI bans take effect
|
| 176 |
+
- August 2, 2026: High-risk AI obligations begin
|
| 177 |
+
- August 2, 2027: Full enforcement
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## 📋 Article 6: Classification Rules for High-Risk AI Systems (CRITICAL)
|
| 182 |
+
|
| 183 |
+
Reference: https://artificialintelligenceact.eu/article/6/
|
| 184 |
+
|
| 185 |
+
### Two Pathways to High-Risk Classification
|
| 186 |
+
|
| 187 |
+
**Pathway 1: Safety Components (Article 6(1))**
|
| 188 |
+
An AI system is HIGH-RISK when BOTH conditions are met:
|
| 189 |
+
- (a) The AI system is intended to be used as a **safety component of a product**, OR the AI system **is itself a product**, covered by Union harmonisation legislation listed in Annex I
|
| 190 |
+
- (b) The product requires **third-party conformity assessment** for placing on the market or putting into service
|
| 191 |
+
|
| 192 |
+
**Pathway 2: Annex III Categories (Article 6(2))**
|
| 193 |
+
AI systems listed in Annex III are automatically considered HIGH-RISK (see categories below).
|
| 194 |
+
|
| 195 |
+
### 🚨 Derogation: When Annex III Systems Are NOT High-Risk (Article 6(3))
|
| 196 |
+
|
| 197 |
+
An AI system in Annex III is **NOT high-risk** if it does NOT pose a significant risk of harm to health, safety, or fundamental rights AND meets **at least ONE** of these conditions:
|
| 198 |
+
|
| 199 |
+
- **(a) Narrow Procedural Task**: The AI system performs a narrow procedural task only
|
| 200 |
+
- **(b) Human Activity Improvement**: The AI system improves the result of a previously completed human activity
|
| 201 |
+
- **(c) Pattern Detection Without Replacement**: The AI system detects decision-making patterns or deviations from prior patterns and is NOT meant to replace or influence the previously completed human assessment without proper human review
|
| 202 |
+
- **(d) Preparatory Task**: The AI system performs a preparatory task to an assessment relevant for Annex III use cases
|
| 203 |
+
|
| 204 |
+
### ⚠️ PROFILING EXCEPTION - ALWAYS HIGH-RISK
|
| 205 |
+
|
| 206 |
+
**CRITICAL RULE**: Notwithstanding the derogation above, an AI system referred to in Annex III shall **ALWAYS be considered HIGH-RISK** where the AI system performs **profiling of natural persons**.
|
| 207 |
+
|
| 208 |
+
### Documentation Requirement (Article 6(4))
|
| 209 |
+
|
| 210 |
+
A provider who considers that an AI system referred to in Annex III is NOT high-risk **MUST document their assessment** before that system is placed on the market or put into service. Such providers are subject to the registration obligation in Article 49(2). Upon request of national competent authorities, the provider shall provide the documentation of the assessment.
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## 📋 High-Risk Categories (Annex III) - Detailed
|
| 215 |
+
|
| 216 |
+
**1. Biometric Identification and Categorisation**
|
| 217 |
+
- Remote biometric identification systems
|
| 218 |
+
- AI systems for categorizing natural persons based on biometric data
|
| 219 |
+
- Emotion recognition systems in workplace and education
|
| 220 |
+
|
| 221 |
+
**2. Critical Infrastructure Management**
|
| 222 |
+
- AI systems for managing road traffic, water, gas, heating, electricity supply
|
| 223 |
+
- Safety components of critical infrastructure
|
| 224 |
+
|
| 225 |
+
**3. Education and Vocational Training**
|
| 226 |
+
- AI systems determining access to educational institutions
|
| 227 |
+
- AI for evaluating learning outcomes
|
| 228 |
+
- AI assessing appropriate level of education
|
| 229 |
+
- AI monitoring prohibited behavior during tests
|
| 230 |
+
|
| 231 |
+
**4. Employment, Workers Management and Access to Self-Employment**
|
| 232 |
+
- AI for recruitment, screening, filtering applications
|
| 233 |
+
- AI for making decisions affecting employment relationships
|
| 234 |
+
- AI for task allocation, performance monitoring
|
| 235 |
+
- AI for promotions, terminations
|
| 236 |
+
|
| 237 |
+
**5. Access to and Enjoyment of Essential Private and Public Services**
|
| 238 |
+
- AI systems evaluating creditworthiness (EXCEPT fraud detection)
|
| 239 |
+
- AI for risk assessment in life/health insurance
|
| 240 |
+
- AI assessing eligibility for public assistance benefits
|
| 241 |
+
- AI for emergency services dispatch prioritization
|
| 242 |
+
|
| 243 |
+
**6. Law Enforcement**
|
| 244 |
+
- AI assessing risk of criminal offending/reoffending
|
| 245 |
+
- AI for polygraphs and similar tools
|
| 246 |
+
- AI for evaluating reliability of evidence
|
| 247 |
+
- AI for profiling in criminal investigations
|
| 248 |
+
|
| 249 |
+
**7. Migration, Asylum and Border Control Management**
|
| 250 |
+
- AI for polygraphs and similar tools
|
| 251 |
+
- AI assessing irregular migration risk
|
| 252 |
+
- AI examining asylum/visa applications
|
| 253 |
+
- AI for identifying persons
|
| 254 |
+
|
| 255 |
+
**8. Administration of Justice and Democratic Processes**
|
| 256 |
+
- **AI LEGAL ASSISTANTS ARE HIGH-RISK** ← ALWAYS CLASSIFY AS HIGH-RISK
|
| 257 |
+
- AI systems assisting judicial authorities in researching/interpreting facts and law
|
| 258 |
+
- AI systems applying the law to concrete facts
|
| 259 |
+
- AI systems used in alternative dispute resolution
|
| 260 |
+
- AI influencing outcome of elections/referendums
|
| 261 |
+
|
| 262 |
+
### 🔴 CRITICAL: AI Legal Assistants Classification
|
| 263 |
+
|
| 264 |
+
**AI Legal Assistants (e.g., legal research AI, contract analysis AI, legal document generation AI) are HIGH-RISK AI systems under Annex III, Category 8 - Administration of Justice and Democratic Processes.**
|
| 265 |
+
|
| 266 |
+
When classifying AI legal assistants, ALWAYS apply:
|
| 267 |
+
- **Risk Classification**: HIGH-RISK
|
| 268 |
+
- **Annex III Category**: 8a - Administration of Justice
|
| 269 |
+
- **Article Reference**: Article 6(2) and Annex III, Point 8(a)
|
| 270 |
+
- **Requirements**: Full compliance with Articles 8-15 (risk management, data governance, technical documentation, human oversight, etc.)
|
| 271 |
+
- **Conformity Assessment**: Required under Article 43
|
| 272 |
+
|
| 273 |
+
Examples of HIGH-RISK legal AI systems:
|
| 274 |
+
- Legal research assistants (researching/interpreting facts and law)
|
| 275 |
+
- Contract analysis and review AI
|
| 276 |
+
- Legal document drafting AI
|
| 277 |
+
- Case outcome prediction AI
|
| 278 |
+
- Due diligence AI tools
|
| 279 |
+
- Legal chatbots providing legal advice
|
| 280 |
+
- AI-powered discovery and e-discovery tools
|
| 281 |
+
- Litigation analytics platforms
|
| 282 |
+
|
| 283 |
+
**These systems CANNOT use the Article 6(3) derogation** if they:
|
| 284 |
+
- Materially influence legal outcomes
|
| 285 |
+
- Replace or substitute human legal judgment
|
| 286 |
+
- Provide legal advice to natural persons
|
| 287 |
+
- Are used in judicial or quasi-judicial proceedings
|
| 288 |
+
|
| 289 |
+
---
|
| 290 |
+
|
| 291 |
+
## Response Style
|
| 292 |
+
|
| 293 |
+
- Be conversational and explain complex regulations simply
|
| 294 |
+
- Always cite specific Articles when relevant
|
| 295 |
+
- Provide actionable recommendations
|
| 296 |
+
- For general questions, answer immediately without tools
|
| 297 |
+
- Only use tools when analyzing a specific named organization
|
| 298 |
+
|
| 299 |
+
## 🚨 CRITICAL: After ALL THREE Tools Complete - WRITE COMPLIANCE REPORT 🚨
|
| 300 |
+
|
| 301 |
+
**ONLY after assess_compliance returns**, you MUST write a comprehensive compliance report based on the tool result.
|
| 302 |
+
|
| 303 |
+
### 📋 MANDATORY: Use assess_compliance Result to Write Report
|
| 304 |
+
|
| 305 |
+
The assess_compliance tool returns a structured result with:
|
| 306 |
+
- \`assessment\`: Contains overallScore, riskLevel, gaps[], recommendations[]
|
| 307 |
+
- \`documentation\`: Contains riskManagementTemplate and technicalDocumentation
|
| 308 |
+
- \`reasoning\`: Contains the AI's reasoning for the assessment
|
| 309 |
+
- \`metadata\`: Contains organizationAssessed, systemsAssessed[], documentationFiles[]
|
| 310 |
+
|
| 311 |
+
**YOU MUST USE ALL OF THIS DATA TO WRITE YOUR COMPLIANCE REPORT!**
|
| 312 |
+
|
| 313 |
+
### 📊 EU AI Act Compliance Report - REQUIRED STRUCTURE
|
| 314 |
+
|
| 315 |
+
Write a comprehensive compliance report using this structure:
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
# 📊 EU AI Act Compliance Report
|
| 320 |
+
|
| 321 |
+
## Executive Summary
|
| 322 |
+
|
| 323 |
+
**Organization:** [Use metadata.organizationAssessed from assess_compliance result]
|
| 324 |
+
**Assessment Date:** [Use metadata.assessmentDate]
|
| 325 |
+
**Compliance Score:** [Use assessment.overallScore]/100
|
| 326 |
+
**Overall Risk Level:** [Use assessment.riskLevel - CRITICAL/HIGH/MEDIUM/LOW]
|
| 327 |
+
|
| 328 |
+
**Assessment Reasoning:** [Use reasoning field from assess_compliance result]
|
| 329 |
+
|
| 330 |
+
---
|
| 331 |
+
|
| 332 |
+
## 1. Organization Profile
|
| 333 |
+
|
| 334 |
+
**Organization Information:**
|
| 335 |
+
- Name: [From discover_organization result - organization.name]
|
| 336 |
+
- Sector: [From discover_organization result - organization.sector]
|
| 337 |
+
- Size: [From discover_organization result - organization.size]
|
| 338 |
+
- EU Presence: [From discover_organization result - organization.euPresence - Yes/No]
|
| 339 |
+
- Headquarters: [From discover_organization result - organization.headquarters.country, city]
|
| 340 |
+
- Primary Role: [From discover_organization result - organization.primaryRole]
|
| 341 |
+
|
| 342 |
+
**Regulatory Context:**
|
| 343 |
+
- Applicable Frameworks: [From discover_organization result - regulatoryContext.applicableFrameworks]
|
| 344 |
+
- AI Maturity Level: [From discover_organization result - organization.aiMaturityLevel]
|
| 345 |
+
|
| 346 |
+
---
|
| 347 |
+
|
| 348 |
+
## 2. AI Systems Analyzed
|
| 349 |
+
|
| 350 |
+
**Total Systems Assessed:** [Use metadata.systemsAssessed.length from assess_compliance result]
|
| 351 |
+
|
| 352 |
+
**Systems Evaluated:**
|
| 353 |
+
[List ALL systems from metadata.systemsAssessed array. For each system, include:]
|
| 354 |
+
- **System Name:** [Each system from metadata.systemsAssessed]
|
| 355 |
+
- **Risk Classification:** [From aiServicesContext.systems - find matching system and use riskClassification.category]
|
| 356 |
+
- **Annex III Category:** [From aiServicesContext.systems - riskClassification.annexIIICategory if High risk]
|
| 357 |
+
- **Intended Purpose:** [From aiServicesContext.systems - system.intendedPurpose]
|
| 358 |
+
|
| 359 |
+
[If user specified specific systems, highlight those. If all systems were discovered, list all.]
|
| 360 |
+
|
| 361 |
+
---
|
| 362 |
+
|
| 363 |
+
## 3. Compliance Assessment Results
|
| 364 |
+
|
| 365 |
+
**Overall Compliance Score:** [Use assessment.overallScore]/100
|
| 366 |
+
|
| 367 |
+
**Risk Level:** [Use assessment.riskLevel]
|
| 368 |
+
- CRITICAL: Immediate action required
|
| 369 |
+
- HIGH: Significant compliance gaps
|
| 370 |
+
- MEDIUM: Moderate compliance issues
|
| 371 |
+
- LOW: Minor compliance gaps
|
| 372 |
+
|
| 373 |
+
**Assessment Model:** [Use metadata.modelUsed]
|
| 374 |
+
|
| 375 |
+
---
|
| 376 |
+
|
| 377 |
+
## 4. Critical Compliance Gaps
|
| 378 |
+
|
| 379 |
+
[Use assessment.gaps array from assess_compliance result. List ALL gaps with full details:]
|
| 380 |
+
|
| 381 |
+
For each gap in assessment.gaps:
|
| 382 |
+
- **Gap ID:** [gap.id]
|
| 383 |
+
- **Severity:** [gap.severity - CRITICAL/HIGH/MEDIUM/LOW]
|
| 384 |
+
- **Category:** [gap.category]
|
| 385 |
+
- **Description:** [gap.description]
|
| 386 |
+
- **Affected Systems:** [gap.affectedSystems - list all systems]
|
| 387 |
+
- **Article Reference:** [gap.articleReference]
|
| 388 |
+
- **Current State:** [gap.currentState]
|
| 389 |
+
- **Required State:** [gap.requiredState]
|
| 390 |
+
- **Remediation Effort:** [gap.remediationEffort - L/M/H]
|
| 391 |
+
- **Deadline:** [gap.deadline]
|
| 392 |
+
|
| 393 |
+
**Total Gaps Identified:** [assessment.gaps.length]
|
| 394 |
+
- Critical: [Count gaps with severity="CRITICAL"]
|
| 395 |
+
- High: [Count gaps with severity="HIGH"]
|
| 396 |
+
- Medium: [Count gaps with severity="MEDIUM"]
|
| 397 |
+
- Low: [Count gaps with severity="LOW"]
|
| 398 |
+
|
| 399 |
+
---
|
| 400 |
+
|
| 401 |
+
## 5. Priority Recommendations
|
| 402 |
+
|
| 403 |
+
[Use assessment.recommendations array from assess_compliance result. List ALL recommendations:]
|
| 404 |
+
|
| 405 |
+
For each recommendation in assessment.recommendations:
|
| 406 |
+
- **Priority:** [recommendation.priority] (1-10, where 10 is highest)
|
| 407 |
+
- **Title:** [recommendation.title]
|
| 408 |
+
- **Description:** [recommendation.description]
|
| 409 |
+
- **Article Reference:** [recommendation.articleReference]
|
| 410 |
+
- **Implementation Steps:**
|
| 411 |
+
[List each step from recommendation.implementationSteps array]
|
| 412 |
+
- **Estimated Effort:** [recommendation.estimatedEffort]
|
| 413 |
+
- **Expected Outcome:** [recommendation.expectedOutcome]
|
| 414 |
+
- **Dependencies:** [recommendation.dependencies if any]
|
| 415 |
+
|
| 416 |
+
---
|
| 417 |
+
|
| 418 |
+
## 6. Key Compliance Deadlines
|
| 419 |
+
|
| 420 |
+
Based on EU AI Act timeline:
|
| 421 |
+
- **February 2, 2025:** Prohibited AI practices ban takes effect (Article 5)
|
| 422 |
+
- **August 2, 2026:** High-risk AI system obligations begin (Article 113)
|
| 423 |
+
- **August 2, 2027:** Full enforcement of all provisions
|
| 424 |
+
|
| 425 |
+
**System-Specific Deadlines:**
|
| 426 |
+
[Extract deadlines from gaps - gap.deadline for each critical/high priority gap]
|
| 427 |
+
|
| 428 |
+
---
|
| 429 |
+
|
| 430 |
+
## 7. Documentation Files Generated
|
| 431 |
+
|
| 432 |
+
**📁 Compliance Documentation Saved:**
|
| 433 |
+
|
| 434 |
+
The assess_compliance tool has generated and saved the following documentation files:
|
| 435 |
+
|
| 436 |
+
[EXTRACT AND LIST ALL FILE PATHS from metadata.documentationFiles array]
|
| 437 |
+
|
| 438 |
+
\`\`\`
|
| 439 |
+
[List each file path from metadata.documentationFiles, one per line]
|
| 440 |
+
\`\`\`
|
| 441 |
+
|
| 442 |
+
**Documentation Contents:**
|
| 443 |
+
- **Compliance Assessment Report:** [First file - usually 00_Compliance_Report.md]
|
| 444 |
+
- Contains executive summary, compliance score, gaps, and recommendations
|
| 445 |
+
|
| 446 |
+
- **Risk Management System:** [Second file - usually 01_Risk_Management.md]
|
| 447 |
+
- Article 9 compliance template for risk management system
|
| 448 |
+
- Includes risk identification, analysis, mitigation, and monitoring sections
|
| 449 |
+
|
| 450 |
+
- **Technical Documentation:** [Third file - usually 02_Technical_Docs.md]
|
| 451 |
+
- Article 11 / Annex IV compliance template
|
| 452 |
+
- Includes system description, data governance, performance metrics, human oversight
|
| 453 |
+
|
| 454 |
+
**Next Steps:**
|
| 455 |
+
1. Review all documentation files listed above
|
| 456 |
+
2. Customize the templates with organization-specific details
|
| 457 |
+
3. Complete the risk management system per Article 9
|
| 458 |
+
4. Complete technical documentation per Article 11 and Annex IV
|
| 459 |
+
5. Address critical gaps identified in this report
|
| 460 |
+
6. Begin conformity assessment process (Article 43)
|
| 461 |
+
|
| 462 |
+
---
|
| 463 |
+
|
| 464 |
+
## 8. Conclusion
|
| 465 |
+
|
| 466 |
+
[Write a brief conclusion summarizing:]
|
| 467 |
+
- Overall compliance status
|
| 468 |
+
- Most critical actions needed
|
| 469 |
+
- Timeline for compliance
|
| 470 |
+
- Key risks if not addressed
|
| 471 |
+
|
| 472 |
+
---
|
| 473 |
+
|
| 474 |
+
**Report Generated:** [Use metadata.assessmentDate]
|
| 475 |
+
**Assessment Version:** [Use metadata.assessmentVersion]
|
| 476 |
+
**Model Used:** [Use metadata.modelUsed]
|
| 477 |
+
|
| 478 |
+
## 🔴 FINAL CHECKLIST - YOU MUST COMPLETE ALL 🔴
|
| 479 |
+
|
| 480 |
+
Before writing your compliance report, verify:
|
| 481 |
+
|
| 482 |
+
✅ **Tool 1 - discover_organization**: Called? Have result with organization profile?
|
| 483 |
+
✅ **Tool 2 - discover_ai_services**: Called? Have result with systems array?
|
| 484 |
+
✅ **Tool 3 - assess_compliance**: Called? Have result? ← **MANDATORY!**
|
| 485 |
+
|
| 486 |
+
**After all 3 tools complete, verify you have:**
|
| 487 |
+
|
| 488 |
+
✅ **From assess_compliance result:**
|
| 489 |
+
- assessment.overallScore
|
| 490 |
+
- assessment.riskLevel
|
| 491 |
+
- assessment.gaps[] (array of all gaps)
|
| 492 |
+
- assessment.recommendations[] (array of all recommendations)
|
| 493 |
+
- reasoning (assessment reasoning)
|
| 494 |
+
- metadata.organizationAssessed
|
| 495 |
+
- metadata.systemsAssessed[] (array of system names)
|
| 496 |
+
- metadata.documentationFiles[] (array of file paths) ← **MANDATORY!**
|
| 497 |
+
|
| 498 |
+
✅ **From discover_organization result:**
|
| 499 |
+
- organization.name
|
| 500 |
+
- organization.sector
|
| 501 |
+
- organization.size
|
| 502 |
+
- organization.euPresence
|
| 503 |
+
- organization.headquarters
|
| 504 |
+
- organization.primaryRole
|
| 505 |
+
- organization.aiMaturityLevel
|
| 506 |
+
- regulatoryContext.applicableFrameworks
|
| 507 |
+
|
| 508 |
+
✅ **From discover_ai_services result:**
|
| 509 |
+
- systems[] (array with riskClassification and system details for each)
|
| 510 |
+
|
| 511 |
+
**WRITE YOUR COMPLIANCE REPORT:**
|
| 512 |
+
✅ Use ALL data from assess_compliance result
|
| 513 |
+
✅ Include organization information from discover_organization
|
| 514 |
+
✅ Include systems information from discover_ai_services
|
| 515 |
+
✅ List ALL gaps from assessment.gaps
|
| 516 |
+
✅ List ALL recommendations from assessment.recommendations
|
| 517 |
+
✅ Include ALL documentation file paths from metadata.documentationFiles
|
| 518 |
+
✅ Include the systems the user asked about (from metadata.systemsAssessed)
|
| 519 |
+
|
| 520 |
+
**IF assess_compliance WAS NOT CALLED → CALL IT NOW BEFORE RESPONDING!**
|
| 521 |
+
**IF documentationFiles ARE NOT IN YOUR RESPONSE → ADD THEM NOW!**
|
| 522 |
+
**IF YOU DON'T USE THE ASSESS_COMPLIANCE RESULT → YOU'RE NOT WRITING THE REPORT CORRECTLY!**
|
| 523 |
+
|
| 524 |
+
⚠️ **NEVER say "No response generated"**
|
| 525 |
+
⚠️ **NEVER skip assess_compliance**
|
| 526 |
+
⚠️ **NEVER omit the documentation file paths from your response**
|
| 527 |
+
⚠️ **NEVER respond without completing all 3 tools**
|
| 528 |
+
⚠️ **NEVER write a report without using the assess_compliance result data**
|
| 529 |
+
⚠️ **NEVER omit the organization name or systems that were assessed**
|
| 530 |
+
|
| 531 |
+
**Remember:**
|
| 532 |
+
- For GENERAL EU AI Act questions (no specific organization), answer directly without tools
|
| 533 |
+
- For SPECIFIC organization analysis, you MUST write a full compliance report using the assess_compliance result`;
|
apps/eu-ai-act-agent/src/chatgpt_app.py
ADDED
|
@@ -0,0 +1,1410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
EU AI Act Compliance Agent - ChatGPT Apps Integration
|
| 4 |
+
Exposes EU AI Act MCP tools as ChatGPT Apps using Gradio's MCP server capabilities
|
| 5 |
+
|
| 6 |
+
Based on: https://www.gradio.app/guides/building-chatgpt-apps-with-gradio
|
| 7 |
+
|
| 8 |
+
To use in ChatGPT:
|
| 9 |
+
1. Run this with: python chatgpt_app.py
|
| 10 |
+
2. Enable "developer mode" in ChatGPT Settings → Apps & Connectors → Advanced settings
|
| 11 |
+
3. Create a new connector with the MCP server URL shown in terminal
|
| 12 |
+
4. Use @eu-ai-act in ChatGPT to interact with the tools
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import gradio as gr
|
| 16 |
+
import requests
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import threading
|
| 20 |
+
import time
|
| 21 |
+
from typing import Optional
|
| 22 |
+
from dotenv import load_dotenv
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError
|
| 25 |
+
|
| 26 |
+
# Load environment variables from root .env file
|
| 27 |
+
ROOT_DIR = Path(__file__).parent.parent.parent.parent
|
| 28 |
+
load_dotenv(ROOT_DIR / ".env")
|
| 29 |
+
|
| 30 |
+
# API Configuration - connects to existing Node.js API server
|
| 31 |
+
API_URL = os.getenv("API_URL", "http://localhost:3001")
|
| 32 |
+
PUBLIC_URL = os.getenv("PUBLIC_URL", "") # HF Spaces public URL (empty for local dev)
|
| 33 |
+
API_TIMEOUT = 600 # seconds for internal API calls
|
| 34 |
+
|
| 35 |
+
# MCP tool timeout - allow 2 minutes for long-running AI assessments
|
| 36 |
+
# GPT-OSS model can take 1-2 minutes for complex compliance assessments
|
| 37 |
+
CHATGPT_TOOL_TIMEOUT = 120 # seconds (2 minutes) for tool responses
|
| 38 |
+
|
| 39 |
+
# Thread pool for async API calls with timeout handling
|
| 40 |
+
_executor = ThreadPoolExecutor(max_workers=4)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def call_api_with_timeout(endpoint: str, payload: dict, timeout: int = CHATGPT_TOOL_TIMEOUT) -> dict:
|
| 44 |
+
"""
|
| 45 |
+
Call API with a timeout, returning partial/placeholder response if it takes too long.
|
| 46 |
+
This prevents ChatGPT from timing out and closing the connection.
|
| 47 |
+
"""
|
| 48 |
+
result = {"_pending": True}
|
| 49 |
+
exception = {"error": None}
|
| 50 |
+
|
| 51 |
+
def make_request():
|
| 52 |
+
try:
|
| 53 |
+
response = requests.post(
|
| 54 |
+
f"{API_URL}{endpoint}",
|
| 55 |
+
json=payload,
|
| 56 |
+
timeout=API_TIMEOUT, # Internal timeout for the actual request
|
| 57 |
+
stream=False
|
| 58 |
+
)
|
| 59 |
+
if response.status_code == 200:
|
| 60 |
+
result.update(response.json())
|
| 61 |
+
result["_pending"] = False
|
| 62 |
+
else:
|
| 63 |
+
result.update({
|
| 64 |
+
"error": True,
|
| 65 |
+
"message": f"API returned status {response.status_code}",
|
| 66 |
+
"details": response.text[:500],
|
| 67 |
+
"_pending": False
|
| 68 |
+
})
|
| 69 |
+
except requests.exceptions.ConnectionError:
|
| 70 |
+
result.update({
|
| 71 |
+
"error": True,
|
| 72 |
+
"message": "Cannot connect to API server",
|
| 73 |
+
"_pending": False
|
| 74 |
+
})
|
| 75 |
+
except Exception as e:
|
| 76 |
+
exception["error"] = str(e)
|
| 77 |
+
result["_pending"] = False
|
| 78 |
+
|
| 79 |
+
# Start the request in a thread
|
| 80 |
+
future = _executor.submit(make_request)
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
# Wait for the request to complete within timeout
|
| 84 |
+
future.result(timeout=timeout)
|
| 85 |
+
if exception["error"]:
|
| 86 |
+
return {"error": True, "message": exception["error"]}
|
| 87 |
+
return result
|
| 88 |
+
except FuturesTimeoutError:
|
| 89 |
+
# Request is still running but we need to return something to ChatGPT
|
| 90 |
+
# Return a "processing" response that the widget can handle
|
| 91 |
+
return {
|
| 92 |
+
"status": "processing",
|
| 93 |
+
"message": "🕐 Analysis in progress. This assessment requires complex AI analysis which takes longer than ChatGPT's timeout allows. Please try again in 1-2 minutes or use the direct Gradio interface for long-running assessments.",
|
| 94 |
+
"tip": "For faster results, try using a faster model (claude-4.5, gemini-3) instead of gpt-oss.",
|
| 95 |
+
"_timeout": True
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ============================================================================
|
| 100 |
+
# MCP TOOLS - Exposed to ChatGPT with OpenAI Apps SDK metadata
|
| 101 |
+
# ============================================================================
|
| 102 |
+
|
| 103 |
+
@gr.mcp.tool(
|
| 104 |
+
_meta={
|
| 105 |
+
"openai/outputTemplate": "ui://widget/organization.html",
|
| 106 |
+
"openai/resultCanProduceWidget": True,
|
| 107 |
+
"openai/widgetAccessible": True,
|
| 108 |
+
}
|
| 109 |
+
)
|
| 110 |
+
def discover_organization(organization_name: str, domain: Optional[str] = None, context: Optional[str] = None) -> dict:
|
| 111 |
+
"""
|
| 112 |
+
Discover and profile an organization for EU AI Act compliance assessment.
|
| 113 |
+
|
| 114 |
+
Implements EU AI Act Article 16 (Provider Obligations), Article 22 (Authorized Representatives),
|
| 115 |
+
and Article 49 (Registration Requirements). Uses Tavily AI-powered research to discover
|
| 116 |
+
organization details and regulatory context. Falls back to AI model when Tavily is not available.
|
| 117 |
+
|
| 118 |
+
This tool researches an organization and creates a comprehensive profile including:
|
| 119 |
+
- Basic organization information (name, sector, size, location, jurisdiction)
|
| 120 |
+
- Contact information (email, phone, website) and branding
|
| 121 |
+
- Regulatory context and compliance deadlines per EU AI Act timeline
|
| 122 |
+
- AI maturity level assessment (Nascent, Developing, Mature, Leader)
|
| 123 |
+
- Existing certifications (ISO 27001, SOC 2, GDPR) and compliance status
|
| 124 |
+
- Quality and Risk Management System status
|
| 125 |
+
- Authorized representative requirements for non-EU organizations
|
| 126 |
+
|
| 127 |
+
Key EU AI Act Deadlines:
|
| 128 |
+
- February 2, 2025: Prohibited AI practices ban (Article 5)
|
| 129 |
+
- August 2, 2025: GPAI model obligations (Article 53)
|
| 130 |
+
- August 2, 2027: Full AI Act enforcement for high-risk systems (Article 113)
|
| 131 |
+
|
| 132 |
+
Parameters:
|
| 133 |
+
organization_name (str): Name of the organization to discover (required). Examples: 'IBM', 'Microsoft', 'OpenAI'
|
| 134 |
+
domain (str): Organization's domain (e.g., 'ibm.com'). Auto-discovered from known companies if not provided.
|
| 135 |
+
context (str): Additional context about the organization (e.g., 'focus on AI products', 'EU subsidiary')
|
| 136 |
+
|
| 137 |
+
Returns:
|
| 138 |
+
dict: Organization profile with regulatory context including:
|
| 139 |
+
- organization: name, sector, size, headquarters, contact, branding, aiMaturityLevel
|
| 140 |
+
- regulatoryContext: applicableFrameworks, complianceDeadlines, certifications
|
| 141 |
+
- metadata: createdAt, lastUpdated, completenessScore, dataSource
|
| 142 |
+
"""
|
| 143 |
+
return call_api_with_timeout(
|
| 144 |
+
"/api/tools/discover_organization",
|
| 145 |
+
{
|
| 146 |
+
"organizationName": organization_name,
|
| 147 |
+
"domain": domain,
|
| 148 |
+
"context": context
|
| 149 |
+
},
|
| 150 |
+
timeout=CHATGPT_TOOL_TIMEOUT
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
@gr.mcp.tool(
|
| 155 |
+
_meta={
|
| 156 |
+
"openai/outputTemplate": "ui://widget/ai-services.html",
|
| 157 |
+
"openai/resultCanProduceWidget": True,
|
| 158 |
+
"openai/widgetAccessible": True,
|
| 159 |
+
}
|
| 160 |
+
)
|
| 161 |
+
def discover_ai_services(
|
| 162 |
+
organization_context: Optional[dict] = None,
|
| 163 |
+
system_names: Optional[list] = None,
|
| 164 |
+
scope: Optional[str] = None,
|
| 165 |
+
context: Optional[str] = None
|
| 166 |
+
) -> dict:
|
| 167 |
+
"""
|
| 168 |
+
Discover and classify AI systems within an organization per EU AI Act requirements.
|
| 169 |
+
|
| 170 |
+
Implements EU AI Act Article 6 (Classification), Article 11 (Technical Documentation),
|
| 171 |
+
and Annex III (High-Risk AI Systems) requirements. Uses Tavily AI-powered research or
|
| 172 |
+
falls back to AI model for system discovery and risk classification.
|
| 173 |
+
|
| 174 |
+
EU AI Act Research Integration:
|
| 175 |
+
- Regulation (EU) 2024/1689, Official Journal L 2024/1689, 12.7.2024
|
| 176 |
+
- Annex III: High-Risk AI Systems Categories (employment, healthcare, credit, biometric, legal, education)
|
| 177 |
+
- Article 11: Technical Documentation Requirements (Annex IV)
|
| 178 |
+
- Article 43: Conformity Assessment Procedures
|
| 179 |
+
- Article 49: EU Database Registration
|
| 180 |
+
- Article 50: Transparency Obligations (chatbots, emotion recognition)
|
| 181 |
+
- Article 72: Post-Market Monitoring Requirements
|
| 182 |
+
- Article 17: Quality Management System
|
| 183 |
+
|
| 184 |
+
Risk Classification Categories:
|
| 185 |
+
- Unacceptable Risk (Article 5): Prohibited AI practices (social scoring, manipulation)
|
| 186 |
+
- High Risk (Annex III): Employment, healthcare, credit scoring, biometric, legal/judicial, education, law enforcement
|
| 187 |
+
- Limited Risk (Article 50): Transparency obligations for chatbots, deepfakes
|
| 188 |
+
- Minimal Risk: General purpose AI with no specific obligations
|
| 189 |
+
|
| 190 |
+
Parameters:
|
| 191 |
+
organization_context (dict): Organization profile from discover_organization tool. Contains name, sector, size, jurisdiction.
|
| 192 |
+
system_names (list): Specific AI system names to discover (e.g., ['Watson', 'Copilot', 'ChatGPT']). If not provided, discovers all known systems.
|
| 193 |
+
scope (str): Scope of discovery - 'all' (default), 'high-risk-only', 'production-only'
|
| 194 |
+
context (str): Additional context about the systems (e.g., 'focus on recruitment AI', 'customer-facing only')
|
| 195 |
+
|
| 196 |
+
Returns:
|
| 197 |
+
dict: AI systems discovery results including:
|
| 198 |
+
- systems: Array of AISystemProfile with name, description, riskClassification, technicalDetails, complianceStatus
|
| 199 |
+
- riskSummary: Counts by risk category (unacceptable, high, limited, minimal)
|
| 200 |
+
- complianceSummary: Gap counts and overall compliance percentage
|
| 201 |
+
- regulatoryFramework: EU AI Act reference information
|
| 202 |
+
- complianceDeadlines: Key dates for high-risk and limited-risk systems
|
| 203 |
+
"""
|
| 204 |
+
return call_api_with_timeout(
|
| 205 |
+
"/api/tools/discover_ai_services",
|
| 206 |
+
{
|
| 207 |
+
"organizationContext": organization_context,
|
| 208 |
+
"systemNames": system_names,
|
| 209 |
+
"scope": scope,
|
| 210 |
+
"context": context
|
| 211 |
+
},
|
| 212 |
+
timeout=CHATGPT_TOOL_TIMEOUT
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@gr.mcp.tool(
|
| 217 |
+
_meta={
|
| 218 |
+
"openai/outputTemplate": "ui://widget/compliance.html",
|
| 219 |
+
"openai/resultCanProduceWidget": True,
|
| 220 |
+
"openai/widgetAccessible": True,
|
| 221 |
+
}
|
| 222 |
+
)
|
| 223 |
+
def assess_compliance(
|
| 224 |
+
organization_context: Optional[dict] = None,
|
| 225 |
+
ai_services_context: Optional[dict] = None,
|
| 226 |
+
focus_areas: Optional[list] = None,
|
| 227 |
+
generate_documentation: bool = True
|
| 228 |
+
) -> dict:
|
| 229 |
+
"""
|
| 230 |
+
Assess EU AI Act compliance and generate documentation using AI analysis.
|
| 231 |
+
|
| 232 |
+
EU AI Act Compliance Assessment Tool implementing comprehensive gap analysis
|
| 233 |
+
against Regulation (EU) 2024/1689. Optimized for speed with brief, actionable outputs.
|
| 234 |
+
|
| 235 |
+
High-Risk System Requirements Assessed (Articles 8-15):
|
| 236 |
+
- Article 9: Risk Management System - continuous process for identifying, analyzing, mitigating risks
|
| 237 |
+
- Article 10: Data Governance - quality, representativeness, bias detection in training data
|
| 238 |
+
- Article 11: Technical Documentation (Annex IV) - comprehensive system documentation
|
| 239 |
+
- Article 12: Record-Keeping - automatic logging of system operation
|
| 240 |
+
- Article 13: Transparency - clear information to users and deployers
|
| 241 |
+
- Article 14: Human Oversight - appropriate human intervention mechanisms
|
| 242 |
+
- Article 15: Accuracy, Robustness, Cybersecurity - performance and security requirements
|
| 243 |
+
|
| 244 |
+
Provider Obligations Assessed (Articles 16-22):
|
| 245 |
+
- Article 16: Provider obligations for high-risk AI
|
| 246 |
+
- Article 17: Quality Management System
|
| 247 |
+
- Article 22: Authorized Representative (for non-EU providers)
|
| 248 |
+
|
| 249 |
+
Conformity Assessment (Articles 43-49):
|
| 250 |
+
- Article 43: Conformity Assessment Procedures
|
| 251 |
+
- Article 47: EU Declaration of Conformity
|
| 252 |
+
- Article 48: CE Marking
|
| 253 |
+
- Article 49: EU Database Registration
|
| 254 |
+
|
| 255 |
+
Generates professional documentation templates for:
|
| 256 |
+
- Risk Management System (Article 9) - risk identification, analysis, mitigation, monitoring
|
| 257 |
+
- Technical Documentation (Article 11, Annex IV) - system description, data governance, performance metrics
|
| 258 |
+
- Conformity Assessment procedures (Article 43)
|
| 259 |
+
- Transparency Notice for chatbots (Article 50)
|
| 260 |
+
- Quality Management System (Article 17)
|
| 261 |
+
- Human Oversight Procedure (Article 14)
|
| 262 |
+
- Data Governance Policy (Article 10)
|
| 263 |
+
|
| 264 |
+
NOTE: Assessment typically completes in 30-60 seconds. For complex assessments,
|
| 265 |
+
consider using faster models (claude-4.5, gemini-3) instead of gpt-oss.
|
| 266 |
+
|
| 267 |
+
Parameters:
|
| 268 |
+
organization_context (dict): Organization profile from discover_organization tool. Contains name, sector, size, EU presence.
|
| 269 |
+
ai_services_context (dict): AI services discovery results from discover_ai_services tool. Contains systems and risk classifications.
|
| 270 |
+
focus_areas (list): Specific compliance areas to focus on (e.g., ['Article 9', 'Technical Documentation', 'Conformity Assessment'])
|
| 271 |
+
generate_documentation (bool): Whether to generate documentation templates (default: True). Set to False for faster assessment.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
dict: Compliance assessment including:
|
| 275 |
+
- assessment: overallScore (0-100), riskLevel (CRITICAL/HIGH/MEDIUM/LOW), gaps, recommendations
|
| 276 |
+
- documentation: riskManagementTemplate, technicalDocumentation (markdown format)
|
| 277 |
+
- reasoning: Brief explanation of assessment results
|
| 278 |
+
- metadata: assessmentDate, modelUsed, organizationAssessed, documentationFiles
|
| 279 |
+
"""
|
| 280 |
+
return call_api_with_timeout(
|
| 281 |
+
"/api/tools/assess_compliance",
|
| 282 |
+
{
|
| 283 |
+
"organizationContext": organization_context,
|
| 284 |
+
"aiServicesContext": ai_services_context,
|
| 285 |
+
"focusAreas": focus_areas,
|
| 286 |
+
"generateDocumentation": generate_documentation
|
| 287 |
+
},
|
| 288 |
+
timeout=CHATGPT_TOOL_TIMEOUT
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
# ============================================================================
|
| 293 |
+
# MCP RESOURCES - HTML/JS/CSS Widgets for ChatGPT Apps
|
| 294 |
+
# ============================================================================
|
| 295 |
+
|
| 296 |
+
@gr.mcp.resource("ui://widget/organization.html", mime_type="text/html+skybridge")
|
| 297 |
+
def organization_widget():
|
| 298 |
+
"""
|
| 299 |
+
Widget for displaying organization discovery results in ChatGPT.
|
| 300 |
+
|
| 301 |
+
Renders a rich card UI showing organization profile data including:
|
| 302 |
+
- Organization name, logo, and sector
|
| 303 |
+
- Company size (Startup, SME, Enterprise) and headquarters location
|
| 304 |
+
- EU presence status and AI maturity level
|
| 305 |
+
- EU AI Act compliance framework badge
|
| 306 |
+
- Next compliance deadline with countdown
|
| 307 |
+
|
| 308 |
+
Handles loading, error, and timeout states gracefully.
|
| 309 |
+
Used with discover_organization MCP tool via openai/outputTemplate.
|
| 310 |
+
"""
|
| 311 |
+
return """
|
| 312 |
+
<style>
|
| 313 |
+
* { box-sizing: border-box; }
|
| 314 |
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
| 315 |
+
.org-card {
|
| 316 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%);
|
| 317 |
+
border-radius: 16px;
|
| 318 |
+
padding: 24px;
|
| 319 |
+
color: white;
|
| 320 |
+
max-width: 500px;
|
| 321 |
+
margin: 0 auto;
|
| 322 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
| 323 |
+
}
|
| 324 |
+
.org-header {
|
| 325 |
+
display: flex;
|
| 326 |
+
align-items: center;
|
| 327 |
+
gap: 16px;
|
| 328 |
+
margin-bottom: 20px;
|
| 329 |
+
}
|
| 330 |
+
.org-logo {
|
| 331 |
+
width: 64px;
|
| 332 |
+
height: 64px;
|
| 333 |
+
background: rgba(255,255,255,0.1);
|
| 334 |
+
border-radius: 12px;
|
| 335 |
+
display: flex;
|
| 336 |
+
align-items: center;
|
| 337 |
+
justify-content: center;
|
| 338 |
+
font-size: 32px;
|
| 339 |
+
}
|
| 340 |
+
.org-name {
|
| 341 |
+
font-size: 24px;
|
| 342 |
+
font-weight: 700;
|
| 343 |
+
margin: 0;
|
| 344 |
+
}
|
| 345 |
+
.org-sector {
|
| 346 |
+
font-size: 14px;
|
| 347 |
+
opacity: 0.8;
|
| 348 |
+
margin: 4px 0 0 0;
|
| 349 |
+
}
|
| 350 |
+
.info-grid {
|
| 351 |
+
display: grid;
|
| 352 |
+
grid-template-columns: repeat(2, 1fr);
|
| 353 |
+
gap: 12px;
|
| 354 |
+
margin: 20px 0;
|
| 355 |
+
}
|
| 356 |
+
.info-item {
|
| 357 |
+
background: rgba(255,255,255,0.1);
|
| 358 |
+
border-radius: 10px;
|
| 359 |
+
padding: 12px;
|
| 360 |
+
}
|
| 361 |
+
.info-label {
|
| 362 |
+
font-size: 11px;
|
| 363 |
+
text-transform: uppercase;
|
| 364 |
+
opacity: 0.7;
|
| 365 |
+
margin-bottom: 4px;
|
| 366 |
+
}
|
| 367 |
+
.info-value {
|
| 368 |
+
font-size: 16px;
|
| 369 |
+
font-weight: 600;
|
| 370 |
+
}
|
| 371 |
+
.compliance-badge {
|
| 372 |
+
display: inline-flex;
|
| 373 |
+
align-items: center;
|
| 374 |
+
gap: 6px;
|
| 375 |
+
background: rgba(255,255,255,0.15);
|
| 376 |
+
padding: 8px 12px;
|
| 377 |
+
border-radius: 20px;
|
| 378 |
+
font-size: 13px;
|
| 379 |
+
margin-top: 16px;
|
| 380 |
+
}
|
| 381 |
+
.eu-flag { font-size: 18px; }
|
| 382 |
+
.deadline {
|
| 383 |
+
background: rgba(255,193,7,0.2);
|
| 384 |
+
border: 1px solid rgba(255,193,7,0.4);
|
| 385 |
+
border-radius: 8px;
|
| 386 |
+
padding: 12px;
|
| 387 |
+
margin-top: 16px;
|
| 388 |
+
}
|
| 389 |
+
.deadline-title {
|
| 390 |
+
font-size: 12px;
|
| 391 |
+
font-weight: 600;
|
| 392 |
+
margin-bottom: 4px;
|
| 393 |
+
color: #ffc107;
|
| 394 |
+
}
|
| 395 |
+
.deadline-date {
|
| 396 |
+
font-size: 14px;
|
| 397 |
+
}
|
| 398 |
+
.error-card {
|
| 399 |
+
background: #f44336;
|
| 400 |
+
border-radius: 12px;
|
| 401 |
+
padding: 20px;
|
| 402 |
+
color: white;
|
| 403 |
+
text-align: center;
|
| 404 |
+
}
|
| 405 |
+
.processing-card {
|
| 406 |
+
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
| 407 |
+
border-radius: 16px;
|
| 408 |
+
padding: 24px;
|
| 409 |
+
color: white;
|
| 410 |
+
text-align: center;
|
| 411 |
+
}
|
| 412 |
+
.processing-icon {
|
| 413 |
+
font-size: 48px;
|
| 414 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 415 |
+
}
|
| 416 |
+
@keyframes pulse {
|
| 417 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 418 |
+
50% { opacity: 0.7; transform: scale(1.1); }
|
| 419 |
+
}
|
| 420 |
+
.processing-title {
|
| 421 |
+
font-size: 18px;
|
| 422 |
+
font-weight: 600;
|
| 423 |
+
margin: 12px 0 8px 0;
|
| 424 |
+
}
|
| 425 |
+
.processing-msg {
|
| 426 |
+
font-size: 14px;
|
| 427 |
+
opacity: 0.9;
|
| 428 |
+
line-height: 1.5;
|
| 429 |
+
}
|
| 430 |
+
.tip-box {
|
| 431 |
+
background: rgba(255,255,255,0.15);
|
| 432 |
+
border-radius: 8px;
|
| 433 |
+
padding: 10px;
|
| 434 |
+
margin-top: 12px;
|
| 435 |
+
font-size: 12px;
|
| 436 |
+
}
|
| 437 |
+
</style>
|
| 438 |
+
|
| 439 |
+
<div id="org-container">
|
| 440 |
+
<div class="org-card" id="card">
|
| 441 |
+
<div class="org-header">
|
| 442 |
+
<div class="org-logo" id="logo">🏢</div>
|
| 443 |
+
<div>
|
| 444 |
+
<h1 class="org-name" id="org-name">Loading...</h1>
|
| 445 |
+
<p class="org-sector" id="org-sector">Discovering organization...</p>
|
| 446 |
+
</div>
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<div class="info-grid">
|
| 450 |
+
<div class="info-item">
|
| 451 |
+
<div class="info-label">Size</div>
|
| 452 |
+
<div class="info-value" id="size">-</div>
|
| 453 |
+
</div>
|
| 454 |
+
<div class="info-item">
|
| 455 |
+
<div class="info-label">Headquarters</div>
|
| 456 |
+
<div class="info-value" id="hq">-</div>
|
| 457 |
+
</div>
|
| 458 |
+
<div class="info-item">
|
| 459 |
+
<div class="info-label">EU Presence</div>
|
| 460 |
+
<div class="info-value" id="eu-presence">-</div>
|
| 461 |
+
</div>
|
| 462 |
+
<div class="info-item">
|
| 463 |
+
<div class="info-label">AI Maturity</div>
|
| 464 |
+
<div class="info-value" id="ai-maturity">-</div>
|
| 465 |
+
</div>
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<div class="compliance-badge">
|
| 469 |
+
<span class="eu-flag">🇪🇺</span>
|
| 470 |
+
<span id="framework">EU AI Act Compliance Assessment</span>
|
| 471 |
+
</div>
|
| 472 |
+
|
| 473 |
+
<div class="deadline" id="deadline-container">
|
| 474 |
+
<div class="deadline-title">⏰ Next Compliance Deadline</div>
|
| 475 |
+
<div class="deadline-date" id="deadline">Loading...</div>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
</div>
|
| 479 |
+
|
| 480 |
+
<script>
|
| 481 |
+
function renderOrganization(data) {
|
| 482 |
+
if (!data || data.error) {
|
| 483 |
+
document.getElementById('org-container').innerHTML =
|
| 484 |
+
'<div class="error-card">❌ ' + (data?.message || 'Failed to load organization') + '</div>';
|
| 485 |
+
return;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
// Handle timeout/processing state
|
| 489 |
+
if (data.status === 'processing' || data._timeout) {
|
| 490 |
+
document.getElementById('org-container').innerHTML =
|
| 491 |
+
'<div class="processing-card">' +
|
| 492 |
+
'<div class="processing-icon">⏳</div>' +
|
| 493 |
+
'<div class="processing-title">Analysis in Progress</div>' +
|
| 494 |
+
'<div class="processing-msg">' + (data.message || 'Processing...') + '</div>' +
|
| 495 |
+
(data.tip ? '<div class="tip-box">💡 ' + data.tip + '</div>' : '') +
|
| 496 |
+
'</div>';
|
| 497 |
+
return;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
const org = data.organization || data;
|
| 501 |
+
const regulatory = data.regulatoryContext || {};
|
| 502 |
+
|
| 503 |
+
document.getElementById('org-name').textContent = org.name || 'Unknown';
|
| 504 |
+
document.getElementById('org-sector').textContent = org.sector || 'Technology';
|
| 505 |
+
document.getElementById('size').textContent = org.size || 'Unknown';
|
| 506 |
+
document.getElementById('hq').textContent =
|
| 507 |
+
(org.headquarters?.city || 'Unknown') + ', ' + (org.headquarters?.country || '');
|
| 508 |
+
document.getElementById('eu-presence').textContent = org.euPresence ? '✅ Yes' : '❌ No';
|
| 509 |
+
document.getElementById('ai-maturity').textContent = org.aiMaturityLevel || 'Unknown';
|
| 510 |
+
|
| 511 |
+
// Show nearest deadline
|
| 512 |
+
const deadlines = regulatory.complianceDeadlines || [];
|
| 513 |
+
if (deadlines.length > 0) {
|
| 514 |
+
const nearest = deadlines[0];
|
| 515 |
+
document.getElementById('deadline').textContent =
|
| 516 |
+
nearest.date + ' - ' + nearest.description;
|
| 517 |
+
} else {
|
| 518 |
+
document.getElementById('deadline-container').style.display = 'none';
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
function render() {
|
| 523 |
+
const data = window.openai?.toolOutput;
|
| 524 |
+
if (data) {
|
| 525 |
+
// Handle both direct text content and structured content
|
| 526 |
+
let parsedData = data;
|
| 527 |
+
if (typeof data === 'string') {
|
| 528 |
+
try { parsedData = JSON.parse(data); } catch (e) {}
|
| 529 |
+
} else if (data.text) {
|
| 530 |
+
try { parsedData = JSON.parse(data.text); } catch (e) { parsedData = data; }
|
| 531 |
+
} else if (data.content) {
|
| 532 |
+
for (const item of data.content) {
|
| 533 |
+
if (item.type === 'text') {
|
| 534 |
+
try { parsedData = JSON.parse(item.text); break; } catch (e) {}
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
}
|
| 538 |
+
renderOrganization(parsedData);
|
| 539 |
+
}
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
window.addEventListener("openai:set_globals", (event) => {
|
| 543 |
+
if (event.detail?.globals?.toolOutput) render();
|
| 544 |
+
}, { passive: true });
|
| 545 |
+
|
| 546 |
+
render();
|
| 547 |
+
</script>
|
| 548 |
+
"""
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
@gr.mcp.resource("ui://widget/ai-services.html", mime_type="text/html+skybridge")
|
| 552 |
+
def ai_services_widget():
|
| 553 |
+
"""
|
| 554 |
+
Widget for displaying AI services discovery results in ChatGPT.
|
| 555 |
+
|
| 556 |
+
Renders a comprehensive risk overview dashboard showing:
|
| 557 |
+
- Risk summary grid: Unacceptable, High, Limited, Minimal risk counts
|
| 558 |
+
- Color-coded system cards for each discovered AI system
|
| 559 |
+
- System details: name, purpose, risk score (0-100), conformity status
|
| 560 |
+
- Visual indicators for compliance requirements
|
| 561 |
+
|
| 562 |
+
Risk categories per EU AI Act:
|
| 563 |
+
- Unacceptable (red): Article 5 prohibited practices
|
| 564 |
+
- High (orange): Annex III systems requiring conformity assessment
|
| 565 |
+
- Limited (yellow): Article 50 transparency obligations
|
| 566 |
+
- Minimal (green): No specific obligations
|
| 567 |
+
|
| 568 |
+
Used with discover_ai_services MCP tool via openai/outputTemplate.
|
| 569 |
+
"""
|
| 570 |
+
return """
|
| 571 |
+
<style>
|
| 572 |
+
* { box-sizing: border-box; }
|
| 573 |
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
| 574 |
+
.services-container {
|
| 575 |
+
max-width: 600px;
|
| 576 |
+
margin: 0 auto;
|
| 577 |
+
padding: 16px;
|
| 578 |
+
}
|
| 579 |
+
.summary-card {
|
| 580 |
+
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
|
| 581 |
+
border-radius: 16px;
|
| 582 |
+
padding: 20px;
|
| 583 |
+
color: white;
|
| 584 |
+
margin-bottom: 16px;
|
| 585 |
+
}
|
| 586 |
+
.summary-title {
|
| 587 |
+
font-size: 18px;
|
| 588 |
+
font-weight: 700;
|
| 589 |
+
margin: 0 0 16px 0;
|
| 590 |
+
}
|
| 591 |
+
.risk-grid {
|
| 592 |
+
display: grid;
|
| 593 |
+
grid-template-columns: repeat(4, 1fr);
|
| 594 |
+
gap: 8px;
|
| 595 |
+
}
|
| 596 |
+
.risk-item {
|
| 597 |
+
text-align: center;
|
| 598 |
+
padding: 12px 8px;
|
| 599 |
+
background: rgba(255,255,255,0.15);
|
| 600 |
+
border-radius: 8px;
|
| 601 |
+
}
|
| 602 |
+
.risk-count {
|
| 603 |
+
font-size: 28px;
|
| 604 |
+
font-weight: 700;
|
| 605 |
+
}
|
| 606 |
+
.risk-label {
|
| 607 |
+
font-size: 10px;
|
| 608 |
+
text-transform: uppercase;
|
| 609 |
+
opacity: 0.8;
|
| 610 |
+
margin-top: 4px;
|
| 611 |
+
}
|
| 612 |
+
.risk-unacceptable { color: #ff1744; }
|
| 613 |
+
.risk-high { color: #ff9100; }
|
| 614 |
+
.risk-limited { color: #ffea00; }
|
| 615 |
+
.risk-minimal { color: #00e676; }
|
| 616 |
+
.system-card {
|
| 617 |
+
background: white;
|
| 618 |
+
border-radius: 12px;
|
| 619 |
+
padding: 16px;
|
| 620 |
+
margin-bottom: 12px;
|
| 621 |
+
border-left: 4px solid #ccc;
|
| 622 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
| 623 |
+
}
|
| 624 |
+
.system-card.high { border-left-color: #ff9100; background: #fff8e1; }
|
| 625 |
+
.system-card.limited { border-left-color: #ffc107; background: #fffde7; }
|
| 626 |
+
.system-card.minimal { border-left-color: #4caf50; background: #e8f5e9; }
|
| 627 |
+
.system-card.unacceptable { border-left-color: #f44336; background: #ffebee; }
|
| 628 |
+
.system-name {
|
| 629 |
+
font-size: 16px;
|
| 630 |
+
font-weight: 600;
|
| 631 |
+
margin: 0 0 8px 0;
|
| 632 |
+
color: #333;
|
| 633 |
+
}
|
| 634 |
+
.system-purpose {
|
| 635 |
+
font-size: 13px;
|
| 636 |
+
color: #666;
|
| 637 |
+
margin: 0 0 12px 0;
|
| 638 |
+
}
|
| 639 |
+
.system-meta {
|
| 640 |
+
display: flex;
|
| 641 |
+
gap: 12px;
|
| 642 |
+
flex-wrap: wrap;
|
| 643 |
+
}
|
| 644 |
+
.meta-badge {
|
| 645 |
+
display: inline-flex;
|
| 646 |
+
align-items: center;
|
| 647 |
+
gap: 4px;
|
| 648 |
+
font-size: 11px;
|
| 649 |
+
background: rgba(0,0,0,0.06);
|
| 650 |
+
padding: 4px 8px;
|
| 651 |
+
border-radius: 12px;
|
| 652 |
+
color: #555;
|
| 653 |
+
}
|
| 654 |
+
.empty-state {
|
| 655 |
+
text-align: center;
|
| 656 |
+
padding: 40px;
|
| 657 |
+
color: #666;
|
| 658 |
+
}
|
| 659 |
+
.processing-card {
|
| 660 |
+
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
| 661 |
+
border-radius: 16px;
|
| 662 |
+
padding: 24px;
|
| 663 |
+
color: white;
|
| 664 |
+
text-align: center;
|
| 665 |
+
}
|
| 666 |
+
.processing-icon {
|
| 667 |
+
font-size: 48px;
|
| 668 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 669 |
+
}
|
| 670 |
+
@keyframes pulse {
|
| 671 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 672 |
+
50% { opacity: 0.7; transform: scale(1.1); }
|
| 673 |
+
}
|
| 674 |
+
.processing-title {
|
| 675 |
+
font-size: 18px;
|
| 676 |
+
font-weight: 600;
|
| 677 |
+
margin: 12px 0 8px 0;
|
| 678 |
+
}
|
| 679 |
+
.processing-msg {
|
| 680 |
+
font-size: 14px;
|
| 681 |
+
opacity: 0.9;
|
| 682 |
+
line-height: 1.5;
|
| 683 |
+
}
|
| 684 |
+
.tip-box {
|
| 685 |
+
background: rgba(255,255,255,0.15);
|
| 686 |
+
border-radius: 8px;
|
| 687 |
+
padding: 10px;
|
| 688 |
+
margin-top: 12px;
|
| 689 |
+
font-size: 12px;
|
| 690 |
+
}
|
| 691 |
+
</style>
|
| 692 |
+
|
| 693 |
+
<div class="services-container" id="container">
|
| 694 |
+
<div class="summary-card">
|
| 695 |
+
<h2 class="summary-title">🤖 AI Systems Risk Overview</h2>
|
| 696 |
+
<div class="risk-grid">
|
| 697 |
+
<div class="risk-item">
|
| 698 |
+
<div class="risk-count risk-unacceptable" id="unacceptable">0</div>
|
| 699 |
+
<div class="risk-label">Unacceptable</div>
|
| 700 |
+
</div>
|
| 701 |
+
<div class="risk-item">
|
| 702 |
+
<div class="risk-count risk-high" id="high">0</div>
|
| 703 |
+
<div class="risk-label">High Risk</div>
|
| 704 |
+
</div>
|
| 705 |
+
<div class="risk-item">
|
| 706 |
+
<div class="risk-count risk-limited" id="limited">0</div>
|
| 707 |
+
<div class="risk-label">Limited</div>
|
| 708 |
+
</div>
|
| 709 |
+
<div class="risk-item">
|
| 710 |
+
<div class="risk-count risk-minimal" id="minimal">0</div>
|
| 711 |
+
<div class="risk-label">Minimal</div>
|
| 712 |
+
</div>
|
| 713 |
+
</div>
|
| 714 |
+
</div>
|
| 715 |
+
<div id="systems-list"></div>
|
| 716 |
+
</div>
|
| 717 |
+
|
| 718 |
+
<script>
|
| 719 |
+
function renderServices(data) {
|
| 720 |
+
if (!data || data.error) {
|
| 721 |
+
document.getElementById('systems-list').innerHTML =
|
| 722 |
+
'<div class="empty-state">❌ ' + (data?.message || 'No AI systems found') + '</div>';
|
| 723 |
+
return;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
// Handle timeout/processing state
|
| 727 |
+
if (data.status === 'processing' || data._timeout) {
|
| 728 |
+
document.getElementById('container').innerHTML =
|
| 729 |
+
'<div class="processing-card">' +
|
| 730 |
+
'<div class="processing-icon">⏳</div>' +
|
| 731 |
+
'<div class="processing-title">Analysis in Progress</div>' +
|
| 732 |
+
'<div class="processing-msg">' + (data.message || 'Processing...') + '</div>' +
|
| 733 |
+
(data.tip ? '<div class="tip-box">💡 ' + data.tip + '</div>' : '') +
|
| 734 |
+
'</div>';
|
| 735 |
+
return;
|
| 736 |
+
}
|
| 737 |
+
|
| 738 |
+
const summary = data.riskSummary || {};
|
| 739 |
+
document.getElementById('unacceptable').textContent = summary.unacceptableRiskCount || 0;
|
| 740 |
+
document.getElementById('high').textContent = summary.highRiskCount || 0;
|
| 741 |
+
document.getElementById('limited').textContent = summary.limitedRiskCount || 0;
|
| 742 |
+
document.getElementById('minimal').textContent = summary.minimalRiskCount || 0;
|
| 743 |
+
|
| 744 |
+
const systems = data.systems || [];
|
| 745 |
+
const listHtml = systems.map(sys => {
|
| 746 |
+
const risk = sys.riskClassification?.category?.toLowerCase() || 'minimal';
|
| 747 |
+
const name = sys.system?.name || 'Unknown System';
|
| 748 |
+
const purpose = sys.system?.intendedPurpose || 'No description';
|
| 749 |
+
const score = sys.riskClassification?.riskScore || 0;
|
| 750 |
+
const conformity = sys.riskClassification?.conformityAssessmentRequired ? '⚠️ Required' : '✅ Not Required';
|
| 751 |
+
|
| 752 |
+
return '<div class="system-card ' + risk + '">' +
|
| 753 |
+
'<h3 class="system-name">' + name + '</h3>' +
|
| 754 |
+
'<p class="system-purpose">' + purpose.substring(0, 120) + (purpose.length > 120 ? '...' : '') + '</p>' +
|
| 755 |
+
'<div class="system-meta">' +
|
| 756 |
+
'<span class="meta-badge">📊 Risk: ' + score + '/100</span>' +
|
| 757 |
+
'<span class="meta-badge">📋 Conformity: ' + conformity + '</span>' +
|
| 758 |
+
'</div></div>';
|
| 759 |
+
}).join('');
|
| 760 |
+
|
| 761 |
+
document.getElementById('systems-list').innerHTML = listHtml || '<div class="empty-state">No systems discovered</div>';
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
function render() {
|
| 765 |
+
const data = window.openai?.toolOutput;
|
| 766 |
+
if (data) {
|
| 767 |
+
let parsedData = data;
|
| 768 |
+
if (typeof data === 'string') {
|
| 769 |
+
try { parsedData = JSON.parse(data); } catch (e) {}
|
| 770 |
+
} else if (data.text) {
|
| 771 |
+
try { parsedData = JSON.parse(data.text); } catch (e) { parsedData = data; }
|
| 772 |
+
} else if (data.content) {
|
| 773 |
+
for (const item of data.content) {
|
| 774 |
+
if (item.type === 'text') {
|
| 775 |
+
try { parsedData = JSON.parse(item.text); break; } catch (e) {}
|
| 776 |
+
}
|
| 777 |
+
}
|
| 778 |
+
}
|
| 779 |
+
renderServices(parsedData);
|
| 780 |
+
}
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
window.addEventListener("openai:set_globals", (event) => {
|
| 784 |
+
if (event.detail?.globals?.toolOutput) render();
|
| 785 |
+
}, { passive: true });
|
| 786 |
+
|
| 787 |
+
render();
|
| 788 |
+
</script>
|
| 789 |
+
"""
|
| 790 |
+
|
| 791 |
+
|
| 792 |
+
@gr.mcp.resource("ui://widget/compliance.html", mime_type="text/html+skybridge")
|
| 793 |
+
def compliance_widget():
|
| 794 |
+
"""
|
| 795 |
+
Widget for displaying EU AI Act compliance assessment results in ChatGPT.
|
| 796 |
+
|
| 797 |
+
Renders an interactive compliance dashboard showing:
|
| 798 |
+
- Animated score ring: Overall compliance score (0-100) with color gradient
|
| 799 |
+
- Risk level badge: CRITICAL, HIGH, MEDIUM, or LOW
|
| 800 |
+
- Stats grid: Compliance gaps count and recommendations count
|
| 801 |
+
- Priority gaps section: Top 3 gaps with severity and Article references
|
| 802 |
+
- Recommendations section: Top 3 prioritized action items
|
| 803 |
+
- Action button: Re-run full assessment with documentation generation
|
| 804 |
+
|
| 805 |
+
Score color coding:
|
| 806 |
+
- Green (80+): Good compliance posture
|
| 807 |
+
- Yellow (60-79): Moderate gaps to address
|
| 808 |
+
- Orange (40-59): Significant compliance work needed
|
| 809 |
+
- Red (<40): Critical compliance issues
|
| 810 |
+
|
| 811 |
+
Used with assess_compliance MCP tool via openai/outputTemplate.
|
| 812 |
+
Includes window.openai.callTool() for interactive re-assessment.
|
| 813 |
+
"""
|
| 814 |
+
return """
|
| 815 |
+
<style>
|
| 816 |
+
* { box-sizing: border-box; }
|
| 817 |
+
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
| 818 |
+
.compliance-container {
|
| 819 |
+
max-width: 600px;
|
| 820 |
+
margin: 0 auto;
|
| 821 |
+
padding: 16px;
|
| 822 |
+
}
|
| 823 |
+
.score-card {
|
| 824 |
+
background: linear-gradient(135deg, #1a237e 0%, #283593 100%);
|
| 825 |
+
border-radius: 20px;
|
| 826 |
+
padding: 24px;
|
| 827 |
+
color: white;
|
| 828 |
+
text-align: center;
|
| 829 |
+
margin-bottom: 16px;
|
| 830 |
+
}
|
| 831 |
+
.score-ring {
|
| 832 |
+
width: 140px;
|
| 833 |
+
height: 140px;
|
| 834 |
+
margin: 0 auto 16px;
|
| 835 |
+
position: relative;
|
| 836 |
+
}
|
| 837 |
+
.score-ring svg {
|
| 838 |
+
transform: rotate(-90deg);
|
| 839 |
+
width: 100%;
|
| 840 |
+
height: 100%;
|
| 841 |
+
}
|
| 842 |
+
.score-ring circle {
|
| 843 |
+
fill: none;
|
| 844 |
+
stroke-width: 12;
|
| 845 |
+
}
|
| 846 |
+
.score-ring .bg { stroke: rgba(255,255,255,0.2); }
|
| 847 |
+
.score-ring .progress { stroke: #4caf50; stroke-linecap: round; transition: stroke-dashoffset 1s ease; }
|
| 848 |
+
.score-value {
|
| 849 |
+
position: absolute;
|
| 850 |
+
top: 50%;
|
| 851 |
+
left: 50%;
|
| 852 |
+
transform: translate(-50%, -50%);
|
| 853 |
+
font-size: 42px;
|
| 854 |
+
font-weight: 800;
|
| 855 |
+
}
|
| 856 |
+
.score-label {
|
| 857 |
+
font-size: 14px;
|
| 858 |
+
opacity: 0.8;
|
| 859 |
+
}
|
| 860 |
+
.risk-badge {
|
| 861 |
+
display: inline-block;
|
| 862 |
+
padding: 8px 16px;
|
| 863 |
+
border-radius: 20px;
|
| 864 |
+
font-weight: 600;
|
| 865 |
+
font-size: 14px;
|
| 866 |
+
margin-top: 12px;
|
| 867 |
+
}
|
| 868 |
+
.risk-critical { background: #f44336; }
|
| 869 |
+
.risk-high { background: #ff9800; }
|
| 870 |
+
.risk-medium { background: #ffc107; color: #333; }
|
| 871 |
+
.risk-low { background: #4caf50; }
|
| 872 |
+
.stats-grid {
|
| 873 |
+
display: grid;
|
| 874 |
+
grid-template-columns: repeat(2, 1fr);
|
| 875 |
+
gap: 12px;
|
| 876 |
+
margin-bottom: 16px;
|
| 877 |
+
}
|
| 878 |
+
.stat-card {
|
| 879 |
+
background: white;
|
| 880 |
+
border-radius: 12px;
|
| 881 |
+
padding: 16px;
|
| 882 |
+
text-align: center;
|
| 883 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
| 884 |
+
}
|
| 885 |
+
.stat-value {
|
| 886 |
+
font-size: 28px;
|
| 887 |
+
font-weight: 700;
|
| 888 |
+
color: #333;
|
| 889 |
+
}
|
| 890 |
+
.stat-label {
|
| 891 |
+
font-size: 12px;
|
| 892 |
+
color: #666;
|
| 893 |
+
margin-top: 4px;
|
| 894 |
+
}
|
| 895 |
+
.gaps-section, .recs-section {
|
| 896 |
+
background: white;
|
| 897 |
+
border-radius: 12px;
|
| 898 |
+
padding: 16px;
|
| 899 |
+
margin-bottom: 12px;
|
| 900 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
| 901 |
+
}
|
| 902 |
+
.section-title {
|
| 903 |
+
font-size: 16px;
|
| 904 |
+
font-weight: 700;
|
| 905 |
+
color: #333;
|
| 906 |
+
margin: 0 0 12px 0;
|
| 907 |
+
}
|
| 908 |
+
.gap-item, .rec-item {
|
| 909 |
+
padding: 10px 12px;
|
| 910 |
+
background: #f5f5f5;
|
| 911 |
+
border-radius: 8px;
|
| 912 |
+
margin-bottom: 8px;
|
| 913 |
+
font-size: 13px;
|
| 914 |
+
}
|
| 915 |
+
.gap-item.critical { background: #ffebee; border-left: 3px solid #f44336; }
|
| 916 |
+
.gap-item.high { background: #fff3e0; border-left: 3px solid #ff9800; }
|
| 917 |
+
.gap-article {
|
| 918 |
+
font-size: 11px;
|
| 919 |
+
color: #666;
|
| 920 |
+
margin-top: 4px;
|
| 921 |
+
}
|
| 922 |
+
.rec-priority {
|
| 923 |
+
display: inline-block;
|
| 924 |
+
background: #e3f2fd;
|
| 925 |
+
color: #1976d2;
|
| 926 |
+
padding: 2px 8px;
|
| 927 |
+
border-radius: 10px;
|
| 928 |
+
font-size: 10px;
|
| 929 |
+
font-weight: 600;
|
| 930 |
+
margin-right: 8px;
|
| 931 |
+
}
|
| 932 |
+
.action-btn {
|
| 933 |
+
display: block;
|
| 934 |
+
width: 100%;
|
| 935 |
+
padding: 14px;
|
| 936 |
+
background: #1976d2;
|
| 937 |
+
color: white;
|
| 938 |
+
border: none;
|
| 939 |
+
border-radius: 10px;
|
| 940 |
+
font-size: 14px;
|
| 941 |
+
font-weight: 600;
|
| 942 |
+
cursor: pointer;
|
| 943 |
+
margin-top: 16px;
|
| 944 |
+
}
|
| 945 |
+
.action-btn:hover { background: #1565c0; }
|
| 946 |
+
.processing-card {
|
| 947 |
+
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
| 948 |
+
border-radius: 16px;
|
| 949 |
+
padding: 24px;
|
| 950 |
+
color: white;
|
| 951 |
+
text-align: center;
|
| 952 |
+
}
|
| 953 |
+
.processing-icon {
|
| 954 |
+
font-size: 48px;
|
| 955 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 956 |
+
}
|
| 957 |
+
@keyframes pulse {
|
| 958 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 959 |
+
50% { opacity: 0.7; transform: scale(1.1); }
|
| 960 |
+
}
|
| 961 |
+
.processing-title {
|
| 962 |
+
font-size: 18px;
|
| 963 |
+
font-weight: 600;
|
| 964 |
+
margin: 12px 0 8px 0;
|
| 965 |
+
}
|
| 966 |
+
.processing-msg {
|
| 967 |
+
font-size: 14px;
|
| 968 |
+
opacity: 0.9;
|
| 969 |
+
line-height: 1.5;
|
| 970 |
+
}
|
| 971 |
+
.tip-box {
|
| 972 |
+
background: rgba(255,255,255,0.15);
|
| 973 |
+
border-radius: 8px;
|
| 974 |
+
padding: 10px;
|
| 975 |
+
margin-top: 12px;
|
| 976 |
+
font-size: 12px;
|
| 977 |
+
}
|
| 978 |
+
</style>
|
| 979 |
+
|
| 980 |
+
<div class="compliance-container" id="container">
|
| 981 |
+
<div class="score-card">
|
| 982 |
+
<div class="score-ring">
|
| 983 |
+
<svg viewBox="0 0 100 100">
|
| 984 |
+
<circle class="bg" cx="50" cy="50" r="42"/>
|
| 985 |
+
<circle class="progress" id="progress-ring" cx="50" cy="50" r="42"
|
| 986 |
+
stroke-dasharray="264" stroke-dashoffset="264"/>
|
| 987 |
+
</svg>
|
| 988 |
+
<div class="score-value" id="score">--</div>
|
| 989 |
+
</div>
|
| 990 |
+
<div class="score-label">EU AI Act Compliance Score</div>
|
| 991 |
+
<div class="risk-badge risk-medium" id="risk-badge">Calculating...</div>
|
| 992 |
+
</div>
|
| 993 |
+
|
| 994 |
+
<div class="stats-grid">
|
| 995 |
+
<div class="stat-card">
|
| 996 |
+
<div class="stat-value" id="gaps-count">-</div>
|
| 997 |
+
<div class="stat-label">Compliance Gaps</div>
|
| 998 |
+
</div>
|
| 999 |
+
<div class="stat-card">
|
| 1000 |
+
<div class="stat-value" id="recs-count">-</div>
|
| 1001 |
+
<div class="stat-label">Recommendations</div>
|
| 1002 |
+
</div>
|
| 1003 |
+
</div>
|
| 1004 |
+
|
| 1005 |
+
<div class="gaps-section">
|
| 1006 |
+
<h3 class="section-title">⚠️ Priority Gaps</h3>
|
| 1007 |
+
<div id="gaps-list"></div>
|
| 1008 |
+
</div>
|
| 1009 |
+
|
| 1010 |
+
<div class="recs-section">
|
| 1011 |
+
<h3 class="section-title">💡 Top Recommendations</h3>
|
| 1012 |
+
<div id="recs-list"></div>
|
| 1013 |
+
</div>
|
| 1014 |
+
|
| 1015 |
+
<button class="action-btn" id="run-again" style="display:none;">
|
| 1016 |
+
🔄 Run Full Assessment
|
| 1017 |
+
</button>
|
| 1018 |
+
</div>
|
| 1019 |
+
|
| 1020 |
+
<script>
|
| 1021 |
+
function renderCompliance(data) {
|
| 1022 |
+
if (!data || data.error) {
|
| 1023 |
+
document.getElementById('score').textContent = '❌';
|
| 1024 |
+
document.getElementById('gaps-list').innerHTML = '<div style="color:#999;text-align:center;">Error: ' + (data?.message || 'Assessment failed') + '</div>';
|
| 1025 |
+
return;
|
| 1026 |
+
}
|
| 1027 |
+
|
| 1028 |
+
// Handle timeout/processing state
|
| 1029 |
+
if (data.status === 'processing' || data._timeout) {
|
| 1030 |
+
document.getElementById('container').innerHTML =
|
| 1031 |
+
'<div class="processing-card">' +
|
| 1032 |
+
'<div class="processing-icon">⏳</div>' +
|
| 1033 |
+
'<div class="processing-title">Compliance Assessment in Progress</div>' +
|
| 1034 |
+
'<div class="processing-msg">' + (data.message || 'Processing...') + '</div>' +
|
| 1035 |
+
(data.tip ? '<div class="tip-box">💡 ' + data.tip + '</div>' : '') +
|
| 1036 |
+
'</div>';
|
| 1037 |
+
return;
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
const assessment = data.assessment || data;
|
| 1041 |
+
const score = assessment.overallScore || 0;
|
| 1042 |
+
const riskLevel = assessment.riskLevel || 'MEDIUM';
|
| 1043 |
+
const gaps = assessment.gaps || [];
|
| 1044 |
+
const recs = assessment.recommendations || [];
|
| 1045 |
+
|
| 1046 |
+
// Animate score ring
|
| 1047 |
+
document.getElementById('score').textContent = score;
|
| 1048 |
+
const offset = 264 - (264 * score / 100);
|
| 1049 |
+
document.getElementById('progress-ring').style.strokeDashoffset = offset;
|
| 1050 |
+
|
| 1051 |
+
// Set progress color based on score
|
| 1052 |
+
const progressEl = document.getElementById('progress-ring');
|
| 1053 |
+
if (score >= 80) progressEl.style.stroke = '#4caf50';
|
| 1054 |
+
else if (score >= 60) progressEl.style.stroke = '#ffc107';
|
| 1055 |
+
else if (score >= 40) progressEl.style.stroke = '#ff9800';
|
| 1056 |
+
else progressEl.style.stroke = '#f44336';
|
| 1057 |
+
|
| 1058 |
+
// Risk badge
|
| 1059 |
+
const badgeEl = document.getElementById('risk-badge');
|
| 1060 |
+
badgeEl.textContent = riskLevel + ' Risk';
|
| 1061 |
+
badgeEl.className = 'risk-badge risk-' + riskLevel.toLowerCase();
|
| 1062 |
+
|
| 1063 |
+
// Stats
|
| 1064 |
+
document.getElementById('gaps-count').textContent = gaps.length;
|
| 1065 |
+
document.getElementById('recs-count').textContent = recs.length;
|
| 1066 |
+
|
| 1067 |
+
// Top gaps (show critical/high first)
|
| 1068 |
+
const topGaps = gaps
|
| 1069 |
+
.sort((a, b) => {
|
| 1070 |
+
const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
|
| 1071 |
+
return (order[a.severity] || 3) - (order[b.severity] || 3);
|
| 1072 |
+
})
|
| 1073 |
+
.slice(0, 3);
|
| 1074 |
+
|
| 1075 |
+
const gapsHtml = topGaps.map(gap => {
|
| 1076 |
+
const severity = (gap.severity || 'medium').toLowerCase();
|
| 1077 |
+
return '<div class="gap-item ' + severity + '">' +
|
| 1078 |
+
'<strong>' + (gap.category || 'Compliance') + ':</strong> ' +
|
| 1079 |
+
(gap.description || 'Gap identified').substring(0, 100) + '...' +
|
| 1080 |
+
'<div class="gap-article">' + (gap.articleReference || '') + '</div>' +
|
| 1081 |
+
'</div>';
|
| 1082 |
+
}).join('');
|
| 1083 |
+
|
| 1084 |
+
document.getElementById('gaps-list').innerHTML = gapsHtml || '<div style="color:#999;">No gaps identified</div>';
|
| 1085 |
+
|
| 1086 |
+
// Top recommendations
|
| 1087 |
+
const topRecs = recs.sort((a, b) => (a.priority || 10) - (b.priority || 10)).slice(0, 3);
|
| 1088 |
+
const recsHtml = topRecs.map(rec => {
|
| 1089 |
+
return '<div class="rec-item">' +
|
| 1090 |
+
'<span class="rec-priority">Priority ' + (rec.priority || '-') + '</span>' +
|
| 1091 |
+
(rec.title || rec.description || 'Recommendation').substring(0, 80) +
|
| 1092 |
+
'</div>';
|
| 1093 |
+
}).join('');
|
| 1094 |
+
|
| 1095 |
+
document.getElementById('recs-list').innerHTML = recsHtml || '<div style="color:#999;">No recommendations</div>';
|
| 1096 |
+
|
| 1097 |
+
// Show action button
|
| 1098 |
+
document.getElementById('run-again').style.display = 'block';
|
| 1099 |
+
document.getElementById('run-again').onclick = async function() {
|
| 1100 |
+
this.textContent = '⏳ Running...';
|
| 1101 |
+
this.disabled = true;
|
| 1102 |
+
try {
|
| 1103 |
+
await window.openai.callTool('assess_compliance', {
|
| 1104 |
+
organization_context: data.metadata?.organizationAssessed ? { name: data.metadata.organizationAssessed } : null,
|
| 1105 |
+
generate_documentation: true
|
| 1106 |
+
});
|
| 1107 |
+
} catch (e) {
|
| 1108 |
+
console.error(e);
|
| 1109 |
+
}
|
| 1110 |
+
this.textContent = '🔄 Run Full Assessment';
|
| 1111 |
+
this.disabled = false;
|
| 1112 |
+
};
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
function render() {
|
| 1116 |
+
const data = window.openai?.toolOutput;
|
| 1117 |
+
if (data) {
|
| 1118 |
+
let parsedData = data;
|
| 1119 |
+
if (typeof data === 'string') {
|
| 1120 |
+
try { parsedData = JSON.parse(data); } catch (e) {}
|
| 1121 |
+
} else if (data.text) {
|
| 1122 |
+
try { parsedData = JSON.parse(data.text); } catch (e) { parsedData = data; }
|
| 1123 |
+
} else if (data.content) {
|
| 1124 |
+
for (const item of data.content) {
|
| 1125 |
+
if (item.type === 'text') {
|
| 1126 |
+
try { parsedData = JSON.parse(item.text); break; } catch (e) {}
|
| 1127 |
+
}
|
| 1128 |
+
}
|
| 1129 |
+
}
|
| 1130 |
+
renderCompliance(parsedData);
|
| 1131 |
+
}
|
| 1132 |
+
}
|
| 1133 |
+
|
| 1134 |
+
window.addEventListener("openai:set_globals", (event) => {
|
| 1135 |
+
if (event.detail?.globals?.toolOutput) render();
|
| 1136 |
+
}, { passive: true });
|
| 1137 |
+
|
| 1138 |
+
render();
|
| 1139 |
+
</script>
|
| 1140 |
+
"""
|
| 1141 |
+
|
| 1142 |
+
|
| 1143 |
+
# ============================================================================
|
| 1144 |
+
# GRADIO UI - For testing tools and displaying resource code
|
| 1145 |
+
# ============================================================================
|
| 1146 |
+
|
| 1147 |
+
# Build header based on environment
|
| 1148 |
+
_is_production = bool(PUBLIC_URL)
|
| 1149 |
+
if _is_production:
|
| 1150 |
+
_mcp_url = f"{PUBLIC_URL.rstrip('/')}/gradio_api/mcp/"
|
| 1151 |
+
_env_info = f"""
|
| 1152 |
+
<div style="background: rgba(76, 175, 80, 0.2); border: 1px solid rgba(76, 175, 80, 0.4); border-radius: 8px; padding: 12px; margin-top: 15px;">
|
| 1153 |
+
<p style="margin: 0; font-size: 0.85em;">🌐 <strong>Production Mode - MCP Server Ready</strong></p>
|
| 1154 |
+
<p style="margin: 8px 0 0 0; font-size: 0.9em;">
|
| 1155 |
+
<strong>MCP URL (copy this):</strong><br>
|
| 1156 |
+
<code style="background: rgba(255,255,255,0.3); padding: 6px 10px; border-radius: 4px; display: inline-block; margin-top: 4px; word-break: break-all; font-size: 0.85em;">{_mcp_url}</code>
|
| 1157 |
+
</p>
|
| 1158 |
+
<p style="margin: 10px 0 0 0; font-size: 0.75em; opacity: 0.8;">
|
| 1159 |
+
ChatGPT → Settings → Apps & Connectors → Create Connector → Paste URL
|
| 1160 |
+
</p>
|
| 1161 |
+
</div>
|
| 1162 |
+
"""
|
| 1163 |
+
else:
|
| 1164 |
+
_env_info = """
|
| 1165 |
+
<div style="background: rgba(33, 150, 243, 0.2); border: 1px solid rgba(33, 150, 243, 0.4); border-radius: 8px; padding: 12px; margin-top: 15px;">
|
| 1166 |
+
<p style="margin: 0; font-size: 0.85em;">🛠️ <strong>Local Development</strong></p>
|
| 1167 |
+
<p style="margin: 5px 0 0 0; font-size: 0.8em; opacity: 0.9;">MCP URL: <code>http://localhost:7860/gradio_api/mcp/</code></p>
|
| 1168 |
+
<p style="margin: 8px 0 0 0; font-size: 0.8em;">For public URL, run with <code>GRADIO_SHARE=true</code></p>
|
| 1169 |
+
</div>
|
| 1170 |
+
"""
|
| 1171 |
+
|
| 1172 |
+
with gr.Blocks(
|
| 1173 |
+
title="🇪🇺 EU AI Act - ChatGPT App",
|
| 1174 |
+
) as demo:
|
| 1175 |
+
|
| 1176 |
+
gr.HTML(f"""
|
| 1177 |
+
<div style="text-align: center; padding: 20px 0; background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%); border-radius: 12px; color: white; margin-bottom: 20px;">
|
| 1178 |
+
<h1 style="margin: 0; font-size: 2em;">🇪🇺 EU AI Act Compliance</h1>
|
| 1179 |
+
<p style="margin: 10px 0 0 0; opacity: 0.9;">ChatGPT App powered by Gradio MCP</p>
|
| 1180 |
+
<p style="margin: 5px 0 0 0; font-size: 0.85em; opacity: 0.7;">by <a href="https://www.legitima.ai/mcp-hackathon" target="_blank" style="color: #90CAF9;">Legitima.ai</a></p>
|
| 1181 |
+
{_env_info}
|
| 1182 |
+
</div>
|
| 1183 |
+
""")
|
| 1184 |
+
|
| 1185 |
+
gr.Markdown("""
|
| 1186 |
+
## 🚀 How to Use in ChatGPT
|
| 1187 |
+
|
| 1188 |
+
1. **Get the MCP URL** from the terminal/Space logs
|
| 1189 |
+
⚠️ **Important:** The URL must end with `/gradio_api/mcp/`
|
| 1190 |
+
Example: `https://xxx.gradio.live/gradio_api/mcp/`
|
| 1191 |
+
2. **Enable Developer Mode** in ChatGPT: Settings → Apps & Connectors → Advanced settings
|
| 1192 |
+
3. **Create a Connector** with the MCP server URL (choose "No authentication")
|
| 1193 |
+
4. **Chat with ChatGPT** using `@eu-ai-act` to access these tools
|
| 1194 |
+
|
| 1195 |
+
---
|
| 1196 |
+
""")
|
| 1197 |
+
|
| 1198 |
+
with gr.Tab("🔧 Test Tools"):
|
| 1199 |
+
gr.Markdown("### Test MCP Tools Directly")
|
| 1200 |
+
|
| 1201 |
+
with gr.Row():
|
| 1202 |
+
with gr.Column():
|
| 1203 |
+
org_name = gr.Textbox(label="Organization Name", placeholder="e.g., Microsoft, IBM, OpenAI")
|
| 1204 |
+
org_domain = gr.Textbox(label="Domain (optional)", placeholder="e.g., microsoft.com")
|
| 1205 |
+
org_context = gr.Textbox(label="Context (optional)", placeholder="Additional context...")
|
| 1206 |
+
discover_btn = gr.Button("🔍 Discover Organization", variant="primary")
|
| 1207 |
+
|
| 1208 |
+
with gr.Column():
|
| 1209 |
+
org_result = gr.JSON(label="Organization Profile")
|
| 1210 |
+
|
| 1211 |
+
gr.Markdown("---")
|
| 1212 |
+
|
| 1213 |
+
with gr.Row():
|
| 1214 |
+
with gr.Column():
|
| 1215 |
+
ai_systems_input = gr.Textbox(label="System Names (comma-separated)", placeholder="e.g., Watson, Copilot")
|
| 1216 |
+
ai_scope = gr.Dropdown(choices=["all", "high-risk-only", "production-only"], value="all", label="Scope")
|
| 1217 |
+
discover_ai_btn = gr.Button("🤖 Discover AI Services", variant="primary")
|
| 1218 |
+
|
| 1219 |
+
with gr.Column():
|
| 1220 |
+
ai_result = gr.JSON(label="AI Services Discovery")
|
| 1221 |
+
|
| 1222 |
+
gr.Markdown("---")
|
| 1223 |
+
|
| 1224 |
+
with gr.Row():
|
| 1225 |
+
with gr.Column():
|
| 1226 |
+
gen_docs = gr.Checkbox(label="Generate Documentation", value=True)
|
| 1227 |
+
assess_btn = gr.Button("📊 Assess Compliance", variant="primary")
|
| 1228 |
+
|
| 1229 |
+
with gr.Column():
|
| 1230 |
+
compliance_result = gr.JSON(label="Compliance Assessment")
|
| 1231 |
+
|
| 1232 |
+
with gr.Tab("📝 Widget Code"):
|
| 1233 |
+
gr.Markdown("### MCP Resource Widgets (HTML/JS/CSS)")
|
| 1234 |
+
gr.Markdown("These widgets are displayed in ChatGPT when tools are called.")
|
| 1235 |
+
|
| 1236 |
+
# Pre-load widget code on startup to ensure MCP resources are registered
|
| 1237 |
+
org_html = gr.Code(language="html", label="organization.html", value=organization_widget())
|
| 1238 |
+
ai_html = gr.Code(language="html", label="ai-services.html", value=ai_services_widget())
|
| 1239 |
+
comp_html = gr.Code(language="html", label="compliance.html", value=compliance_widget())
|
| 1240 |
+
|
| 1241 |
+
# Also keep button handlers for refreshing
|
| 1242 |
+
with gr.Row():
|
| 1243 |
+
org_btn = gr.Button("🔄 Refresh Org Widget")
|
| 1244 |
+
ai_btn = gr.Button("🔄 Refresh AI Widget")
|
| 1245 |
+
comp_btn = gr.Button("🔄 Refresh Compliance Widget")
|
| 1246 |
+
|
| 1247 |
+
org_btn.click(organization_widget, outputs=org_html)
|
| 1248 |
+
ai_btn.click(ai_services_widget, outputs=ai_html)
|
| 1249 |
+
comp_btn.click(compliance_widget, outputs=comp_html)
|
| 1250 |
+
|
| 1251 |
+
with gr.Tab("ℹ️ About"):
|
| 1252 |
+
gr.Markdown("""
|
| 1253 |
+
## About This ChatGPT App
|
| 1254 |
+
|
| 1255 |
+
This Gradio app exposes **EU AI Act compliance tools** as a ChatGPT App using the
|
| 1256 |
+
[Gradio MCP Server](https://www.gradio.app/guides/building-chatgpt-apps-with-gradio) capabilities.
|
| 1257 |
+
|
| 1258 |
+
### Available Tools
|
| 1259 |
+
|
| 1260 |
+
| Tool | Description | EU AI Act Articles |
|
| 1261 |
+
|------|-------------|-------------------|
|
| 1262 |
+
| `discover_organization` | Research organization profile | Articles 16, 22, 49 |
|
| 1263 |
+
| `discover_ai_services` | Classify AI systems by risk | Articles 6, 11, Annex III |
|
| 1264 |
+
| `assess_compliance` | Generate compliance report | Articles 9-15, 43-50 |
|
| 1265 |
+
|
| 1266 |
+
### Key Features
|
| 1267 |
+
|
| 1268 |
+
- 🏢 **Organization Discovery**: Automatic research using Tavily AI or model fallback
|
| 1269 |
+
- 🤖 **AI Systems Classification**: Risk categorization per EU AI Act Annex III
|
| 1270 |
+
- 📊 **Compliance Assessment**: Gap analysis and documentation generation
|
| 1271 |
+
- 🎨 **Beautiful Widgets**: Rich UI cards displayed directly in ChatGPT
|
| 1272 |
+
|
| 1273 |
+
### Tech Stack
|
| 1274 |
+
|
| 1275 |
+
- **Gradio** with MCP server (`gradio[mcp]>=6.0`)
|
| 1276 |
+
- **OpenAI Apps SDK** compatible widgets
|
| 1277 |
+
- **Node.js API** backend with Vercel AI SDK
|
| 1278 |
+
|
| 1279 |
+
---
|
| 1280 |
+
|
| 1281 |
+
Built for the **MCP 1st Birthday Hackathon** 🎂
|
| 1282 |
+
""")
|
| 1283 |
+
|
| 1284 |
+
# Event handlers for Gradio UI testing
|
| 1285 |
+
def run_discover_org(name, domain, context):
|
| 1286 |
+
"""
|
| 1287 |
+
Discover organization profile for EU AI Act compliance.
|
| 1288 |
+
|
| 1289 |
+
Researches an organization using Tavily AI search or AI model fallback to create
|
| 1290 |
+
a comprehensive profile including sector, size, EU presence, headquarters,
|
| 1291 |
+
certifications, and regulatory context per EU AI Act Articles 16, 22, and 49.
|
| 1292 |
+
|
| 1293 |
+
Parameters:
|
| 1294 |
+
name (str): Organization name to discover (e.g., 'IBM', 'Microsoft', 'OpenAI')
|
| 1295 |
+
domain (str): Organization's domain (e.g., 'ibm.com'). Auto-discovered if empty.
|
| 1296 |
+
context (str): Additional context about the organization
|
| 1297 |
+
|
| 1298 |
+
Returns:
|
| 1299 |
+
dict: Organization profile with regulatory context and compliance deadlines
|
| 1300 |
+
"""
|
| 1301 |
+
if not name:
|
| 1302 |
+
return {"error": "Please enter an organization name"}
|
| 1303 |
+
return discover_organization(name, domain or None, context or None)
|
| 1304 |
+
|
| 1305 |
+
def run_discover_ai(systems, scope):
|
| 1306 |
+
"""
|
| 1307 |
+
Discover and classify AI systems per EU AI Act Annex III risk categories.
|
| 1308 |
+
|
| 1309 |
+
Scans for AI systems and classifies them according to EU AI Act risk tiers:
|
| 1310 |
+
Unacceptable (Article 5), High (Annex III), Limited (Article 50), or Minimal.
|
| 1311 |
+
Analyzes compliance gaps and conformity assessment requirements.
|
| 1312 |
+
|
| 1313 |
+
Parameters:
|
| 1314 |
+
systems (str): Comma-separated AI system names to discover (e.g., 'Watson, Copilot')
|
| 1315 |
+
scope (str): Discovery scope - 'all', 'high-risk-only', or 'production-only'
|
| 1316 |
+
|
| 1317 |
+
Returns:
|
| 1318 |
+
dict: AI systems with risk classifications, compliance gaps, and deadlines
|
| 1319 |
+
"""
|
| 1320 |
+
system_names = [s.strip() for s in systems.split(",")] if systems else None
|
| 1321 |
+
return discover_ai_services(None, system_names, scope, None)
|
| 1322 |
+
|
| 1323 |
+
def run_assess(gen_docs):
|
| 1324 |
+
"""
|
| 1325 |
+
Assess EU AI Act compliance and generate documentation templates.
|
| 1326 |
+
|
| 1327 |
+
Performs comprehensive compliance gap analysis against EU AI Act requirements
|
| 1328 |
+
(Articles 9-15, 16-22, 43-50) and generates professional documentation
|
| 1329 |
+
templates for Risk Management, Technical Documentation, and Conformity Assessment.
|
| 1330 |
+
|
| 1331 |
+
Parameters:
|
| 1332 |
+
gen_docs (bool): Whether to generate documentation templates (Article 9, 11, 43)
|
| 1333 |
+
|
| 1334 |
+
Returns:
|
| 1335 |
+
dict: Compliance score (0-100), risk level, gaps, recommendations, and documentation
|
| 1336 |
+
"""
|
| 1337 |
+
return assess_compliance(None, None, None, gen_docs)
|
| 1338 |
+
|
| 1339 |
+
discover_btn.click(run_discover_org, [org_name, org_domain, org_context], org_result)
|
| 1340 |
+
discover_ai_btn.click(run_discover_ai, [ai_systems_input, ai_scope], ai_result)
|
| 1341 |
+
assess_btn.click(run_assess, [gen_docs], compliance_result)
|
| 1342 |
+
|
| 1343 |
+
|
| 1344 |
+
# ============================================================================
|
| 1345 |
+
# LAUNCH
|
| 1346 |
+
# ============================================================================
|
| 1347 |
+
|
| 1348 |
+
# File to store the MCP URL for the main Gradio app to read
|
| 1349 |
+
MCP_URL_FILE = Path(__file__).parent / ".mcp_url"
|
| 1350 |
+
|
| 1351 |
+
def save_mcp_url(url: str):
|
| 1352 |
+
"""Save the MCP URL to a file for the main Gradio app to read"""
|
| 1353 |
+
try:
|
| 1354 |
+
# Ensure parent directory exists
|
| 1355 |
+
MCP_URL_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 1356 |
+
MCP_URL_FILE.write_text(url)
|
| 1357 |
+
print(f"\n✅ MCP URL saved to: {MCP_URL_FILE}")
|
| 1358 |
+
print(f" URL content: {url}")
|
| 1359 |
+
except Exception as e:
|
| 1360 |
+
print(f"⚠️ Could not save MCP URL: {e}")
|
| 1361 |
+
import traceback
|
| 1362 |
+
traceback.print_exc()
|
| 1363 |
+
|
| 1364 |
+
if __name__ == "__main__":
|
| 1365 |
+
is_production = bool(PUBLIC_URL)
|
| 1366 |
+
# ChatGPT MCP app runs on 7861 by default (separate from main Gradio UI on 7860)
|
| 1367 |
+
server_port = int(os.getenv("CHATGPT_APP_SERVER_PORT", "7861"))
|
| 1368 |
+
use_share = os.getenv("GRADIO_SHARE", "false").lower() == "true"
|
| 1369 |
+
|
| 1370 |
+
print("\n" + "=" * 70)
|
| 1371 |
+
print("🇪🇺 EU AI Act Compliance - ChatGPT App (MCP Server)")
|
| 1372 |
+
print("=" * 70)
|
| 1373 |
+
|
| 1374 |
+
if is_production:
|
| 1375 |
+
# Production on HF Spaces - MCP URL is based on PUBLIC_URL
|
| 1376 |
+
mcp_url = f"{PUBLIC_URL.rstrip('/')}/gradio_api/mcp/"
|
| 1377 |
+
print(f"\n🌐 Environment: PRODUCTION (HF Spaces)")
|
| 1378 |
+
print(f"\n" + "=" * 70)
|
| 1379 |
+
print("🎉 MCP SERVER READY!")
|
| 1380 |
+
print("=" * 70)
|
| 1381 |
+
print(f"\n🔗 MCP URL FOR CHATGPT (copy this):\n")
|
| 1382 |
+
print(f" {mcp_url}")
|
| 1383 |
+
print(f"\n📍 Space URL: {PUBLIC_URL}")
|
| 1384 |
+
print("=" * 70)
|
| 1385 |
+
else:
|
| 1386 |
+
print(f"\n🛠️ Environment: LOCAL DEVELOPMENT")
|
| 1387 |
+
if use_share:
|
| 1388 |
+
print(f" MCP URL will be shown after launch (share=True)")
|
| 1389 |
+
else:
|
| 1390 |
+
print(f" MCP URL: http://localhost:{server_port}/gradio_api/mcp/")
|
| 1391 |
+
|
| 1392 |
+
print(f"\n📡 API Server: {API_URL}")
|
| 1393 |
+
print(f"📍 Server Port: {server_port}")
|
| 1394 |
+
print("\n📖 ChatGPT Integration:")
|
| 1395 |
+
print(" 1. Copy the MCP URL shown above")
|
| 1396 |
+
print(" 2. Enable 'Developer Mode' in ChatGPT Settings → Apps & Connectors")
|
| 1397 |
+
print(" 3. Create a connector with the MCP URL (No authentication)")
|
| 1398 |
+
print(" 4. Use @eu-ai-act in ChatGPT to access tools")
|
| 1399 |
+
print("\n🚀 Starting Gradio MCP Server...")
|
| 1400 |
+
print("=" * 70 + "\n")
|
| 1401 |
+
|
| 1402 |
+
# Launch the MCP server on port 7860 (standalone) or 7861 (local dev with gradio_app)
|
| 1403 |
+
demo.launch(
|
| 1404 |
+
server_name=os.getenv("CHATGPT_APP_SERVER_NAME", "0.0.0.0"),
|
| 1405 |
+
server_port=server_port,
|
| 1406 |
+
share=use_share, # Only use share for local dev if needed
|
| 1407 |
+
mcp_server=True, # Enable MCP server for ChatGPT integration
|
| 1408 |
+
show_error=True,
|
| 1409 |
+
)
|
| 1410 |
+
|
apps/eu-ai-act-agent/src/gradio_app.py
ADDED
|
@@ -0,0 +1,1502 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
EU AI Act Compliance Agent - Gradio UI
|
| 4 |
+
Interactive web interface for EU AI Act compliance assessment
|
| 5 |
+
With MCP tool call visualization and multi-model support
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
from gradio import ChatMessage
|
| 10 |
+
import requests
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import threading
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import List, Generator, Optional
|
| 16 |
+
from dotenv import load_dotenv
|
| 17 |
+
|
| 18 |
+
# Load environment variables from root .env file
|
| 19 |
+
ROOT_DIR = Path(__file__).parent.parent.parent.parent # Go up from src -> eu-ai-act-agent -> apps -> root
|
| 20 |
+
load_dotenv(ROOT_DIR / ".env")
|
| 21 |
+
|
| 22 |
+
# API Configuration
|
| 23 |
+
API_URL = os.getenv("API_URL", "http://localhost:3001")
|
| 24 |
+
PUBLIC_URL = os.getenv("PUBLIC_URL", "") # HF Spaces public URL (empty for local dev)
|
| 25 |
+
API_TIMEOUT = 600 # seconds - increased for long-running compliance assessments
|
| 26 |
+
|
| 27 |
+
def get_mcp_url() -> str:
|
| 28 |
+
"""Get the MCP server URL based on environment"""
|
| 29 |
+
if PUBLIC_URL:
|
| 30 |
+
# Production: MCP is on the same server via chatgpt_app.py
|
| 31 |
+
return f"{PUBLIC_URL.rstrip('/')}/gradio_api/mcp/"
|
| 32 |
+
return ""
|
| 33 |
+
|
| 34 |
+
# Model Configuration
|
| 35 |
+
AVAILABLE_MODELS = {
|
| 36 |
+
"gpt-oss": {
|
| 37 |
+
"name": "🆓 GPT-OSS 20B (Modal - FREE)",
|
| 38 |
+
"api_key_env": "MODAL_ENDPOINT_URL",
|
| 39 |
+
"description": "Free OpenAI GPT-OSS 20B model hosted on Modal.com - No API key required! ⚠️ May take up to 60s to start responding (cold start). For faster responses and better precision, use another model with your API key."
|
| 40 |
+
},
|
| 41 |
+
"claude-4.5": {
|
| 42 |
+
"name": "Claude 4.5 Sonnet (Anthropic)",
|
| 43 |
+
"api_key_env": "ANTHROPIC_API_KEY",
|
| 44 |
+
"description": "Anthropic's latest Claude Sonnet model"
|
| 45 |
+
},
|
| 46 |
+
"claude-opus": {
|
| 47 |
+
"name": "Claude Opus 4 (Anthropic)",
|
| 48 |
+
"api_key_env": "ANTHROPIC_API_KEY",
|
| 49 |
+
"description": "Anthropic's most powerful Claude model"
|
| 50 |
+
},
|
| 51 |
+
"gemini-3": {
|
| 52 |
+
"name": "Gemini 3 Pro (Google)",
|
| 53 |
+
"api_key_env": "GOOGLE_GENERATIVE_AI_API_KEY",
|
| 54 |
+
"description": "Google's advanced reasoning model with thinking"
|
| 55 |
+
},
|
| 56 |
+
"gpt-5": {
|
| 57 |
+
"name": "GPT-5 (OpenAI)",
|
| 58 |
+
"api_key_env": "OPENAI_API_KEY",
|
| 59 |
+
"description": "OpenAI's most advanced model"
|
| 60 |
+
},
|
| 61 |
+
"grok-4-1": {
|
| 62 |
+
"name": "Grok 4.1 (xAI)",
|
| 63 |
+
"api_key_env": "XAI_API_KEY",
|
| 64 |
+
"description": "xAI's fast reasoning model"
|
| 65 |
+
},
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
# Current model settings (can be updated via UI)
|
| 69 |
+
# SECURITY: Store user-provided keys for this session only
|
| 70 |
+
# NOTE: API keys are REQUIRED for paid models - GPT-OSS is FREE!
|
| 71 |
+
# IMPORTANT: Always default to gpt-oss (FREE model) regardless of env var
|
| 72 |
+
# The env var might be set for the API server, but the UI should default to FREE model
|
| 73 |
+
current_model_settings = {
|
| 74 |
+
"model": "gpt-oss", # Always default to FREE GPT-OSS model in UI!
|
| 75 |
+
# User-provided keys (REQUIRED for paid models, optional for GPT-OSS)
|
| 76 |
+
"openai_api_key": "",
|
| 77 |
+
"xai_api_key": "",
|
| 78 |
+
"anthropic_api_key": "",
|
| 79 |
+
"google_api_key": "", # Google Generative AI API key
|
| 80 |
+
"tavily_api_key": "", # Required for web research & organization discovery
|
| 81 |
+
"modal_endpoint_url": "https://vasilis--gpt-oss-vllm-inference-serve.modal.run" # Hardcoded Modal.com endpoint for GPT-OSS (no trailing slash!)
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
# Thread-safe cancellation flag for stopping ongoing requests
|
| 85 |
+
class CancellationToken:
|
| 86 |
+
def __init__(self):
|
| 87 |
+
self._cancelled = False
|
| 88 |
+
self._lock = threading.Lock()
|
| 89 |
+
self._response = None
|
| 90 |
+
|
| 91 |
+
def cancel(self):
|
| 92 |
+
with self._lock:
|
| 93 |
+
self._cancelled = True
|
| 94 |
+
# Close any active response to stop streaming
|
| 95 |
+
if self._response is not None:
|
| 96 |
+
try:
|
| 97 |
+
self._response.close()
|
| 98 |
+
except:
|
| 99 |
+
pass
|
| 100 |
+
|
| 101 |
+
def is_cancelled(self):
|
| 102 |
+
with self._lock:
|
| 103 |
+
return self._cancelled
|
| 104 |
+
|
| 105 |
+
def set_response(self, response):
|
| 106 |
+
with self._lock:
|
| 107 |
+
self._response = response
|
| 108 |
+
|
| 109 |
+
def reset(self):
|
| 110 |
+
with self._lock:
|
| 111 |
+
self._cancelled = False
|
| 112 |
+
self._response = None
|
| 113 |
+
|
| 114 |
+
# Global cancellation token
|
| 115 |
+
cancel_token = CancellationToken()
|
| 116 |
+
|
| 117 |
+
def format_tool_call(tool_name: str, args: dict) -> str:
|
| 118 |
+
"""Format a tool call for display"""
|
| 119 |
+
args_str = json.dumps(args, indent=2) if args else "{}"
|
| 120 |
+
return f"""
|
| 121 |
+
🔧 **MCP Tool Call: `{tool_name}`**
|
| 122 |
+
|
| 123 |
+
**Arguments:**
|
| 124 |
+
```json
|
| 125 |
+
{args_str}
|
| 126 |
+
```
|
| 127 |
+
"""
|
| 128 |
+
|
| 129 |
+
def format_thinking_section(thinking_text: str, tool_name: str = None) -> str:
|
| 130 |
+
"""Format AI thinking/reasoning in a collapsible section"""
|
| 131 |
+
if not thinking_text or not thinking_text.strip():
|
| 132 |
+
return ""
|
| 133 |
+
|
| 134 |
+
# Clean up the thinking text
|
| 135 |
+
thinking_clean = thinking_text.strip()
|
| 136 |
+
|
| 137 |
+
# Create a descriptive title based on context
|
| 138 |
+
if tool_name:
|
| 139 |
+
title = f"🧠 AI Reasoning (before {tool_name.replace('_', ' ').title()})"
|
| 140 |
+
else:
|
| 141 |
+
title = "🧠 AI Reasoning"
|
| 142 |
+
|
| 143 |
+
return f"""
|
| 144 |
+
<details>
|
| 145 |
+
<summary>{title}</summary>
|
| 146 |
+
|
| 147 |
+
*The model's thought process:*
|
| 148 |
+
|
| 149 |
+
{thinking_clean}
|
| 150 |
+
|
| 151 |
+
</details>
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
def format_tool_result(tool_name: str, result) -> str:
|
| 155 |
+
"""Format a tool result for display"""
|
| 156 |
+
|
| 157 |
+
# Special handling for assess_compliance - show generated documentation with full content
|
| 158 |
+
if tool_name == "assess_compliance" and result:
|
| 159 |
+
output = f"\n✅ **Tool Result: `{tool_name}`**\n\n"
|
| 160 |
+
|
| 161 |
+
# Extract key information
|
| 162 |
+
assessment = result.get("assessment", {})
|
| 163 |
+
metadata = result.get("metadata", {})
|
| 164 |
+
documentation = result.get("documentation", {})
|
| 165 |
+
reasoning = result.get("reasoning", "")
|
| 166 |
+
doc_files = metadata.get("documentationFiles", [])
|
| 167 |
+
|
| 168 |
+
# Show assessment summary
|
| 169 |
+
if assessment:
|
| 170 |
+
score = assessment.get("overallScore", "N/A")
|
| 171 |
+
risk_level = assessment.get("riskLevel", "N/A")
|
| 172 |
+
gaps = assessment.get("gaps", [])
|
| 173 |
+
recommendations = assessment.get("recommendations", [])
|
| 174 |
+
gaps_count = len(gaps)
|
| 175 |
+
recs_count = len(recommendations)
|
| 176 |
+
|
| 177 |
+
# Risk level emoji
|
| 178 |
+
risk_emoji = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢"}.get(risk_level, "⚪")
|
| 179 |
+
|
| 180 |
+
output += f"""### 📊 Compliance Assessment Summary
|
| 181 |
+
|
| 182 |
+
| Metric | Value |
|
| 183 |
+
|--------|-------|
|
| 184 |
+
| **Overall Score** | **{score}/100** |
|
| 185 |
+
| **Risk Level** | {risk_emoji} **{risk_level}** |
|
| 186 |
+
| **Gaps Identified** | {gaps_count} |
|
| 187 |
+
| **Recommendations** | {recs_count} |
|
| 188 |
+
|
| 189 |
+
"""
|
| 190 |
+
|
| 191 |
+
# Show AI reasoning in collapsible section
|
| 192 |
+
if reasoning:
|
| 193 |
+
output += f"""
|
| 194 |
+
<details>
|
| 195 |
+
<summary>🧠 AI Reasoning & Analysis</summary>
|
| 196 |
+
|
| 197 |
+
{reasoning}
|
| 198 |
+
|
| 199 |
+
</details>
|
| 200 |
+
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
# Show gaps summary in collapsible section
|
| 204 |
+
if gaps:
|
| 205 |
+
critical_gaps = [g for g in gaps if g.get("severity") == "CRITICAL"]
|
| 206 |
+
high_gaps = [g for g in gaps if g.get("severity") == "HIGH"]
|
| 207 |
+
|
| 208 |
+
output += f"""
|
| 209 |
+
<details>
|
| 210 |
+
<summary>⚠️ Compliance Gaps ({gaps_count} total: {len(critical_gaps)} Critical, {len(high_gaps)} High)</summary>
|
| 211 |
+
|
| 212 |
+
"""
|
| 213 |
+
# Group by severity
|
| 214 |
+
for severity in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
|
| 215 |
+
severity_gaps = [g for g in gaps if g.get("severity") == severity]
|
| 216 |
+
if severity_gaps:
|
| 217 |
+
severity_emoji = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢"}.get(severity, "⚪")
|
| 218 |
+
output += f"\n**{severity_emoji} {severity} Priority Gaps:**\n\n"
|
| 219 |
+
for gap in severity_gaps:
|
| 220 |
+
output += f"- **{gap.get('category', 'Unknown')}**: {gap.get('description', 'No description')}\n"
|
| 221 |
+
output += f" - *Article:* {gap.get('articleReference', 'N/A')} | *Effort:* {gap.get('remediationEffort', 'N/A')}\n"
|
| 222 |
+
|
| 223 |
+
output += "\n</details>\n\n"
|
| 224 |
+
|
| 225 |
+
# Show top recommendations in collapsible section
|
| 226 |
+
if recommendations:
|
| 227 |
+
# Sort by priority
|
| 228 |
+
sorted_recs = sorted(recommendations, key=lambda r: r.get("priority", 10))
|
| 229 |
+
top_recs = sorted_recs[:5]
|
| 230 |
+
|
| 231 |
+
output += f"""
|
| 232 |
+
<details>
|
| 233 |
+
<summary>💡 Priority Recommendations (Top {len(top_recs)} of {recs_count})</summary>
|
| 234 |
+
|
| 235 |
+
"""
|
| 236 |
+
for i, rec in enumerate(top_recs, 1):
|
| 237 |
+
output += f"\n**{i}. {rec.get('title', 'Recommendation')}** (Priority: {rec.get('priority', 'N/A')}/10)\n\n"
|
| 238 |
+
output += f"{rec.get('description', 'No description')}\n\n"
|
| 239 |
+
output += f"- *Article:* {rec.get('articleReference', 'N/A')}\n"
|
| 240 |
+
output += f"- *Estimated Effort:* {rec.get('estimatedEffort', 'N/A')}\n"
|
| 241 |
+
|
| 242 |
+
steps = rec.get("implementationSteps", [])
|
| 243 |
+
if steps:
|
| 244 |
+
output += f"- *Implementation Steps:*\n"
|
| 245 |
+
for step in steps[:3]: # Show first 3 steps
|
| 246 |
+
output += f" 1. {step}\n"
|
| 247 |
+
if len(steps) > 3:
|
| 248 |
+
output += f" *(+ {len(steps) - 3} more steps)*\n"
|
| 249 |
+
output += "\n"
|
| 250 |
+
|
| 251 |
+
output += "</details>\n\n"
|
| 252 |
+
|
| 253 |
+
# Show generated documentation content in collapsible sections
|
| 254 |
+
output += "---\n\n### 📄 Generated EU AI Act Documentation\n\n"
|
| 255 |
+
|
| 256 |
+
# Map of documentation keys to display info
|
| 257 |
+
doc_display_map = {
|
| 258 |
+
"riskManagementTemplate": {
|
| 259 |
+
"title": "Risk Management System",
|
| 260 |
+
"article": "Article 9",
|
| 261 |
+
"emoji": "⚡",
|
| 262 |
+
"description": "Continuous risk identification, analysis, estimation and mitigation process"
|
| 263 |
+
},
|
| 264 |
+
"technicalDocumentation": {
|
| 265 |
+
"title": "Technical Documentation",
|
| 266 |
+
"article": "Article 11 / Annex IV",
|
| 267 |
+
"emoji": "📋",
|
| 268 |
+
"description": "Comprehensive technical documentation for high-risk AI systems"
|
| 269 |
+
},
|
| 270 |
+
"conformityAssessment": {
|
| 271 |
+
"title": "Conformity Assessment",
|
| 272 |
+
"article": "Article 43",
|
| 273 |
+
"emoji": "✅",
|
| 274 |
+
"description": "Procedures for conformity assessment of high-risk AI systems"
|
| 275 |
+
},
|
| 276 |
+
"transparencyNotice": {
|
| 277 |
+
"title": "Transparency Notice",
|
| 278 |
+
"article": "Article 50",
|
| 279 |
+
"emoji": "👁️",
|
| 280 |
+
"description": "Transparency obligations for AI system interactions"
|
| 281 |
+
},
|
| 282 |
+
"qualityManagementSystem": {
|
| 283 |
+
"title": "Quality Management System",
|
| 284 |
+
"article": "Article 17",
|
| 285 |
+
"emoji": "🏆",
|
| 286 |
+
"description": "Quality management system for AI system providers"
|
| 287 |
+
},
|
| 288 |
+
"humanOversightProcedure": {
|
| 289 |
+
"title": "Human Oversight Procedure",
|
| 290 |
+
"article": "Article 14",
|
| 291 |
+
"emoji": "👤",
|
| 292 |
+
"description": "Human oversight measures for high-risk AI systems"
|
| 293 |
+
},
|
| 294 |
+
"dataGovernancePolicy": {
|
| 295 |
+
"title": "Data Governance Policy",
|
| 296 |
+
"article": "Article 10",
|
| 297 |
+
"emoji": "🗃️",
|
| 298 |
+
"description": "Data and data governance practices for training, validation and testing"
|
| 299 |
+
},
|
| 300 |
+
"incidentReportingProcedure": {
|
| 301 |
+
"title": "Incident Reporting Procedure",
|
| 302 |
+
"article": "Article 62",
|
| 303 |
+
"emoji": "🚨",
|
| 304 |
+
"description": "Reporting of serious incidents and malfunctioning"
|
| 305 |
+
},
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
# Display each documentation template in its own collapsible section
|
| 309 |
+
docs_found = 0
|
| 310 |
+
for doc_key, doc_info in doc_display_map.items():
|
| 311 |
+
doc_content = documentation.get(doc_key)
|
| 312 |
+
if doc_content:
|
| 313 |
+
docs_found += 1
|
| 314 |
+
output += f"""
|
| 315 |
+
<details>
|
| 316 |
+
<summary>{doc_info['emoji']} **{doc_info['title']}** — {doc_info['article']}</summary>
|
| 317 |
+
|
| 318 |
+
*{doc_info['description']}*
|
| 319 |
+
|
| 320 |
+
---
|
| 321 |
+
|
| 322 |
+
{doc_content}
|
| 323 |
+
|
| 324 |
+
</details>
|
| 325 |
+
|
| 326 |
+
"""
|
| 327 |
+
|
| 328 |
+
if docs_found == 0:
|
| 329 |
+
output += "*No documentation templates were generated in this assessment.*\n\n"
|
| 330 |
+
else:
|
| 331 |
+
output += f"\n> ✨ **{docs_found} documentation template(s) generated.** Expand each section above to view the full content.\n\n"
|
| 332 |
+
|
| 333 |
+
# Note about limited templates for speed/cost optimization
|
| 334 |
+
if docs_found < 8:
|
| 335 |
+
output += "> ℹ️ **Note:** Currently generating **2 core templates** (Risk Management & Technical Documentation) for faster responses and API cost optimization. Additional templates (Conformity Assessment, Transparency Notice, etc.) are planned for future releases.\n\n"
|
| 336 |
+
|
| 337 |
+
# Show file paths if documents were saved to disk
|
| 338 |
+
if doc_files:
|
| 339 |
+
output += "---\n\n### 💾 Saved Documentation Files\n\n"
|
| 340 |
+
output += "The documentation has also been saved to disk:\n\n"
|
| 341 |
+
|
| 342 |
+
# Map filenames to EU AI Act articles for context
|
| 343 |
+
article_map = {
|
| 344 |
+
"Risk_Management_System": "Article 9",
|
| 345 |
+
"Technical_Documentation": "Article 11 / Annex IV",
|
| 346 |
+
"Conformity_Assessment": "Article 43",
|
| 347 |
+
"Transparency_Notice": "Article 50",
|
| 348 |
+
"Quality_Management_System": "Article 17",
|
| 349 |
+
"Human_Oversight_Procedure": "Article 14",
|
| 350 |
+
"Data_Governance_Policy": "Article 10",
|
| 351 |
+
"Incident_Reporting_Procedure": "Article 62",
|
| 352 |
+
"Compliance_Assessment_Report": "Full Assessment",
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
output += "| Document | EU AI Act Reference | File Path |\n"
|
| 356 |
+
output += "|----------|--------------------|-----------|\n"
|
| 357 |
+
|
| 358 |
+
for file_path in doc_files:
|
| 359 |
+
# Extract filename from path
|
| 360 |
+
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
| 361 |
+
# Remove .md extension for display name
|
| 362 |
+
display_name = filename.replace(".md", "").replace("_", " ")
|
| 363 |
+
# Remove leading numbers like "01_" or "00_"
|
| 364 |
+
if len(display_name) > 3 and display_name[:2].isdigit() and display_name[2] == " ":
|
| 365 |
+
display_name = display_name[3:]
|
| 366 |
+
|
| 367 |
+
# Find article reference
|
| 368 |
+
article_ref = "—"
|
| 369 |
+
for key, article in article_map.items():
|
| 370 |
+
if key.lower().replace("_", " ") in display_name.lower():
|
| 371 |
+
article_ref = article
|
| 372 |
+
break
|
| 373 |
+
|
| 374 |
+
output += f"| 📄 {display_name} | {article_ref} | `{filename}` |\n"
|
| 375 |
+
|
| 376 |
+
# Show the directory where files are saved
|
| 377 |
+
if doc_files:
|
| 378 |
+
docs_dir = "/".join(doc_files[0].split("/")[:-1])
|
| 379 |
+
output += f"\n**📂 Documents Directory:** `{docs_dir}`\n\n"
|
| 380 |
+
|
| 381 |
+
# Collapsible raw JSON for reference (at the very end)
|
| 382 |
+
result_str = json.dumps(result, indent=2) if result else "null"
|
| 383 |
+
if len(result_str) > 5000:
|
| 384 |
+
result_str = result_str[:5000] + "\n... (truncated)"
|
| 385 |
+
|
| 386 |
+
output += f"""
|
| 387 |
+
---
|
| 388 |
+
|
| 389 |
+
<details>
|
| 390 |
+
<summary>🔍 View Raw JSON Response</summary>
|
| 391 |
+
|
| 392 |
+
```json
|
| 393 |
+
{result_str}
|
| 394 |
+
```
|
| 395 |
+
|
| 396 |
+
</details>
|
| 397 |
+
"""
|
| 398 |
+
return output
|
| 399 |
+
|
| 400 |
+
# Default formatting for other tools
|
| 401 |
+
result_str = json.dumps(result, indent=2) if result else "null"
|
| 402 |
+
if len(result_str) > 1500:
|
| 403 |
+
result_str = result_str[:1500] + "\n... (truncated)"
|
| 404 |
+
|
| 405 |
+
return f"""
|
| 406 |
+
✅ **Tool Result: `{tool_name}`**
|
| 407 |
+
|
| 408 |
+
<details>
|
| 409 |
+
<summary>📋 Click to expand result</summary>
|
| 410 |
+
|
| 411 |
+
```json
|
| 412 |
+
{result_str}
|
| 413 |
+
```
|
| 414 |
+
|
| 415 |
+
</details>
|
| 416 |
+
"""
|
| 417 |
+
|
| 418 |
+
def format_thinking_indicator(tool_name: str = None) -> str:
|
| 419 |
+
"""Format a thinking/processing indicator"""
|
| 420 |
+
if tool_name:
|
| 421 |
+
# Show specific tool name if available
|
| 422 |
+
tool_display_name = {
|
| 423 |
+
"assess_compliance": "EU AI Act Compliance Assessment",
|
| 424 |
+
"discover_ai_services": "AI Systems Discovery",
|
| 425 |
+
"discover_organization": "Organization Discovery"
|
| 426 |
+
}.get(tool_name, tool_name.replace("_", " ").title())
|
| 427 |
+
|
| 428 |
+
return f"\n\n⏳ **Processing: {tool_display_name}...**\n\n*This may take a moment while the tool analyzes data and generates documentation.*\n"
|
| 429 |
+
return "\n\n⏳ **Processing with MCP tools...**\n\n*Please wait while the tools execute...*\n"
|
| 430 |
+
|
| 431 |
+
def get_api_headers() -> dict:
|
| 432 |
+
"""Get headers with model configuration for API requests
|
| 433 |
+
|
| 434 |
+
SECURITY: Only pass model selection and user-provided API keys.
|
| 435 |
+
API keys are REQUIRED for paid models - GPT-OSS is FREE!
|
| 436 |
+
User must provide their own keys via the Model Settings UI (except for GPT-OSS).
|
| 437 |
+
"""
|
| 438 |
+
headers = {"Content-Type": "application/json"}
|
| 439 |
+
|
| 440 |
+
# Pass model selection - always use the current model setting
|
| 441 |
+
selected_model = current_model_settings.get("model", "gpt-oss")
|
| 442 |
+
headers["X-AI-Model"] = selected_model
|
| 443 |
+
print(f"[Gradio] Sending model to API: {selected_model}")
|
| 444 |
+
|
| 445 |
+
# Pass user-provided API keys based on selected model
|
| 446 |
+
model = current_model_settings["model"]
|
| 447 |
+
if model == "gpt-oss":
|
| 448 |
+
# GPT-OSS uses hardcoded Modal endpoint URL (FREE - no API key required!)
|
| 449 |
+
headers["X-Modal-Endpoint-URL"] = current_model_settings["modal_endpoint_url"]
|
| 450 |
+
elif model == "gpt-5" and current_model_settings["openai_api_key"]:
|
| 451 |
+
headers["X-OpenAI-API-Key"] = current_model_settings["openai_api_key"]
|
| 452 |
+
elif model == "grok-4-1" and current_model_settings["xai_api_key"]:
|
| 453 |
+
headers["X-XAI-API-Key"] = current_model_settings["xai_api_key"]
|
| 454 |
+
elif model in ["claude-4.5", "claude-opus"] and current_model_settings["anthropic_api_key"]:
|
| 455 |
+
headers["X-Anthropic-API-Key"] = current_model_settings["anthropic_api_key"]
|
| 456 |
+
elif model == "gemini-3" and current_model_settings["google_api_key"]:
|
| 457 |
+
headers["X-Google-API-Key"] = current_model_settings["google_api_key"]
|
| 458 |
+
|
| 459 |
+
# Tavily API key for web research (optional - AI model used as fallback)
|
| 460 |
+
if current_model_settings["tavily_api_key"]:
|
| 461 |
+
headers["X-Tavily-API-Key"] = current_model_settings["tavily_api_key"]
|
| 462 |
+
|
| 463 |
+
return headers
|
| 464 |
+
|
| 465 |
+
def chat_with_agent_streaming(message: str, history: list, initialized_history: list = None) -> Generator:
|
| 466 |
+
"""
|
| 467 |
+
Send a message to the EU AI Act agent and stream the response with tool calls
|
| 468 |
+
|
| 469 |
+
Args:
|
| 470 |
+
message: User's input message
|
| 471 |
+
history: Original chat history for API (without current user message)
|
| 472 |
+
initialized_history: Pre-initialized history with user message and loading (optional)
|
| 473 |
+
|
| 474 |
+
Yields:
|
| 475 |
+
Updated history with streaming content
|
| 476 |
+
"""
|
| 477 |
+
global cancel_token
|
| 478 |
+
|
| 479 |
+
if not message.strip():
|
| 480 |
+
yield initialized_history or history
|
| 481 |
+
return
|
| 482 |
+
|
| 483 |
+
# Reset cancellation token for new request
|
| 484 |
+
cancel_token.reset()
|
| 485 |
+
|
| 486 |
+
# Use pre-initialized history or create one
|
| 487 |
+
if initialized_history:
|
| 488 |
+
new_history = list(initialized_history)
|
| 489 |
+
else:
|
| 490 |
+
new_history = list(history) + [
|
| 491 |
+
ChatMessage(role="user", content=message),
|
| 492 |
+
ChatMessage(role="assistant", content="⏳ *Thinking...*")
|
| 493 |
+
]
|
| 494 |
+
|
| 495 |
+
response = None
|
| 496 |
+
bot_response = ""
|
| 497 |
+
tool_calls_content = "" # All tool calls, results, and thinking sections (in order)
|
| 498 |
+
current_thinking = "" # Accumulate thinking text before tool calls
|
| 499 |
+
|
| 500 |
+
try:
|
| 501 |
+
# Convert original history to API format (handle both ChatMessage and dict)
|
| 502 |
+
api_history = []
|
| 503 |
+
for msg in history:
|
| 504 |
+
if isinstance(msg, dict):
|
| 505 |
+
api_history.append({"role": msg.get("role", "user"), "content": msg.get("content", "")})
|
| 506 |
+
else:
|
| 507 |
+
api_history.append({"role": msg.role, "content": msg.content})
|
| 508 |
+
|
| 509 |
+
# Make streaming request to API with model configuration headers
|
| 510 |
+
response = requests.post(
|
| 511 |
+
f"{API_URL}/api/chat",
|
| 512 |
+
json={"message": message, "history": api_history},
|
| 513 |
+
headers=get_api_headers(),
|
| 514 |
+
stream=True,
|
| 515 |
+
timeout=API_TIMEOUT,
|
| 516 |
+
)
|
| 517 |
+
|
| 518 |
+
# Register response for potential cancellation
|
| 519 |
+
cancel_token.set_response(response)
|
| 520 |
+
|
| 521 |
+
if response.status_code != 200:
|
| 522 |
+
error_msg = f"⚠️ Error: API returned status {response.status_code}"
|
| 523 |
+
new_history[-1] = ChatMessage(role="assistant", content=error_msg)
|
| 524 |
+
yield new_history
|
| 525 |
+
return
|
| 526 |
+
|
| 527 |
+
# Initialize assistant response
|
| 528 |
+
current_tool_call = None
|
| 529 |
+
|
| 530 |
+
for line in response.iter_lines():
|
| 531 |
+
# Check for cancellation
|
| 532 |
+
if cancel_token.is_cancelled():
|
| 533 |
+
# Include any accumulated thinking before cancellation
|
| 534 |
+
if current_thinking.strip():
|
| 535 |
+
tool_calls_content += format_thinking_section(current_thinking)
|
| 536 |
+
|
| 537 |
+
final_content = tool_calls_content + bot_response
|
| 538 |
+
if final_content:
|
| 539 |
+
final_content += "\n\n*— Execution stopped by user*"
|
| 540 |
+
else:
|
| 541 |
+
final_content = "*— Execution stopped by user*"
|
| 542 |
+
new_history[-1] = ChatMessage(role="assistant", content=final_content)
|
| 543 |
+
yield new_history
|
| 544 |
+
return
|
| 545 |
+
|
| 546 |
+
if line:
|
| 547 |
+
line_str = line.decode('utf-8')
|
| 548 |
+
print(f"[DEBUG] Received: {line_str[:100]}...") # Debug log
|
| 549 |
+
if line_str.startswith('data: '):
|
| 550 |
+
try:
|
| 551 |
+
data = json.loads(line_str[6:]) # Remove 'data: ' prefix
|
| 552 |
+
event_type = data.get("type")
|
| 553 |
+
print(f"[DEBUG] Event type: {event_type}, data: {str(data)[:100]}")
|
| 554 |
+
|
| 555 |
+
if event_type == "thinking":
|
| 556 |
+
# Handle thinking/reasoning tokens from Claude or GPT
|
| 557 |
+
# Show thinking at the END (bottom) where action is happening
|
| 558 |
+
thinking_content = data.get("content", "")
|
| 559 |
+
if thinking_content:
|
| 560 |
+
current_thinking += thinking_content
|
| 561 |
+
|
| 562 |
+
# Show thinking tokens in real-time AT THE BOTTOM
|
| 563 |
+
# Tool calls first, then current thinking at the end
|
| 564 |
+
live_thinking = f"\n\n🧠 **Model Thinking (live):**\n\n```\n{current_thinking}\n```"
|
| 565 |
+
full_content = tool_calls_content + live_thinking
|
| 566 |
+
|
| 567 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 568 |
+
yield new_history
|
| 569 |
+
|
| 570 |
+
elif event_type == "text":
|
| 571 |
+
# Append text chunk
|
| 572 |
+
text_content = data.get("content", "")
|
| 573 |
+
text_phase = data.get("phase", "thinking") # Server tells us the phase
|
| 574 |
+
has_had_tools = data.get("hasHadToolCalls", False)
|
| 575 |
+
|
| 576 |
+
# Determine if this is "thinking" text or final response based on server phase
|
| 577 |
+
if text_phase == "thinking" or (not has_had_tools and not tool_calls_content):
|
| 578 |
+
# This is thinking text (before tool calls or between them)
|
| 579 |
+
current_thinking += text_content
|
| 580 |
+
|
| 581 |
+
# Show thinking AT THE BOTTOM after tool calls
|
| 582 |
+
if not tool_calls_content:
|
| 583 |
+
# Initial thinking - show with brain indicator
|
| 584 |
+
display_content = f"🧠 **AI is reasoning...**\n\n{current_thinking}"
|
| 585 |
+
else:
|
| 586 |
+
# Thinking between tool calls - show at the end
|
| 587 |
+
display_content = tool_calls_content + f"\n\n🧠 *Reasoning:* {current_thinking}"
|
| 588 |
+
|
| 589 |
+
new_history[-1] = ChatMessage(role="assistant", content=display_content)
|
| 590 |
+
else:
|
| 591 |
+
# This is "potential_response" - text after tool results
|
| 592 |
+
# Could be final response OR thinking before another tool call
|
| 593 |
+
# We accumulate it and will format appropriately when we know more
|
| 594 |
+
current_thinking += text_content
|
| 595 |
+
|
| 596 |
+
# Show as streaming response AT THE BOTTOM
|
| 597 |
+
full_content = tool_calls_content + f"\n\n{current_thinking}"
|
| 598 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 599 |
+
|
| 600 |
+
yield new_history
|
| 601 |
+
|
| 602 |
+
elif event_type == "tool_call":
|
| 603 |
+
# Before showing tool call, save any accumulated thinking as collapsible
|
| 604 |
+
tool_name = data.get("toolName", "unknown")
|
| 605 |
+
args = data.get("args", {})
|
| 606 |
+
|
| 607 |
+
# If we have accumulated thinking text, add it as collapsible BEFORE this tool call
|
| 608 |
+
if current_thinking.strip():
|
| 609 |
+
tool_calls_content += format_thinking_section(current_thinking, tool_name)
|
| 610 |
+
current_thinking = "" # Reset for next thinking block
|
| 611 |
+
else:
|
| 612 |
+
# No thinking text was output - add a synthetic thinking note
|
| 613 |
+
tool_display = tool_name.replace('_', ' ').title()
|
| 614 |
+
synthetic_thinking = f"I'll use the **{tool_display}** tool to gather the necessary information."
|
| 615 |
+
tool_calls_content += format_thinking_section(synthetic_thinking, tool_name)
|
| 616 |
+
|
| 617 |
+
# Show tool call with prominent loading indicator AT THE BOTTOM
|
| 618 |
+
tool_calls_content += format_tool_call(tool_name, args)
|
| 619 |
+
# Add prominent loading indicator specific to this tool
|
| 620 |
+
loading_indicator = format_thinking_indicator(tool_name)
|
| 621 |
+
full_content = tool_calls_content + bot_response + loading_indicator
|
| 622 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 623 |
+
yield new_history
|
| 624 |
+
current_tool_call = tool_name
|
| 625 |
+
|
| 626 |
+
elif event_type == "tool_result":
|
| 627 |
+
# Show tool result (removes loading indicator)
|
| 628 |
+
tool_name = data.get("toolName", current_tool_call or "unknown")
|
| 629 |
+
result = data.get("result")
|
| 630 |
+
tool_calls_content += format_tool_result(tool_name, result)
|
| 631 |
+
|
| 632 |
+
# After tool result, show "analyzing results" indicator AT THE BOTTOM
|
| 633 |
+
analyzing_indicator = f"\n\n🧠 **Analyzing {tool_name.replace('_', ' ')} results...**\n"
|
| 634 |
+
full_content = tool_calls_content + analyzing_indicator
|
| 635 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 636 |
+
yield new_history
|
| 637 |
+
current_tool_call = None
|
| 638 |
+
|
| 639 |
+
elif event_type == "step_finish":
|
| 640 |
+
# Step completed - if there's accumulated thinking, add it to tool_calls_content
|
| 641 |
+
has_had_tools_in_step = data.get("hasHadToolCalls", False)
|
| 642 |
+
|
| 643 |
+
if current_thinking.strip():
|
| 644 |
+
tool_calls_content += format_thinking_section(current_thinking)
|
| 645 |
+
current_thinking = ""
|
| 646 |
+
|
| 647 |
+
# Show "preparing response" if we had tool calls and step is finishing AT THE BOTTOM
|
| 648 |
+
if has_had_tools_in_step and tool_calls_content:
|
| 649 |
+
preparing_indicator = "\n\n✨ **Preparing comprehensive response based on analysis...**\n"
|
| 650 |
+
full_content = tool_calls_content + preparing_indicator
|
| 651 |
+
else:
|
| 652 |
+
full_content = tool_calls_content + bot_response
|
| 653 |
+
|
| 654 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 655 |
+
yield new_history
|
| 656 |
+
|
| 657 |
+
elif event_type == "error":
|
| 658 |
+
error_msg = data.get("error", "Unknown error")
|
| 659 |
+
bot_response += f"\n\n⚠️ Error: {error_msg}"
|
| 660 |
+
full_content = tool_calls_content + bot_response
|
| 661 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 662 |
+
yield new_history
|
| 663 |
+
|
| 664 |
+
elif event_type == "done":
|
| 665 |
+
# Final update
|
| 666 |
+
# If we have tool calls, any remaining current_thinking is the final response
|
| 667 |
+
# If no tool calls, current_thinking was just direct response (no tools needed)
|
| 668 |
+
if tool_calls_content:
|
| 669 |
+
# We had tool calls - current_thinking after last tool is the final response
|
| 670 |
+
bot_response = current_thinking
|
| 671 |
+
current_thinking = ""
|
| 672 |
+
else:
|
| 673 |
+
# No tool calls - current_thinking is the direct response
|
| 674 |
+
bot_response = current_thinking
|
| 675 |
+
current_thinking = ""
|
| 676 |
+
|
| 677 |
+
# Final response AT THE BOTTOM after all tool calls
|
| 678 |
+
full_content = tool_calls_content + bot_response
|
| 679 |
+
new_history[-1] = ChatMessage(role="assistant", content=full_content)
|
| 680 |
+
yield new_history
|
| 681 |
+
break
|
| 682 |
+
|
| 683 |
+
except json.JSONDecodeError:
|
| 684 |
+
continue
|
| 685 |
+
|
| 686 |
+
# Ensure final state (only if not cancelled)
|
| 687 |
+
if not cancel_token.is_cancelled():
|
| 688 |
+
# If we have accumulated text that wasn't finalized, treat it as the response
|
| 689 |
+
if current_thinking.strip() and not bot_response:
|
| 690 |
+
bot_response = current_thinking
|
| 691 |
+
|
| 692 |
+
# Final content: tool calls first, then response at the bottom
|
| 693 |
+
final_content = tool_calls_content + (bot_response or "No response generated.")
|
| 694 |
+
new_history[-1] = ChatMessage(role="assistant", content=final_content)
|
| 695 |
+
yield new_history
|
| 696 |
+
|
| 697 |
+
except requests.exceptions.ConnectionError:
|
| 698 |
+
if not cancel_token.is_cancelled():
|
| 699 |
+
error_msg = "⚠️ Cannot connect to API server. Make sure it's running on http://localhost:3001"
|
| 700 |
+
new_history[-1] = ChatMessage(role="assistant", content=error_msg)
|
| 701 |
+
yield new_history
|
| 702 |
+
except requests.exceptions.Timeout:
|
| 703 |
+
if not cancel_token.is_cancelled():
|
| 704 |
+
error_msg = "⚠️ Request timed out. The agent might be processing a complex query."
|
| 705 |
+
final_content = tool_calls_content + bot_response + "\n\n" + error_msg
|
| 706 |
+
new_history[-1] = ChatMessage(role="assistant", content=final_content)
|
| 707 |
+
yield new_history
|
| 708 |
+
except (requests.exceptions.ChunkedEncodingError, ConnectionError):
|
| 709 |
+
# This can happen when we close the connection during cancellation - it's expected
|
| 710 |
+
if not cancel_token.is_cancelled():
|
| 711 |
+
error_msg = "⚠️ Connection was interrupted."
|
| 712 |
+
final_content = tool_calls_content + bot_response + "\n\n" + error_msg
|
| 713 |
+
new_history[-1] = ChatMessage(role="assistant", content=final_content)
|
| 714 |
+
yield new_history
|
| 715 |
+
except Exception as e:
|
| 716 |
+
if not cancel_token.is_cancelled():
|
| 717 |
+
error_msg = f"⚠️ Error: {str(e)}"
|
| 718 |
+
final_content = tool_calls_content + bot_response + "\n\n" + error_msg if (tool_calls_content or bot_response) else error_msg
|
| 719 |
+
new_history[-1] = ChatMessage(role="assistant", content=final_content)
|
| 720 |
+
yield new_history
|
| 721 |
+
finally:
|
| 722 |
+
# Clean up the response connection
|
| 723 |
+
if response is not None:
|
| 724 |
+
try:
|
| 725 |
+
response.close()
|
| 726 |
+
except:
|
| 727 |
+
pass
|
| 728 |
+
cancel_token.set_response(None)
|
| 729 |
+
|
| 730 |
+
def check_api_status() -> str:
|
| 731 |
+
"""Check if the API server is running"""
|
| 732 |
+
try:
|
| 733 |
+
response = requests.get(f"{API_URL}/health", timeout=5)
|
| 734 |
+
if response.status_code == 200:
|
| 735 |
+
data = response.json()
|
| 736 |
+
return f"✅ API Server: {data.get('service')} v{data.get('version')}"
|
| 737 |
+
else:
|
| 738 |
+
return f"⚠️ API Server returned status {response.status_code}"
|
| 739 |
+
except requests.exceptions.ConnectionError:
|
| 740 |
+
return "❌ API Server not running. Start it with: pnpm dev"
|
| 741 |
+
except Exception as e:
|
| 742 |
+
return f"❌ Error: {str(e)}"
|
| 743 |
+
|
| 744 |
+
def get_available_tools() -> str:
|
| 745 |
+
"""Get list of available MCP tools with descriptions"""
|
| 746 |
+
try:
|
| 747 |
+
response = requests.get(f"{API_URL}/api/tools", timeout=5)
|
| 748 |
+
if response.status_code == 200:
|
| 749 |
+
data = response.json()
|
| 750 |
+
tools = data.get("tools", [])
|
| 751 |
+
if tools:
|
| 752 |
+
tool_list = "\n".join([f"• **{t['name']}**" for t in tools])
|
| 753 |
+
return f"""**Available MCP Tools:**
|
| 754 |
+
|
| 755 |
+
{tool_list}
|
| 756 |
+
|
| 757 |
+
**✨ Capabilities:**
|
| 758 |
+
• Generate complete compliance reports
|
| 759 |
+
• Create documentation templates (Risk Management, Technical Docs, etc.)
|
| 760 |
+
• Discover AI systems and assess risk levels
|
| 761 |
+
• Analyze organization compliance gaps"""
|
| 762 |
+
return "No tools available"
|
| 763 |
+
return "Could not fetch tools"
|
| 764 |
+
except:
|
| 765 |
+
return "Could not connect to API"
|
| 766 |
+
|
| 767 |
+
def get_example_queries() -> List[List[str]]:
|
| 768 |
+
"""Get example queries for the interface"""
|
| 769 |
+
return [
|
| 770 |
+
# MCP Tools Examples - Showcase full compliance analysis capabilities
|
| 771 |
+
["Generate a complete EU AI Act compliance report for Microsoft with all documentation templates"],
|
| 772 |
+
["Analyze IBM's watsonX system compliance and generate risk management documentation"],
|
| 773 |
+
["Create full compliance assessment for OpenAI including technical documentation templates"],
|
| 774 |
+
# General Questions
|
| 775 |
+
["What is the EU AI Act?"],
|
| 776 |
+
["Is a recruitment screening AI considered high-risk?"],
|
| 777 |
+
["What are the compliance requirements for chatbots?"],
|
| 778 |
+
["What's the timeline for EU AI Act enforcement?"],
|
| 779 |
+
]
|
| 780 |
+
|
| 781 |
+
# Default browser state for persistent storage
|
| 782 |
+
DEFAULT_BROWSER_STATE = {
|
| 783 |
+
"api_keys": {
|
| 784 |
+
"tavily": "",
|
| 785 |
+
"anthropic": "",
|
| 786 |
+
"google": "",
|
| 787 |
+
"openai": "",
|
| 788 |
+
"xai": ""
|
| 789 |
+
},
|
| 790 |
+
"model": "gpt-oss"
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
# Create Gradio interface
|
| 794 |
+
with gr.Blocks(
|
| 795 |
+
title="🇪🇺 EU AI Act Compliance Agent",
|
| 796 |
+
) as demo:
|
| 797 |
+
# Browser state for persistent storage (persists across page refreshes)
|
| 798 |
+
browser_state = gr.BrowserState(DEFAULT_BROWSER_STATE)
|
| 799 |
+
|
| 800 |
+
# Custom CSS only - JavaScript is loaded via gr.Blocks(js=...) parameter
|
| 801 |
+
gr.HTML("""
|
| 802 |
+
<style>
|
| 803 |
+
/* Hide Gradio's default footer */
|
| 804 |
+
footer { display: none !important; }
|
| 805 |
+
.gradio-container footer { display: none !important; }
|
| 806 |
+
.footer { display: none !important; }
|
| 807 |
+
[data-testid="footer"] { display: none !important; }
|
| 808 |
+
|
| 809 |
+
/* Style the stop button */
|
| 810 |
+
button.stop {
|
| 811 |
+
background-color: #dc3545 !important;
|
| 812 |
+
border-color: #dc3545 !important;
|
| 813 |
+
}
|
| 814 |
+
button.stop:hover {
|
| 815 |
+
background-color: #c82333 !important;
|
| 816 |
+
border-color: #bd2130 !important;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
/* Scroll indicator when user has scrolled up */
|
| 820 |
+
.scroll-indicator {
|
| 821 |
+
position: absolute;
|
| 822 |
+
bottom: 10px;
|
| 823 |
+
right: 20px;
|
| 824 |
+
background: rgba(0, 0, 0, 0.7);
|
| 825 |
+
color: white;
|
| 826 |
+
padding: 8px 16px;
|
| 827 |
+
border-radius: 20px;
|
| 828 |
+
font-size: 12px;
|
| 829 |
+
cursor: pointer;
|
| 830 |
+
z-index: 1000;
|
| 831 |
+
display: none;
|
| 832 |
+
}
|
| 833 |
+
.scroll-indicator:hover {
|
| 834 |
+
background: rgba(0, 0, 0, 0.9);
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
/* Keys loaded indicator */
|
| 838 |
+
.keys-loaded-badge {
|
| 839 |
+
display: inline-block;
|
| 840 |
+
background: #28a745;
|
| 841 |
+
color: white;
|
| 842 |
+
padding: 2px 8px;
|
| 843 |
+
border-radius: 12px;
|
| 844 |
+
font-size: 11px;
|
| 845 |
+
margin-left: 8px;
|
| 846 |
+
}
|
| 847 |
+
</style>
|
| 848 |
+
""")
|
| 849 |
+
|
| 850 |
+
# Header - use PUBLIC_URL for production links
|
| 851 |
+
# In production (HF Spaces): Show info about gradio.live URL
|
| 852 |
+
# In local dev: Direct link to localhost:7861
|
| 853 |
+
chatgpt_link_href = PUBLIC_URL if PUBLIC_URL else "http://localhost:7861"
|
| 854 |
+
is_production = bool(PUBLIC_URL)
|
| 855 |
+
|
| 856 |
+
# Get MCP URL (written by chatgpt_app.py when it starts)
|
| 857 |
+
mcp_url = get_mcp_url()
|
| 858 |
+
|
| 859 |
+
# MCP Server is deployed separately
|
| 860 |
+
MCP_SPACE_URL = "https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space"
|
| 861 |
+
MCP_URL = f"{MCP_SPACE_URL}/gradio_api/mcp/"
|
| 862 |
+
|
| 863 |
+
if is_production:
|
| 864 |
+
# Production: Link to separate MCP Space
|
| 865 |
+
chatgpt_section = f"""
|
| 866 |
+
<a href="{MCP_SPACE_URL}" target="_blank" style="text-decoration: none;">
|
| 867 |
+
<div style="background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); padding: 12px 20px; border-radius: 10px; display: inline-block;">
|
| 868 |
+
<span style="color: #fff; font-size: 0.9em;">
|
| 869 |
+
💬 <strong>ChatGPT MCP Server</strong>
|
| 870 |
+
</span>
|
| 871 |
+
<p style="color: rgba(255,255,255,0.9); font-size: 0.75em; margin: 8px 0 0 0; word-break: break-all;">
|
| 872 |
+
<code style="background: rgba(255,255,255,0.2); padding: 3px 6px; border-radius: 3px;">{MCP_URL}</code>
|
| 873 |
+
</p>
|
| 874 |
+
<p style="color: rgba(255,255,255,0.7); font-size: 0.7em; margin: 6px 0 0 0;">
|
| 875 |
+
Click to open MCP Server Space →
|
| 876 |
+
</p>
|
| 877 |
+
</div>
|
| 878 |
+
</a>
|
| 879 |
+
"""
|
| 880 |
+
else:
|
| 881 |
+
# Local dev: Direct link
|
| 882 |
+
chatgpt_section = """
|
| 883 |
+
<a href="http://localhost:7861" target="_blank" style="color: #2196F3; text-decoration: none; padding: 6px 12px; border: 1px solid #2196F3; border-radius: 4px; display: inline-block;">
|
| 884 |
+
💬 Open ChatGPT App (MCP Server)
|
| 885 |
+
</a>
|
| 886 |
+
"""
|
| 887 |
+
|
| 888 |
+
gr.HTML(f"""
|
| 889 |
+
<div style="text-align: center; padding: 20px 0;">
|
| 890 |
+
<h1 style="margin: 0; font-size: 2em;">🇪🇺 EU AI Act Compliance Agent</h1>
|
| 891 |
+
<p style="margin: 10px 0 0 0; opacity: 0.8;">by <a href="https://www.legitima.ai/mcp-hackathon" target="_blank" style="color: #4CAF50;">Legitima.ai</a></p>
|
| 892 |
+
<p style="margin: 5px 0; font-size: 0.9em; opacity: 0.7;">Your intelligent assistant for navigating European AI regulation</p>
|
| 893 |
+
<p style="margin: 10px 0 0 0; font-size: 0.9em;">
|
| 894 |
+
{chatgpt_section}
|
| 895 |
+
</p>
|
| 896 |
+
</div>
|
| 897 |
+
""")
|
| 898 |
+
|
| 899 |
+
# Main content
|
| 900 |
+
with gr.Row():
|
| 901 |
+
with gr.Column(scale=3):
|
| 902 |
+
# Chat interface - using ChatMessage format
|
| 903 |
+
chatbot = gr.Chatbot(
|
| 904 |
+
label="Chat with EU AI Act Expert",
|
| 905 |
+
height=550,
|
| 906 |
+
show_label=True,
|
| 907 |
+
autoscroll=False, # Disable auto-scroll - we handle it with JS
|
| 908 |
+
)
|
| 909 |
+
|
| 910 |
+
with gr.Row():
|
| 911 |
+
msg = gr.Textbox(
|
| 912 |
+
placeholder="Ask about compliance, or request a full compliance report with documentation for an organization...",
|
| 913 |
+
show_label=False,
|
| 914 |
+
scale=8,
|
| 915 |
+
)
|
| 916 |
+
submit = gr.Button("Send", variant="primary", scale=1)
|
| 917 |
+
stop_btn = gr.Button("⏹ Stop", variant="stop", scale=1, visible=False)
|
| 918 |
+
|
| 919 |
+
gr.Examples(
|
| 920 |
+
examples=get_example_queries(),
|
| 921 |
+
inputs=msg,
|
| 922 |
+
label="💡 Example Questions (Try MCP tools for compliance reports & documentation!)",
|
| 923 |
+
)
|
| 924 |
+
|
| 925 |
+
with gr.Column(scale=1):
|
| 926 |
+
# Sidebar
|
| 927 |
+
gr.Markdown("### 🤖 Model Settings")
|
| 928 |
+
|
| 929 |
+
model_dropdown = gr.Dropdown(
|
| 930 |
+
choices=[(v["name"], k) for k, v in AVAILABLE_MODELS.items()],
|
| 931 |
+
value=current_model_settings["model"], # Use current model setting
|
| 932 |
+
label="AI Model",
|
| 933 |
+
info="Select the AI model to use. ⚠️ GPT-OSS is FREE but may take up to 60s to start (cold start). For faster responses and better precision, use another model with your API key.",
|
| 934 |
+
elem_id="model_dropdown"
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
+
# API Key inputs (password fields) - GPT-OSS is FREE, other models require API keys
|
| 938 |
+
with gr.Accordion("🔑 API Keys & Settings", open=True):
|
| 939 |
+
gr.Markdown("""🆓 **GPT-OSS 20B is FREE** - Uses pre-configured Modal endpoint (no setup required).
|
| 940 |
+
|
| 941 |
+
⏱️ **Note:** GPT-OSS may take up to **60 seconds** to start responding due to cold start. For **faster responses and better precision**, select another model and provide your API key below.
|
| 942 |
+
|
| 943 |
+
⚠️ For paid models (Claude, GPT-5, Gemini, Grok), an API key is required.
|
| 944 |
+
|
| 945 |
+
🔐 Keys are stored securely in encoded cookies and **auto-expire after 24 hours**.
|
| 946 |
+
|
| 947 |
+
ℹ️ *Tavily is **optional** - enhances web research for organization & AI systems discovery. Falls back to server's `TAVILY_API_KEY` env var if not provided, then to AI model.*""")
|
| 948 |
+
|
| 949 |
+
gr.Markdown("#### 🔍 Research API (Optional)")
|
| 950 |
+
tavily_key = gr.Textbox(
|
| 951 |
+
label="Tavily API Key (Optional)",
|
| 952 |
+
placeholder="tvly-... (optional - enhances web research)",
|
| 953 |
+
type="password",
|
| 954 |
+
value="", # Will be populated from cookies via JS
|
| 955 |
+
info="Optional - uses server env var fallback, then AI model.",
|
| 956 |
+
elem_id="tavily_key_input"
|
| 957 |
+
)
|
| 958 |
+
|
| 959 |
+
gr.Markdown("#### 🤖 AI Model APIs")
|
| 960 |
+
anthropic_key = gr.Textbox(
|
| 961 |
+
label="Anthropic API Key *",
|
| 962 |
+
placeholder="sk-ant-... (required for Claude models)",
|
| 963 |
+
type="password",
|
| 964 |
+
value="", # Will be populated from cookies via JS
|
| 965 |
+
info="Required - for Claude 4.5 Sonnet or Claude Opus 4",
|
| 966 |
+
elem_id="anthropic_key_input"
|
| 967 |
+
)
|
| 968 |
+
google_key = gr.Textbox(
|
| 969 |
+
label="Google API Key",
|
| 970 |
+
placeholder="AIza... (required for Gemini 3)",
|
| 971 |
+
type="password",
|
| 972 |
+
value="", # Will be populated from cookies via JS
|
| 973 |
+
info="Required if using Gemini 3 Pro model",
|
| 974 |
+
elem_id="google_key_input"
|
| 975 |
+
)
|
| 976 |
+
openai_key = gr.Textbox(
|
| 977 |
+
label="OpenAI API Key",
|
| 978 |
+
placeholder="sk-... (required for GPT-5)",
|
| 979 |
+
type="password",
|
| 980 |
+
value="", # Will be populated from cookies via JS
|
| 981 |
+
info="Required if using GPT-5 model",
|
| 982 |
+
elem_id="openai_key_input"
|
| 983 |
+
)
|
| 984 |
+
xai_key = gr.Textbox(
|
| 985 |
+
label="xAI API Key",
|
| 986 |
+
placeholder="xai-... (required for Grok 4.1)",
|
| 987 |
+
type="password",
|
| 988 |
+
value="", # Will be populated from cookies via JS
|
| 989 |
+
info="Required if using Grok 4.1 model",
|
| 990 |
+
elem_id="xai_key_input"
|
| 991 |
+
)
|
| 992 |
+
with gr.Row():
|
| 993 |
+
save_keys_btn = gr.Button("💾 Save Keys", variant="secondary", size="sm")
|
| 994 |
+
clear_keys_btn = gr.Button("🗑️ Clear Keys", variant="stop", size="sm")
|
| 995 |
+
keys_status = gr.Markdown("")
|
| 996 |
+
|
| 997 |
+
gr.Markdown("---")
|
| 998 |
+
|
| 999 |
+
gr.Markdown("### 📊 Quick Reference")
|
| 1000 |
+
|
| 1001 |
+
gr.Markdown("""
|
| 1002 |
+
**Risk Categories:**
|
| 1003 |
+
- 🔴 **Unacceptable** - Banned
|
| 1004 |
+
- 🟠 **High Risk** - Strict requirements
|
| 1005 |
+
- 🟡 **Limited Risk** - Transparency
|
| 1006 |
+
- 🟢 **Minimal Risk** - No obligations
|
| 1007 |
+
|
| 1008 |
+
**Key Deadlines:**
|
| 1009 |
+
- 📅 Feb 2, 2025: Banned AI
|
| 1010 |
+
- 📅 Aug 2, 2026: High-risk rules
|
| 1011 |
+
- 📅 Aug 2, 2027: Full enforcement
|
| 1012 |
+
""")
|
| 1013 |
+
|
| 1014 |
+
gr.Markdown("---")
|
| 1015 |
+
|
| 1016 |
+
tools_info = gr.Markdown(
|
| 1017 |
+
value=get_available_tools(),
|
| 1018 |
+
label="🔧 MCP Tools - Generate Reports & Documentation"
|
| 1019 |
+
)
|
| 1020 |
+
|
| 1021 |
+
gr.Markdown("---")
|
| 1022 |
+
|
| 1023 |
+
# Sidebar ChatGPT App section
|
| 1024 |
+
if is_production:
|
| 1025 |
+
sidebar_chatgpt = f"""
|
| 1026 |
+
<a href="{MCP_SPACE_URL}" target="_blank" style="text-decoration: none;">
|
| 1027 |
+
<div style="background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); padding: 12px; border-radius: 8px; margin: 5px 0;">
|
| 1028 |
+
<strong style="color: #fff;">💬 MCP Server</strong>
|
| 1029 |
+
<p style="color: rgba(255,255,255,0.9); font-size: 0.7em; margin: 6px 0 0 0;">
|
| 1030 |
+
Click to get MCP URL →
|
| 1031 |
+
</p>
|
| 1032 |
+
</div>
|
| 1033 |
+
</a>
|
| 1034 |
+
"""
|
| 1035 |
+
else:
|
| 1036 |
+
sidebar_chatgpt = """
|
| 1037 |
+
<a href="http://localhost:7861" target="_blank" style="color: #2196F3; text-decoration: none; padding: 8px 16px; border: 1px solid #2196F3; border-radius: 6px; display: inline-block; margin: 5px 0;">
|
| 1038 |
+
💬 ChatGPT App (MCP Server)
|
| 1039 |
+
</a>
|
| 1040 |
+
<p style="font-size: 0.85em; opacity: 0.7; margin-top: 8px;">
|
| 1041 |
+
Use the ChatGPT App to connect with ChatGPT Desktop and access MCP tools via OpenAI Apps SDK.
|
| 1042 |
+
</p>
|
| 1043 |
+
"""
|
| 1044 |
+
|
| 1045 |
+
gr.HTML(f"""
|
| 1046 |
+
<div>
|
| 1047 |
+
<h3 style="margin-bottom: 10px;">🔗 Other Interfaces</h3>
|
| 1048 |
+
<div>
|
| 1049 |
+
{sidebar_chatgpt}
|
| 1050 |
+
</div>
|
| 1051 |
+
</div>
|
| 1052 |
+
""")
|
| 1053 |
+
|
| 1054 |
+
gr.Markdown("---")
|
| 1055 |
+
|
| 1056 |
+
status = gr.Textbox(
|
| 1057 |
+
label="🔌 API Status",
|
| 1058 |
+
value=check_api_status(),
|
| 1059 |
+
interactive=False,
|
| 1060 |
+
max_lines=2,
|
| 1061 |
+
)
|
| 1062 |
+
|
| 1063 |
+
with gr.Row():
|
| 1064 |
+
refresh_btn = gr.Button("🔄 Refresh", size="sm")
|
| 1065 |
+
clear_btn = gr.Button("🗑️ Clear", size="sm")
|
| 1066 |
+
|
| 1067 |
+
# Footer
|
| 1068 |
+
gr.Markdown("""
|
| 1069 |
+
---
|
| 1070 |
+
<div style="text-align: center; opacity: 0.7; font-size: 0.85em;">
|
| 1071 |
+
<p>Built for the MCP 1st Birthday Hackathon 🎂</p>
|
| 1072 |
+
<p>Powered by Vercel AI SDK v5 + Model Context Protocol + Gradio</p>
|
| 1073 |
+
</div>
|
| 1074 |
+
""")
|
| 1075 |
+
|
| 1076 |
+
# Disclaimer box - separate for better visibility
|
| 1077 |
+
gr.HTML("""
|
| 1078 |
+
<style>
|
| 1079 |
+
.disclaimer-box {
|
| 1080 |
+
text-align: center;
|
| 1081 |
+
margin: 20px auto;
|
| 1082 |
+
padding: 15px 20px;
|
| 1083 |
+
max-width: 800px;
|
| 1084 |
+
background: #fff3cd !important;
|
| 1085 |
+
border: 2px solid #ffc107 !important;
|
| 1086 |
+
border-radius: 8px;
|
| 1087 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 1088 |
+
}
|
| 1089 |
+
.disclaimer-box p {
|
| 1090 |
+
color: #000000 !important;
|
| 1091 |
+
margin: 0;
|
| 1092 |
+
}
|
| 1093 |
+
.disclaimer-box strong {
|
| 1094 |
+
color: #000000 !important;
|
| 1095 |
+
}
|
| 1096 |
+
</style>
|
| 1097 |
+
<div class="disclaimer-box">
|
| 1098 |
+
<p style="font-size: 0.9em; font-weight: 500; margin-bottom: 8px;">
|
| 1099 |
+
<strong>⚠️ Disclaimer:</strong> This is a <strong style="background: rgba(255, 193, 7, 0.3); padding: 2px 4px; border-radius: 3px;">demo application (Work in Progress)</strong> and does not constitute legal advice.
|
| 1100 |
+
</p>
|
| 1101 |
+
<p style="font-size: 0.85em; line-height: 1.4;">
|
| 1102 |
+
Always consult with qualified legal professionals before making compliance decisions based on AI outputs.
|
| 1103 |
+
</p>
|
| 1104 |
+
</div>
|
| 1105 |
+
""")
|
| 1106 |
+
|
| 1107 |
+
# Model and API key handlers
|
| 1108 |
+
def update_model(model_value):
|
| 1109 |
+
"""Update the selected model"""
|
| 1110 |
+
current_model_settings["model"] = model_value
|
| 1111 |
+
model_info = AVAILABLE_MODELS.get(model_value, {})
|
| 1112 |
+
print(f"[Gradio] Model updated to: {model_value} ({model_info.get('name', model_value)})")
|
| 1113 |
+
return f"✅ Model set to: **{model_info.get('name', model_value)}**"
|
| 1114 |
+
|
| 1115 |
+
def save_api_keys(tavily_val, anthropic_val, google_val, openai_val, xai_val):
|
| 1116 |
+
"""Save user-provided API keys to session AND secure cookie storage
|
| 1117 |
+
|
| 1118 |
+
SECURITY: Keys are stored in memory for this session AND in encoded cookies
|
| 1119 |
+
that expire after 1 day. Cookies use XOR obfuscation + base64 encoding.
|
| 1120 |
+
GPT-OSS is FREE and uses pre-configured Modal endpoint from environment.
|
| 1121 |
+
Paid models require API keys. Note: Tavily is OPTIONAL - AI model is used as fallback for research.
|
| 1122 |
+
"""
|
| 1123 |
+
saved = []
|
| 1124 |
+
|
| 1125 |
+
# Only update if a real key is provided
|
| 1126 |
+
if tavily_val and len(tavily_val) > 10:
|
| 1127 |
+
current_model_settings["tavily_api_key"] = tavily_val
|
| 1128 |
+
saved.append("Tavily")
|
| 1129 |
+
|
| 1130 |
+
if anthropic_val and len(anthropic_val) > 10:
|
| 1131 |
+
current_model_settings["anthropic_api_key"] = anthropic_val
|
| 1132 |
+
saved.append("Anthropic")
|
| 1133 |
+
|
| 1134 |
+
if google_val and len(google_val) > 10:
|
| 1135 |
+
current_model_settings["google_api_key"] = google_val
|
| 1136 |
+
saved.append("Google")
|
| 1137 |
+
|
| 1138 |
+
if openai_val and len(openai_val) > 10:
|
| 1139 |
+
current_model_settings["openai_api_key"] = openai_val
|
| 1140 |
+
saved.append("OpenAI")
|
| 1141 |
+
|
| 1142 |
+
if xai_val and len(xai_val) > 10:
|
| 1143 |
+
current_model_settings["xai_api_key"] = xai_val
|
| 1144 |
+
saved.append("xAI")
|
| 1145 |
+
|
| 1146 |
+
# Build status message
|
| 1147 |
+
status_parts = []
|
| 1148 |
+
|
| 1149 |
+
# Always show current model
|
| 1150 |
+
model = current_model_settings["model"]
|
| 1151 |
+
model_info = AVAILABLE_MODELS.get(model, {})
|
| 1152 |
+
status_parts.append(f"🤖 **Model:** {model_info.get('name', model)}")
|
| 1153 |
+
|
| 1154 |
+
if saved:
|
| 1155 |
+
status_parts.append(f"✅ **Keys Saved:** {', '.join(saved)}")
|
| 1156 |
+
|
| 1157 |
+
status_parts.append("🔐 *Settings stored in secure cookies (expires in 24h)*")
|
| 1158 |
+
|
| 1159 |
+
# Check for missing required keys based on selected model
|
| 1160 |
+
if model == "gpt-oss":
|
| 1161 |
+
# GPT-OSS uses hardcoded Modal endpoint - always available
|
| 1162 |
+
status_parts.append("🆓 *GPT-OSS model is FREE - no API key required!*")
|
| 1163 |
+
elif model in ["claude-4.5", "claude-opus"] and not current_model_settings["anthropic_api_key"]:
|
| 1164 |
+
status_parts.append(f"⚠️ **Anthropic API key required** for {model}")
|
| 1165 |
+
elif model == "gemini-3" and not current_model_settings["google_api_key"]:
|
| 1166 |
+
status_parts.append("⚠️ **Google API key required** for Gemini 3")
|
| 1167 |
+
elif model == "gpt-5" and not current_model_settings["openai_api_key"]:
|
| 1168 |
+
status_parts.append("⚠️ **OpenAI API key required** for GPT-5")
|
| 1169 |
+
elif model == "grok-4-1" and not current_model_settings["xai_api_key"]:
|
| 1170 |
+
status_parts.append("⚠️ **xAI API key required** for Grok 4.1")
|
| 1171 |
+
|
| 1172 |
+
# Tavily is optional - just inform user about enhanced features if they have it
|
| 1173 |
+
if not current_model_settings["tavily_api_key"]:
|
| 1174 |
+
status_parts.append("ℹ️ *Tavily not set - will use server env var fallback or AI model*")
|
| 1175 |
+
|
| 1176 |
+
return "\n\n".join(status_parts)
|
| 1177 |
+
|
| 1178 |
+
def get_current_model_status():
|
| 1179 |
+
"""Get current model and key status"""
|
| 1180 |
+
model = current_model_settings["model"]
|
| 1181 |
+
model_info = AVAILABLE_MODELS.get(model, {})
|
| 1182 |
+
required_key = model_info.get("api_key_env", "")
|
| 1183 |
+
|
| 1184 |
+
key_status = "❌ Missing"
|
| 1185 |
+
if required_key == "OPENAI_API_KEY" and current_model_settings["openai_api_key"]:
|
| 1186 |
+
key_status = "✅ Set"
|
| 1187 |
+
elif required_key == "XAI_API_KEY" and current_model_settings["xai_api_key"]:
|
| 1188 |
+
key_status = "✅ Set"
|
| 1189 |
+
elif required_key == "ANTHROPIC_API_KEY" and current_model_settings["anthropic_api_key"]:
|
| 1190 |
+
key_status = "✅ Set"
|
| 1191 |
+
|
| 1192 |
+
return f"**Model:** {model_info.get('name', model)}\n**Key Status:** {key_status}"
|
| 1193 |
+
|
| 1194 |
+
def check_required_keys():
|
| 1195 |
+
"""Check if required API keys are configured
|
| 1196 |
+
|
| 1197 |
+
Returns a tuple of (is_valid, error_message)
|
| 1198 |
+
- is_valid: True if all required keys are present
|
| 1199 |
+
- error_message: Description of missing keys if not valid
|
| 1200 |
+
|
| 1201 |
+
Note: GPT-OSS is FREE and no API key is required.
|
| 1202 |
+
Tavily API key is optional - the system will fallback to AI model for research.
|
| 1203 |
+
"""
|
| 1204 |
+
missing_keys = []
|
| 1205 |
+
|
| 1206 |
+
# Check model API key based on selected model
|
| 1207 |
+
model = current_model_settings["model"]
|
| 1208 |
+
if model == "gpt-oss":
|
| 1209 |
+
# GPT-OSS uses hardcoded endpoint - no check needed
|
| 1210 |
+
pass
|
| 1211 |
+
elif model in ["claude-4.5", "claude-opus"] and not current_model_settings["anthropic_api_key"]:
|
| 1212 |
+
missing_keys.append(f"**Anthropic API Key** (required for {model})")
|
| 1213 |
+
elif model == "gemini-3" and not current_model_settings["google_api_key"]:
|
| 1214 |
+
missing_keys.append("**Google API Key** (required for Gemini 3)")
|
| 1215 |
+
elif model == "gpt-5" and not current_model_settings["openai_api_key"]:
|
| 1216 |
+
missing_keys.append("**OpenAI API Key** (required for GPT-5)")
|
| 1217 |
+
elif model == "grok-4-1" and not current_model_settings["xai_api_key"]:
|
| 1218 |
+
missing_keys.append("**xAI API Key** (required for Grok 4.1)")
|
| 1219 |
+
|
| 1220 |
+
# Note: Tavily is OPTIONAL - system will fallback to AI model for research
|
| 1221 |
+
# We no longer require Tavily API key
|
| 1222 |
+
|
| 1223 |
+
if missing_keys:
|
| 1224 |
+
error_msg = """## ⚠️ API Keys Required
|
| 1225 |
+
|
| 1226 |
+
To use this service, you need to provide your own API keys. The following keys are missing:
|
| 1227 |
+
|
| 1228 |
+
"""
|
| 1229 |
+
for key in missing_keys:
|
| 1230 |
+
error_msg += f"- {key}\n"
|
| 1231 |
+
|
| 1232 |
+
error_msg += """
|
| 1233 |
+
### How to add your API keys:
|
| 1234 |
+
|
| 1235 |
+
1. Expand the **🔑 API Keys & Settings** section in the sidebar
|
| 1236 |
+
2. Enter your API keys in the corresponding fields
|
| 1237 |
+
3. Click **💾 Save Keys**
|
| 1238 |
+
|
| 1239 |
+
### Where to get API keys:
|
| 1240 |
+
|
| 1241 |
+
- **Anthropic**: [console.anthropic.com](https://console.anthropic.com) - Get Claude API key
|
| 1242 |
+
- **Google**: [aistudio.google.com](https://aistudio.google.com/apikey) - Get Gemini API key
|
| 1243 |
+
- **OpenAI**: [platform.openai.com](https://platform.openai.com) - Get GPT API key
|
| 1244 |
+
- **xAI**: [console.x.ai](https://console.x.ai) - Get Grok API key
|
| 1245 |
+
|
| 1246 |
+
**🆓 FREE Alternative:**
|
| 1247 |
+
- Select **GPT-OSS 20B** from the model dropdown - it's FREE via Modal.com!
|
| 1248 |
+
|
| 1249 |
+
**Optional:**
|
| 1250 |
+
- **Tavily**: [tavily.com](https://tavily.com) - For enhanced web research (falls back to server env var, then AI model)
|
| 1251 |
+
"""
|
| 1252 |
+
return False, error_msg
|
| 1253 |
+
|
| 1254 |
+
return True, ""
|
| 1255 |
+
|
| 1256 |
+
# Event handlers - clear input immediately and stream response together
|
| 1257 |
+
def respond_and_clear(message: str, history: list):
|
| 1258 |
+
"""Wrapper that yields (cleared_input, chat_history, stop_visible) tuples"""
|
| 1259 |
+
global cancel_token
|
| 1260 |
+
|
| 1261 |
+
if not message.strip():
|
| 1262 |
+
yield "", history, gr.update(visible=False)
|
| 1263 |
+
return
|
| 1264 |
+
|
| 1265 |
+
# Check for required API keys before proceeding
|
| 1266 |
+
keys_valid, error_message = check_required_keys()
|
| 1267 |
+
if not keys_valid:
|
| 1268 |
+
# Show user message and error about missing keys
|
| 1269 |
+
error_history = list(history) + [
|
| 1270 |
+
ChatMessage(role="user", content=message),
|
| 1271 |
+
ChatMessage(role="assistant", content=error_message)
|
| 1272 |
+
]
|
| 1273 |
+
yield "", error_history, gr.update(visible=False)
|
| 1274 |
+
return
|
| 1275 |
+
|
| 1276 |
+
# Reset cancellation token for new request
|
| 1277 |
+
cancel_token.reset()
|
| 1278 |
+
|
| 1279 |
+
# First yield: clear input, show loading, and show stop button
|
| 1280 |
+
# Get the actual selected model from current_model_settings (not from env)
|
| 1281 |
+
selected_model = current_model_settings.get("model", "gpt-oss")
|
| 1282 |
+
model_info = AVAILABLE_MODELS.get(selected_model, {})
|
| 1283 |
+
model_name = model_info.get("name", selected_model)
|
| 1284 |
+
print(f"[Gradio] Using model: {selected_model} ({model_name})")
|
| 1285 |
+
initial_history = list(history) + [
|
| 1286 |
+
ChatMessage(role="user", content=message),
|
| 1287 |
+
ChatMessage(role="assistant", content=f"⏳ *Thinking with {model_name}...*")
|
| 1288 |
+
]
|
| 1289 |
+
yield "", initial_history, gr.update(visible=True)
|
| 1290 |
+
|
| 1291 |
+
# Stream the actual response (pass initialized_history to avoid duplication)
|
| 1292 |
+
updated_history = initial_history # Initialize in case generator doesn't yield
|
| 1293 |
+
for updated_history in chat_with_agent_streaming(message, history, initial_history):
|
| 1294 |
+
# Check if cancelled during streaming
|
| 1295 |
+
if cancel_token.is_cancelled():
|
| 1296 |
+
break
|
| 1297 |
+
yield "", updated_history, gr.update(visible=True)
|
| 1298 |
+
|
| 1299 |
+
# Final yield: hide stop button when done
|
| 1300 |
+
yield "", updated_history, gr.update(visible=False)
|
| 1301 |
+
|
| 1302 |
+
def stop_response(history: list):
|
| 1303 |
+
"""Stop the current response by triggering cancellation"""
|
| 1304 |
+
global cancel_token
|
| 1305 |
+
|
| 1306 |
+
# Trigger cancellation - this will close the HTTP connection
|
| 1307 |
+
cancel_token.cancel()
|
| 1308 |
+
|
| 1309 |
+
# Update history to show stopped state (the generator will also update when it detects cancellation)
|
| 1310 |
+
if history and len(history) > 0:
|
| 1311 |
+
last_msg = history[-1]
|
| 1312 |
+
if isinstance(last_msg, ChatMessage):
|
| 1313 |
+
content = last_msg.content
|
| 1314 |
+
# Remove thinking indicator and add stopped message
|
| 1315 |
+
if "⏳" in content:
|
| 1316 |
+
content = content.replace("⏳ *Thinking", "⏹️ *Stopped")
|
| 1317 |
+
content = content.replace("⏳ *Processing", "⏹️ *Stopped")
|
| 1318 |
+
if "*— Execution stopped by user*" not in content:
|
| 1319 |
+
content += "\n\n*— Execution stopped by user*"
|
| 1320 |
+
history[-1] = ChatMessage(role="assistant", content=content)
|
| 1321 |
+
|
| 1322 |
+
# Return history and hide stop button
|
| 1323 |
+
return history, gr.update(visible=False)
|
| 1324 |
+
|
| 1325 |
+
# On submit/click: clear input immediately while streaming response
|
| 1326 |
+
# These events are cancellable by the stop button
|
| 1327 |
+
submit_event = msg.submit(respond_and_clear, [msg, chatbot], [msg, chatbot, stop_btn])
|
| 1328 |
+
click_event = submit.click(respond_and_clear, [msg, chatbot], [msg, chatbot, stop_btn])
|
| 1329 |
+
|
| 1330 |
+
# Stop button cancels the streaming events and updates the chat
|
| 1331 |
+
stop_btn.click(
|
| 1332 |
+
fn=stop_response,
|
| 1333 |
+
inputs=[chatbot],
|
| 1334 |
+
outputs=[chatbot, stop_btn],
|
| 1335 |
+
cancels=[submit_event, click_event]
|
| 1336 |
+
)
|
| 1337 |
+
|
| 1338 |
+
refresh_btn.click(
|
| 1339 |
+
lambda: (check_api_status(), get_available_tools()),
|
| 1340 |
+
None,
|
| 1341 |
+
[status, tools_info]
|
| 1342 |
+
)
|
| 1343 |
+
clear_btn.click(lambda: [], None, chatbot)
|
| 1344 |
+
|
| 1345 |
+
# Function to update model in browser state
|
| 1346 |
+
def save_model_to_browser_state(model_val, stored_data):
|
| 1347 |
+
"""Save model selection to browser state"""
|
| 1348 |
+
if stored_data is None:
|
| 1349 |
+
stored_data = DEFAULT_BROWSER_STATE.copy()
|
| 1350 |
+
new_data = stored_data.copy()
|
| 1351 |
+
new_data["model"] = model_val or "gpt-oss"
|
| 1352 |
+
print(f"[BrowserState] Model changed to: {model_val}")
|
| 1353 |
+
return new_data
|
| 1354 |
+
|
| 1355 |
+
# Model selection handler - also saves to browser state
|
| 1356 |
+
model_dropdown.change(
|
| 1357 |
+
update_model,
|
| 1358 |
+
[model_dropdown],
|
| 1359 |
+
[keys_status]
|
| 1360 |
+
).then(
|
| 1361 |
+
save_model_to_browser_state,
|
| 1362 |
+
[model_dropdown, browser_state],
|
| 1363 |
+
[browser_state]
|
| 1364 |
+
)
|
| 1365 |
+
|
| 1366 |
+
# Function to save settings to browser state
|
| 1367 |
+
def save_to_browser_state(tavily_val, anthropic_val, google_val, openai_val, xai_val, model_val, stored_data):
|
| 1368 |
+
"""Save API keys and model to browser state (persists across refreshes)"""
|
| 1369 |
+
new_data = {
|
| 1370 |
+
"api_keys": {
|
| 1371 |
+
"tavily": tavily_val or "",
|
| 1372 |
+
"anthropic": anthropic_val or "",
|
| 1373 |
+
"google": google_val or "",
|
| 1374 |
+
"openai": openai_val or "",
|
| 1375 |
+
"xai": xai_val or ""
|
| 1376 |
+
},
|
| 1377 |
+
"model": model_val or "gpt-oss"
|
| 1378 |
+
}
|
| 1379 |
+
print(f"[BrowserState] Saving to browser: model={model_val}, keys={[k for k,v in new_data['api_keys'].items() if v]}")
|
| 1380 |
+
return new_data
|
| 1381 |
+
|
| 1382 |
+
# Combined save handler - saves to session AND browser state
|
| 1383 |
+
save_keys_btn.click(
|
| 1384 |
+
save_api_keys,
|
| 1385 |
+
[tavily_key, anthropic_key, google_key, openai_key, xai_key],
|
| 1386 |
+
[keys_status]
|
| 1387 |
+
).then(
|
| 1388 |
+
save_to_browser_state,
|
| 1389 |
+
[tavily_key, anthropic_key, google_key, openai_key, xai_key, model_dropdown, browser_state],
|
| 1390 |
+
[browser_state]
|
| 1391 |
+
)
|
| 1392 |
+
|
| 1393 |
+
# Clear keys function - clears both session and cookies, resets model to default
|
| 1394 |
+
def clear_api_keys():
|
| 1395 |
+
"""Clear all stored API keys from session and cookies, reset model to default"""
|
| 1396 |
+
# Note: modal_endpoint_url is hardcoded, so we don't clear it
|
| 1397 |
+
current_model_settings["tavily_api_key"] = ""
|
| 1398 |
+
current_model_settings["anthropic_api_key"] = ""
|
| 1399 |
+
current_model_settings["google_api_key"] = ""
|
| 1400 |
+
current_model_settings["openai_api_key"] = ""
|
| 1401 |
+
current_model_settings["xai_api_key"] = ""
|
| 1402 |
+
current_model_settings["model"] = "gpt-oss" # Reset to default FREE model
|
| 1403 |
+
# Return: tavily, anthropic, google, openai, xai, model_value, status
|
| 1404 |
+
return "", "", "", "", "", "gpt-oss", "🗑️ All settings cleared (model reset to GPT-OSS)"
|
| 1405 |
+
|
| 1406 |
+
# Function to clear browser state
|
| 1407 |
+
def clear_browser_state():
|
| 1408 |
+
"""Clear all stored data from browser state"""
|
| 1409 |
+
print("[BrowserState] Clearing all stored data")
|
| 1410 |
+
return DEFAULT_BROWSER_STATE
|
| 1411 |
+
|
| 1412 |
+
# Combined clear handler - clears session AND browser state
|
| 1413 |
+
clear_keys_btn.click(
|
| 1414 |
+
clear_api_keys,
|
| 1415 |
+
[],
|
| 1416 |
+
[tavily_key, anthropic_key, google_key, openai_key, xai_key, model_dropdown, keys_status]
|
| 1417 |
+
).then(
|
| 1418 |
+
clear_browser_state,
|
| 1419 |
+
[],
|
| 1420 |
+
[browser_state]
|
| 1421 |
+
)
|
| 1422 |
+
|
| 1423 |
+
# === Load handler: Restore API keys and model from browser storage on page load ===
|
| 1424 |
+
def load_from_browser_state(stored_data):
|
| 1425 |
+
"""Load API keys and model from browser storage (runs on page load)
|
| 1426 |
+
|
| 1427 |
+
Returns: (tavily, anthropic, google, openai, xai, model, status_message)
|
| 1428 |
+
"""
|
| 1429 |
+
if not stored_data:
|
| 1430 |
+
print("[BrowserState] No stored data found")
|
| 1431 |
+
return "", "", "", "", "", "gpt-oss", "🔧 Ready - configure API keys to get started"
|
| 1432 |
+
|
| 1433 |
+
api_keys = stored_data.get("api_keys", {})
|
| 1434 |
+
model = stored_data.get("model", "gpt-oss")
|
| 1435 |
+
|
| 1436 |
+
# Extract individual keys
|
| 1437 |
+
tavily = api_keys.get("tavily", "")
|
| 1438 |
+
anthropic = api_keys.get("anthropic", "")
|
| 1439 |
+
google = api_keys.get("google", "")
|
| 1440 |
+
openai = api_keys.get("openai", "")
|
| 1441 |
+
xai = api_keys.get("xai", "")
|
| 1442 |
+
|
| 1443 |
+
# Check which keys were loaded
|
| 1444 |
+
loaded_keys = [k for k, v in api_keys.items() if v]
|
| 1445 |
+
|
| 1446 |
+
if loaded_keys or model != "gpt-oss":
|
| 1447 |
+
print(f"[BrowserState] Restoring: model={model}, keys={loaded_keys}")
|
| 1448 |
+
|
| 1449 |
+
# Also update the session state
|
| 1450 |
+
current_model_settings["model"] = model
|
| 1451 |
+
if tavily:
|
| 1452 |
+
current_model_settings["tavily_api_key"] = tavily
|
| 1453 |
+
if anthropic:
|
| 1454 |
+
current_model_settings["anthropic_api_key"] = anthropic
|
| 1455 |
+
if google:
|
| 1456 |
+
current_model_settings["google_api_key"] = google
|
| 1457 |
+
if openai:
|
| 1458 |
+
current_model_settings["openai_api_key"] = openai
|
| 1459 |
+
if xai:
|
| 1460 |
+
current_model_settings["xai_api_key"] = xai
|
| 1461 |
+
|
| 1462 |
+
# Build status message
|
| 1463 |
+
model_info = AVAILABLE_MODELS.get(model, {})
|
| 1464 |
+
status_parts = []
|
| 1465 |
+
status_parts.append(f"🤖 **Model:** {model_info.get('name', model)}")
|
| 1466 |
+
|
| 1467 |
+
if loaded_keys:
|
| 1468 |
+
status_parts.append(f"🔐 **Restored from browser:** {', '.join(loaded_keys)}")
|
| 1469 |
+
|
| 1470 |
+
if model == "gpt-oss":
|
| 1471 |
+
status_parts.append("🆓 *GPT-OSS model is FREE - no API key required!*")
|
| 1472 |
+
|
| 1473 |
+
status = "\n\n".join(status_parts)
|
| 1474 |
+
else:
|
| 1475 |
+
print("[BrowserState] No saved settings to restore")
|
| 1476 |
+
status = "🔧 Ready - configure API keys to get started"
|
| 1477 |
+
|
| 1478 |
+
return tavily, anthropic, google, openai, xai, model, status
|
| 1479 |
+
|
| 1480 |
+
# Trigger on page load - restore saved settings from browser storage
|
| 1481 |
+
demo.load(
|
| 1482 |
+
load_from_browser_state,
|
| 1483 |
+
inputs=[browser_state],
|
| 1484 |
+
outputs=[tavily_key, anthropic_key, google_key, openai_key, xai_key, model_dropdown, keys_status]
|
| 1485 |
+
)
|
| 1486 |
+
|
| 1487 |
+
# Launch the app
|
| 1488 |
+
if __name__ == "__main__":
|
| 1489 |
+
print("\n" + "="*60)
|
| 1490 |
+
print("🇪🇺 EU AI Act Compliance Agent - Gradio UI")
|
| 1491 |
+
print("="*60)
|
| 1492 |
+
print(f"\n📡 API Server: {API_URL}")
|
| 1493 |
+
print(f"✓ Status: {check_api_status()}")
|
| 1494 |
+
print(f"\n🚀 Starting Gradio interface...")
|
| 1495 |
+
print("="*60 + "\n")
|
| 1496 |
+
|
| 1497 |
+
demo.launch(
|
| 1498 |
+
server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"),
|
| 1499 |
+
server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")),
|
| 1500 |
+
share=os.getenv("GRADIO_SHARE", "false").lower() == "true",
|
| 1501 |
+
show_error=True,
|
| 1502 |
+
)
|
apps/eu-ai-act-agent/src/server.ts
ADDED
|
@@ -0,0 +1,1235 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* EU AI Act Compliance Agent Server
|
| 5 |
+
* Express API with Vercel AI SDK v5 agent
|
| 6 |
+
*
|
| 7 |
+
* Supports streaming text and tool calls
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
import express from "express";
|
| 11 |
+
import cors from "cors";
|
| 12 |
+
import { config } from "dotenv";
|
| 13 |
+
import { resolve, dirname } from "path";
|
| 14 |
+
import { fileURLToPath } from "url";
|
| 15 |
+
import { createAgent } from "./agent/index.js";
|
| 16 |
+
import {
|
| 17 |
+
discoverOrganization,
|
| 18 |
+
discoverAIServices,
|
| 19 |
+
assessCompliance,
|
| 20 |
+
type ApiKeys,
|
| 21 |
+
} from "@eu-ai-act/mcp-server";
|
| 22 |
+
|
| 23 |
+
// Load environment variables from project root
|
| 24 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 25 |
+
const __dirname = dirname(__filename);
|
| 26 |
+
config({ path: resolve(__dirname, "../../../.env") }); // Go up from src -> eu-ai-act-agent -> apps -> root
|
| 27 |
+
|
| 28 |
+
const app = express();
|
| 29 |
+
const PORT = process.env.PORT || 3001;
|
| 30 |
+
|
| 31 |
+
// Middleware
|
| 32 |
+
app.use(
|
| 33 |
+
cors({
|
| 34 |
+
origin: [
|
| 35 |
+
"http://localhost:7860",
|
| 36 |
+
"http://127.0.0.1:7860",
|
| 37 |
+
"http://localhost:3000",
|
| 38 |
+
],
|
| 39 |
+
credentials: true,
|
| 40 |
+
}),
|
| 41 |
+
);
|
| 42 |
+
app.use(express.json());
|
| 43 |
+
|
| 44 |
+
import { readFileSync, existsSync } from "fs";
|
| 45 |
+
|
| 46 |
+
// Health check
|
| 47 |
+
app.get("/health", (_req, res) => {
|
| 48 |
+
res.json({
|
| 49 |
+
status: "ok",
|
| 50 |
+
service: "EU AI Act Compliance Agent",
|
| 51 |
+
version: "0.1.0",
|
| 52 |
+
});
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
// MCP URL endpoint - returns the gradio.live URL for ChatGPT integration
|
| 56 |
+
app.get("/api/mcp-url", (_req, res) => {
|
| 57 |
+
try {
|
| 58 |
+
const mcpUrlFile = resolve(__dirname, ".mcp_url");
|
| 59 |
+
if (existsSync(mcpUrlFile)) {
|
| 60 |
+
const url = readFileSync(mcpUrlFile, "utf-8").trim();
|
| 61 |
+
res.json({ url, status: "ready" });
|
| 62 |
+
} else {
|
| 63 |
+
res.json({ url: null, status: "starting" });
|
| 64 |
+
}
|
| 65 |
+
} catch (error) {
|
| 66 |
+
res.json({ url: null, status: "error", error: String(error) });
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* Process stream events and write to response
|
| 72 |
+
* Returns set of tool names that were called
|
| 73 |
+
*
|
| 74 |
+
* Tracks "thinking" phases - text that appears before tool calls
|
| 75 |
+
* vs "response" text that appears after all tools complete
|
| 76 |
+
*/
|
| 77 |
+
async function processStreamEvents(
|
| 78 |
+
stream: AsyncIterable<any>,
|
| 79 |
+
res: express.Response,
|
| 80 |
+
): Promise<{
|
| 81 |
+
toolsCalled: Set<string>;
|
| 82 |
+
toolResults: Map<string, any>;
|
| 83 |
+
hasText: boolean;
|
| 84 |
+
}> {
|
| 85 |
+
const toolsCalled = new Set<string>();
|
| 86 |
+
const toolResults = new Map<string, any>();
|
| 87 |
+
let hasText = false;
|
| 88 |
+
|
| 89 |
+
// Track phase for thinking vs response text
|
| 90 |
+
let pendingToolCall = false; // True when we've seen a tool_call but not its result yet
|
| 91 |
+
let hasHadToolCalls = false; // True once we've seen at least one tool call
|
| 92 |
+
|
| 93 |
+
for await (const event of stream) {
|
| 94 |
+
// Log all non-text events for debugging
|
| 95 |
+
if (event.type !== "text-delta") {
|
| 96 |
+
console.log(
|
| 97 |
+
"Stream event:",
|
| 98 |
+
event.type,
|
| 99 |
+
JSON.stringify(event).substring(0, 200),
|
| 100 |
+
);
|
| 101 |
+
} else {
|
| 102 |
+
hasText = true;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
switch (event.type) {
|
| 106 |
+
// Handle reasoning/thinking tokens from Claude and GPT
|
| 107 |
+
// Claude uses "reasoning" with textDelta, OpenAI may use different formats
|
| 108 |
+
case "reasoning":
|
| 109 |
+
const reasoningText = (event as any).textDelta ?? "";
|
| 110 |
+
if (reasoningText) {
|
| 111 |
+
console.log("[THINKING]", reasoningText.substring(0, 100));
|
| 112 |
+
res.write(
|
| 113 |
+
`data: ${JSON.stringify({
|
| 114 |
+
type: "thinking",
|
| 115 |
+
content: reasoningText,
|
| 116 |
+
})}\n\n`,
|
| 117 |
+
);
|
| 118 |
+
}
|
| 119 |
+
break;
|
| 120 |
+
|
| 121 |
+
// Handle reasoning signature (Claude's thinking summary)
|
| 122 |
+
case "reasoning-signature":
|
| 123 |
+
const signatureText = (event as any).signature ?? "";
|
| 124 |
+
if (signatureText) {
|
| 125 |
+
console.log("[THINKING SIGNATURE]", signatureText.substring(0, 100));
|
| 126 |
+
res.write(
|
| 127 |
+
`data: ${JSON.stringify({
|
| 128 |
+
type: "thinking",
|
| 129 |
+
content: `[Reasoning Summary] ${signatureText}`,
|
| 130 |
+
})}\n\n`,
|
| 131 |
+
);
|
| 132 |
+
}
|
| 133 |
+
break;
|
| 134 |
+
|
| 135 |
+
// Handle redacted reasoning (when thinking is hidden)
|
| 136 |
+
case "redacted-reasoning":
|
| 137 |
+
console.log("[REDACTED REASONING]");
|
| 138 |
+
res.write(
|
| 139 |
+
`data: ${JSON.stringify({
|
| 140 |
+
type: "thinking",
|
| 141 |
+
content: "[Model is reasoning internally...]",
|
| 142 |
+
})}\n\n`,
|
| 143 |
+
);
|
| 144 |
+
break;
|
| 145 |
+
|
| 146 |
+
case "text-delta":
|
| 147 |
+
const textContent =
|
| 148 |
+
(event as any).textDelta ??
|
| 149 |
+
(event as any).delta ??
|
| 150 |
+
(event as any).text ??
|
| 151 |
+
"";
|
| 152 |
+
|
| 153 |
+
// Determine if this is thinking or response text
|
| 154 |
+
// Text before any tool call = thinking
|
| 155 |
+
// Text between tool result and next tool call = thinking
|
| 156 |
+
// Text after last tool result with no more tool calls = response (we can't know this yet, so we mark it as potential_response)
|
| 157 |
+
const textPhase =
|
| 158 |
+
hasHadToolCalls && !pendingToolCall
|
| 159 |
+
? "potential_response"
|
| 160 |
+
: "thinking";
|
| 161 |
+
|
| 162 |
+
res.write(
|
| 163 |
+
`data: ${JSON.stringify({
|
| 164 |
+
type: "text",
|
| 165 |
+
content: textContent,
|
| 166 |
+
phase: textPhase,
|
| 167 |
+
hasHadToolCalls,
|
| 168 |
+
})}\n\n`,
|
| 169 |
+
);
|
| 170 |
+
break;
|
| 171 |
+
|
| 172 |
+
case "tool-call":
|
| 173 |
+
console.log("TOOL CALL:", event.toolName);
|
| 174 |
+
hasHadToolCalls = true;
|
| 175 |
+
pendingToolCall = true;
|
| 176 |
+
|
| 177 |
+
toolsCalled.add(event.toolName);
|
| 178 |
+
const toolArgs = (event as any).args ?? (event as any).input ?? {};
|
| 179 |
+
res.write(
|
| 180 |
+
`data: ${JSON.stringify({
|
| 181 |
+
type: "tool_call",
|
| 182 |
+
toolName: event.toolName,
|
| 183 |
+
toolCallId: event.toolCallId,
|
| 184 |
+
args: toolArgs,
|
| 185 |
+
})}\n\n`,
|
| 186 |
+
);
|
| 187 |
+
break;
|
| 188 |
+
|
| 189 |
+
case "tool-result":
|
| 190 |
+
console.log("TOOL RESULT:", event.toolName);
|
| 191 |
+
pendingToolCall = false;
|
| 192 |
+
|
| 193 |
+
const toolOutput = (event as any).output;
|
| 194 |
+
const directResult = (event as any).result;
|
| 195 |
+
let parsedResult = null;
|
| 196 |
+
|
| 197 |
+
if (directResult) {
|
| 198 |
+
parsedResult = directResult;
|
| 199 |
+
} else if (toolOutput?.content?.[0]?.text) {
|
| 200 |
+
try {
|
| 201 |
+
parsedResult = JSON.parse(toolOutput.content[0].text);
|
| 202 |
+
} catch {
|
| 203 |
+
parsedResult = toolOutput.content[0].text;
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
toolResults.set(event.toolName, parsedResult);
|
| 208 |
+
|
| 209 |
+
res.write(
|
| 210 |
+
`data: ${JSON.stringify({
|
| 211 |
+
type: "tool_result",
|
| 212 |
+
toolName: event.toolName,
|
| 213 |
+
toolCallId: event.toolCallId,
|
| 214 |
+
result: parsedResult,
|
| 215 |
+
})}\n\n`,
|
| 216 |
+
);
|
| 217 |
+
break;
|
| 218 |
+
|
| 219 |
+
case "step-finish":
|
| 220 |
+
// When a step finishes, if we had tool calls and there's no pending tool,
|
| 221 |
+
// the next text will be response (or thinking for next tool)
|
| 222 |
+
res.write(
|
| 223 |
+
`data: ${JSON.stringify({
|
| 224 |
+
type: "step_finish",
|
| 225 |
+
finishReason: event.finishReason,
|
| 226 |
+
hasHadToolCalls,
|
| 227 |
+
})}\n\n`,
|
| 228 |
+
);
|
| 229 |
+
break;
|
| 230 |
+
|
| 231 |
+
case "error":
|
| 232 |
+
res.write(
|
| 233 |
+
`data: ${JSON.stringify({
|
| 234 |
+
type: "error",
|
| 235 |
+
error: String(event.error),
|
| 236 |
+
})}\n\n`,
|
| 237 |
+
);
|
| 238 |
+
break;
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
return { toolsCalled, toolResults, hasText };
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
// Main chat endpoint with full streaming support
|
| 246 |
+
app.post("/api/chat", async (req, res) => {
|
| 247 |
+
try {
|
| 248 |
+
const { message, history = [] } = req.body;
|
| 249 |
+
|
| 250 |
+
if (!message || typeof message !== "string") {
|
| 251 |
+
return res.status(400).json({ error: "Message is required" });
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
// Read model selection and API keys from headers (set by Gradio UI)
|
| 255 |
+
// IMPORTANT: API keys are ONLY from user input via Gradio UI - NEVER from env vars!
|
| 256 |
+
const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss";
|
| 257 |
+
|
| 258 |
+
// API keys from Gradio UI (stored in user's cookies)
|
| 259 |
+
const apiKeys = {
|
| 260 |
+
modalEndpointUrl:
|
| 261 |
+
(req.headers["x-modal-endpoint-url"] as string) || undefined,
|
| 262 |
+
openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined,
|
| 263 |
+
xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined,
|
| 264 |
+
anthropicApiKey:
|
| 265 |
+
(req.headers["x-anthropic-api-key"] as string) || undefined,
|
| 266 |
+
googleApiKey: (req.headers["x-google-api-key"] as string) || undefined,
|
| 267 |
+
};
|
| 268 |
+
|
| 269 |
+
// Tavily API key (optional - for web research)
|
| 270 |
+
const tavilyApiKey =
|
| 271 |
+
(req.headers["x-tavily-api-key"] as string) || undefined;
|
| 272 |
+
|
| 273 |
+
console.log(
|
| 274 |
+
`[API] Model: ${modelName}, API keys provided: ${
|
| 275 |
+
Object.entries(apiKeys)
|
| 276 |
+
.filter(([_, v]) => v)
|
| 277 |
+
.map(([k]) => k)
|
| 278 |
+
.join(", ") || "none (GPT-OSS is FREE)"
|
| 279 |
+
}`,
|
| 280 |
+
);
|
| 281 |
+
if (tavilyApiKey) {
|
| 282 |
+
console.log(
|
| 283 |
+
`[API] Tavily API key provided: ${tavilyApiKey.substring(0, 10)}...`,
|
| 284 |
+
);
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// For GPT-OSS, use default Modal endpoint if not provided
|
| 288 |
+
if (modelName === "gpt-oss" && !apiKeys.modalEndpointUrl) {
|
| 289 |
+
apiKeys.modalEndpointUrl =
|
| 290 |
+
"https://vasilis--gpt-oss-vllm-inference-serve.modal.run";
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// Set headers for streaming
|
| 294 |
+
res.setHeader("Content-Type", "text/event-stream");
|
| 295 |
+
res.setHeader("Cache-Control", "no-cache");
|
| 296 |
+
res.setHeader("Connection", "keep-alive");
|
| 297 |
+
res.setHeader("X-Accel-Buffering", "no");
|
| 298 |
+
|
| 299 |
+
// Send user message confirmation immediately
|
| 300 |
+
res.write(
|
| 301 |
+
`data: ${JSON.stringify({ type: "user_message", content: message })}\n\n`,
|
| 302 |
+
);
|
| 303 |
+
|
| 304 |
+
// Create agent instance with model, API keys, and Tavily key from Gradio UI
|
| 305 |
+
const agent = createAgent({ modelName, apiKeys, tavilyApiKey });
|
| 306 |
+
|
| 307 |
+
// Convert history to messages format
|
| 308 |
+
let messages = history.map((msg: any) => ({
|
| 309 |
+
role: msg.role,
|
| 310 |
+
content: msg.content,
|
| 311 |
+
}));
|
| 312 |
+
|
| 313 |
+
// For GPT-OSS (smaller model), implement token-based trimming to avoid context overflow
|
| 314 |
+
// GPT-OSS 20B has 16K context window
|
| 315 |
+
const isGptOss = modelName === "gpt-oss";
|
| 316 |
+
if (isGptOss && messages.length > 0) {
|
| 317 |
+
// Token budget calculation for GPT-OSS:
|
| 318 |
+
// - Context window: 16,384 tokens
|
| 319 |
+
// - System prompt: ~5,000 tokens (expanded with Article 6 guidelines)
|
| 320 |
+
// - Tool definitions: ~1,500 tokens
|
| 321 |
+
// - Output buffer (maxOutputTokens): 8,000 tokens (for comprehensive reports)
|
| 322 |
+
// - Safety margin: 300 tokens
|
| 323 |
+
// Available for history: ~1,584 tokens (~6 short messages)
|
| 324 |
+
const GPT_OSS_CONTEXT_WINDOW = 16384;
|
| 325 |
+
const SYSTEM_PROMPT_TOKENS = 5000;
|
| 326 |
+
const TOOL_DEFINITIONS_TOKENS = 1500;
|
| 327 |
+
const OUTPUT_BUFFER_TOKENS = 8000;
|
| 328 |
+
const SAFETY_MARGIN_TOKENS = 300;
|
| 329 |
+
const MAX_HISTORY_TOKENS =
|
| 330 |
+
GPT_OSS_CONTEXT_WINDOW -
|
| 331 |
+
SYSTEM_PROMPT_TOKENS -
|
| 332 |
+
TOOL_DEFINITIONS_TOKENS -
|
| 333 |
+
OUTPUT_BUFFER_TOKENS -
|
| 334 |
+
SAFETY_MARGIN_TOKENS;
|
| 335 |
+
|
| 336 |
+
// Estimate tokens: ~4 characters per token (conservative estimate for English text)
|
| 337 |
+
const estimateTokens = (text: string): number =>
|
| 338 |
+
Math.ceil((text || "").length / 4);
|
| 339 |
+
|
| 340 |
+
// First, truncate very long individual messages (e.g., tool results)
|
| 341 |
+
const MAX_MESSAGE_CHARS = 1000; // ~250 tokens per message max (tight budget)
|
| 342 |
+
messages = messages.map((msg: any) => {
|
| 343 |
+
if (msg.content && msg.content.length > MAX_MESSAGE_CHARS) {
|
| 344 |
+
console.log(
|
| 345 |
+
`[API] GPT-OSS: Truncating long ${msg.role} message (${msg.content.length} chars → ${MAX_MESSAGE_CHARS} chars)`,
|
| 346 |
+
);
|
| 347 |
+
return {
|
| 348 |
+
...msg,
|
| 349 |
+
content:
|
| 350 |
+
msg.content.substring(0, MAX_MESSAGE_CHARS) +
|
| 351 |
+
"\n\n[...truncated for context limits...]",
|
| 352 |
+
};
|
| 353 |
+
}
|
| 354 |
+
return msg;
|
| 355 |
+
});
|
| 356 |
+
|
| 357 |
+
// Calculate total history tokens
|
| 358 |
+
let totalHistoryTokens = messages.reduce(
|
| 359 |
+
(sum: number, msg: any) => sum + estimateTokens(msg.content),
|
| 360 |
+
0,
|
| 361 |
+
);
|
| 362 |
+
|
| 363 |
+
// Also count tokens for the current message we're about to add
|
| 364 |
+
const currentMessageTokens = estimateTokens(message);
|
| 365 |
+
totalHistoryTokens += currentMessageTokens;
|
| 366 |
+
|
| 367 |
+
console.log(
|
| 368 |
+
`[API] GPT-OSS: History tokens estimate: ${totalHistoryTokens} / ${MAX_HISTORY_TOKENS} max (${messages.length} messages)`,
|
| 369 |
+
);
|
| 370 |
+
|
| 371 |
+
// If over budget, trim oldest messages first
|
| 372 |
+
while (totalHistoryTokens > MAX_HISTORY_TOKENS && messages.length > 0) {
|
| 373 |
+
const removedMsg = messages.shift();
|
| 374 |
+
const removedTokens = estimateTokens(removedMsg?.content || "");
|
| 375 |
+
totalHistoryTokens -= removedTokens;
|
| 376 |
+
console.log(
|
| 377 |
+
`[API] GPT-OSS: Trimmed oldest ${removedMsg?.role} message (${removedTokens} tokens). New total: ${totalHistoryTokens}`,
|
| 378 |
+
);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
// Hard limit: max 4 messages (2 turns) due to limited history budget
|
| 382 |
+
// With 8000 output tokens, we need to prioritize output over history
|
| 383 |
+
const MAX_HISTORY_MESSAGES = 4;
|
| 384 |
+
if (messages.length > MAX_HISTORY_MESSAGES) {
|
| 385 |
+
const trimCount = messages.length - MAX_HISTORY_MESSAGES;
|
| 386 |
+
console.log(
|
| 387 |
+
`[API] GPT-OSS: Trimming ${trimCount} messages to stay under ${MAX_HISTORY_MESSAGES} message limit`,
|
| 388 |
+
);
|
| 389 |
+
messages = messages.slice(-MAX_HISTORY_MESSAGES);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
console.log(
|
| 393 |
+
`[API] GPT-OSS: Final history: ${messages.length} messages, ~${totalHistoryTokens} tokens`,
|
| 394 |
+
);
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
// Add current message
|
| 398 |
+
messages.push({
|
| 399 |
+
role: "user",
|
| 400 |
+
content: message,
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
console.log(
|
| 404 |
+
`Starting stream for message: ${message} (history: ${messages.length - 1} messages)`,
|
| 405 |
+
);
|
| 406 |
+
|
| 407 |
+
// First pass - stream the response
|
| 408 |
+
const result = await agent.streamText({ messages });
|
| 409 |
+
let { toolsCalled, toolResults, hasText } = await processStreamEvents(
|
| 410 |
+
result.fullStream,
|
| 411 |
+
res,
|
| 412 |
+
);
|
| 413 |
+
|
| 414 |
+
console.log("First pass complete. Tools called:", [...toolsCalled]);
|
| 415 |
+
|
| 416 |
+
// Check if this looks like an organization analysis that needs more tool calls
|
| 417 |
+
const hasOrgDiscovery = toolsCalled.has("discover_organization");
|
| 418 |
+
const hasAIServicesDiscovery = toolsCalled.has("discover_ai_services");
|
| 419 |
+
|
| 420 |
+
// Need AI services discovery if we have org but not AI services
|
| 421 |
+
const needsAIServicesDiscovery = hasOrgDiscovery && !hasAIServicesDiscovery;
|
| 422 |
+
|
| 423 |
+
// If discover_ai_services wasn't called but discover_organization was, make a follow-up request for AI services
|
| 424 |
+
if (needsAIServicesDiscovery && !hasText) {
|
| 425 |
+
console.log(
|
| 426 |
+
"⚠️ discover_organization called but discover_ai_services missing. Making follow-up request...",
|
| 427 |
+
);
|
| 428 |
+
|
| 429 |
+
const orgContext = toolResults.get("discover_organization");
|
| 430 |
+
|
| 431 |
+
// List which tools were already called to prevent duplicates
|
| 432 |
+
const alreadyCalled = [...toolsCalled].join(", ");
|
| 433 |
+
|
| 434 |
+
const aiServicesFollowUp = `
|
| 435 |
+
You called discover_organization but SKIPPED discover_ai_services.
|
| 436 |
+
|
| 437 |
+
## TOOLS ALREADY CALLED (DO NOT CALL AGAIN): ${alreadyCalled}
|
| 438 |
+
|
| 439 |
+
## CRITICAL: Call discover_ai_services NOW (ONLY ONCE)
|
| 440 |
+
|
| 441 |
+
Organization context is ready:
|
| 442 |
+
- Name: ${orgContext?.organization?.name || "Unknown"}
|
| 443 |
+
- Sector: ${orgContext?.organization?.sector || "Unknown"}
|
| 444 |
+
|
| 445 |
+
Call discover_ai_services ONCE with:
|
| 446 |
+
- organizationContext: Use the organization profile from discover_organization
|
| 447 |
+
- systemNames: Extract any AI systems mentioned in the user's original query
|
| 448 |
+
|
| 449 |
+
After discover_ai_services completes, call assess_compliance ONCE with BOTH contexts.
|
| 450 |
+
|
| 451 |
+
⚠️ EACH TOOL MUST BE CALLED EXACTLY ONCE - NO DUPLICATES!`;
|
| 452 |
+
|
| 453 |
+
const aiServicesMessages = [
|
| 454 |
+
...messages,
|
| 455 |
+
{
|
| 456 |
+
role: "assistant",
|
| 457 |
+
content: `I have gathered the organization profile for ${orgContext?.organization?.name || "the organization"}. Now I will discover their AI systems.`,
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
role: "user",
|
| 461 |
+
content: aiServicesFollowUp,
|
| 462 |
+
},
|
| 463 |
+
];
|
| 464 |
+
|
| 465 |
+
console.log("Making follow-up request to call discover_ai_services...");
|
| 466 |
+
|
| 467 |
+
const aiServicesResult = await agent.streamText({
|
| 468 |
+
messages: aiServicesMessages,
|
| 469 |
+
});
|
| 470 |
+
const aiServicesData = await processStreamEvents(
|
| 471 |
+
aiServicesResult.fullStream,
|
| 472 |
+
res,
|
| 473 |
+
);
|
| 474 |
+
|
| 475 |
+
// Update tracking with follow-up results (only add new tools)
|
| 476 |
+
for (const [tool, result] of aiServicesData.toolResults) {
|
| 477 |
+
if (!toolResults.has(tool)) {
|
| 478 |
+
toolResults.set(tool, result);
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
for (const tool of aiServicesData.toolsCalled) {
|
| 482 |
+
toolsCalled.add(tool);
|
| 483 |
+
}
|
| 484 |
+
hasText = hasText || aiServicesData.hasText;
|
| 485 |
+
|
| 486 |
+
// Update needsAssessment check
|
| 487 |
+
const nowHasAssessment = toolsCalled.has("assess_compliance");
|
| 488 |
+
if (!nowHasAssessment) {
|
| 489 |
+
console.log(
|
| 490 |
+
"discover_ai_services called but assess_compliance still missing...",
|
| 491 |
+
);
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
// Recalculate if we still need assessment after AI services discovery
|
| 496 |
+
const stillNeedsAssessment =
|
| 497 |
+
(toolsCalled.has("discover_organization") ||
|
| 498 |
+
toolsCalled.has("discover_ai_services")) &&
|
| 499 |
+
!toolsCalled.has("assess_compliance");
|
| 500 |
+
|
| 501 |
+
// If organization/AI services tools were called but assess_compliance wasn't, make a follow-up request
|
| 502 |
+
if (stillNeedsAssessment && !hasText) {
|
| 503 |
+
console.log(
|
| 504 |
+
"⚠️ Organization/AI tools called but assess_compliance missing. Making follow-up request...",
|
| 505 |
+
);
|
| 506 |
+
|
| 507 |
+
// Build context from tool results - these are the FULL results from the previous tools
|
| 508 |
+
const orgContext = toolResults.get("discover_organization");
|
| 509 |
+
const aiServicesContext = toolResults.get("discover_ai_services");
|
| 510 |
+
|
| 511 |
+
// Create a follow-up message that includes the COMPLETE tool results as JSON
|
| 512 |
+
// This ensures the model has all the data needed to call assess_compliance correctly
|
| 513 |
+
const alreadyCalledTools = [...toolsCalled].join(", ");
|
| 514 |
+
|
| 515 |
+
const fullContextMessage = `
|
| 516 |
+
I have received the complete results from the previous tools. Now I need you to call assess_compliance with the FULL context.
|
| 517 |
+
|
| 518 |
+
## ⚠️ TOOLS ALREADY CALLED (DO NOT CALL AGAIN): ${alreadyCalledTools}
|
| 519 |
+
|
| 520 |
+
## COMPLETE ORGANIZATION CONTEXT (from discover_organization):
|
| 521 |
+
\`\`\`json
|
| 522 |
+
${JSON.stringify(orgContext, null, 2)}
|
| 523 |
+
\`\`\`
|
| 524 |
+
|
| 525 |
+
## COMPLETE AI SERVICES CONTEXT (from discover_ai_services):
|
| 526 |
+
\`\`\`json
|
| 527 |
+
${JSON.stringify(aiServicesContext, null, 2)}
|
| 528 |
+
\`\`\`
|
| 529 |
+
|
| 530 |
+
## INSTRUCTION:
|
| 531 |
+
Call assess_compliance ONCE with these EXACT parameters:
|
| 532 |
+
- organizationContext: Pass the COMPLETE organization context JSON shown above (not a summary)
|
| 533 |
+
- aiServicesContext: Pass the COMPLETE AI services context JSON shown above (not a summary)
|
| 534 |
+
- generateDocumentation: true
|
| 535 |
+
|
| 536 |
+
⚠️ CALL assess_compliance EXACTLY ONCE - DO NOT call any tool that was already called!
|
| 537 |
+
After assess_compliance returns, provide a human-readable summary of the compliance assessment.`;
|
| 538 |
+
|
| 539 |
+
const followUpMessages = [
|
| 540 |
+
...messages,
|
| 541 |
+
{
|
| 542 |
+
role: "assistant",
|
| 543 |
+
content: `I have gathered the organization profile for ${orgContext?.organization?.name || "the organization"} and discovered ${aiServicesContext?.systems?.length || 0} AI systems. Now I will call assess_compliance with the complete context to generate the full compliance report.`,
|
| 544 |
+
},
|
| 545 |
+
{
|
| 546 |
+
role: "user",
|
| 547 |
+
content: fullContextMessage,
|
| 548 |
+
},
|
| 549 |
+
];
|
| 550 |
+
|
| 551 |
+
console.log(
|
| 552 |
+
"Making follow-up request to call assess_compliance with FULL context...",
|
| 553 |
+
);
|
| 554 |
+
console.log(
|
| 555 |
+
`Organization context size: ${JSON.stringify(orgContext || {}).length} chars`,
|
| 556 |
+
);
|
| 557 |
+
console.log(
|
| 558 |
+
`AI services context size: ${JSON.stringify(aiServicesContext || {}).length} chars`,
|
| 559 |
+
);
|
| 560 |
+
|
| 561 |
+
const followUpResult = await agent.streamText({
|
| 562 |
+
messages: followUpMessages,
|
| 563 |
+
});
|
| 564 |
+
const followUpData = await processStreamEvents(
|
| 565 |
+
followUpResult.fullStream,
|
| 566 |
+
res,
|
| 567 |
+
);
|
| 568 |
+
|
| 569 |
+
// Update tracking with follow-up results
|
| 570 |
+
for (const [tool, result] of followUpData.toolResults) {
|
| 571 |
+
toolResults.set(tool, result);
|
| 572 |
+
}
|
| 573 |
+
for (const tool of followUpData.toolsCalled) {
|
| 574 |
+
toolsCalled.add(tool);
|
| 575 |
+
}
|
| 576 |
+
// Update hasText from follow-up
|
| 577 |
+
hasText = hasText || followUpData.hasText;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
// Final check for text
|
| 581 |
+
const hasTextNow = hasText;
|
| 582 |
+
|
| 583 |
+
// If still no text response, generate a comprehensive summary based on available tool results
|
| 584 |
+
if (!hasTextNow && toolResults.size > 0) {
|
| 585 |
+
console.log(
|
| 586 |
+
"Generating comprehensive compliance report from tool results...",
|
| 587 |
+
);
|
| 588 |
+
|
| 589 |
+
// Create a summary from available data
|
| 590 |
+
const orgData = toolResults.get("discover_organization");
|
| 591 |
+
const aiData = toolResults.get("discover_ai_services");
|
| 592 |
+
const assessData = toolResults.get("assess_compliance");
|
| 593 |
+
|
| 594 |
+
let summary = "\n\n---\n\n";
|
| 595 |
+
|
| 596 |
+
// ================== HEADER ==================
|
| 597 |
+
const orgName = orgData?.organization?.name || "Organization";
|
| 598 |
+
summary += `# 🇪🇺 EU AI Act Compliance Report\n`;
|
| 599 |
+
summary += `## ${orgName}\n\n`;
|
| 600 |
+
summary += `*Assessment Date: ${new Date().toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" })}*\n\n`;
|
| 601 |
+
summary += `---\n\n`;
|
| 602 |
+
|
| 603 |
+
// ================== ORGANIZATION PROFILE ==================
|
| 604 |
+
if (orgData?.organization) {
|
| 605 |
+
const org = orgData.organization;
|
| 606 |
+
summary += `## 🏢 Organization Profile\n\n`;
|
| 607 |
+
summary += `| Attribute | Value |\n`;
|
| 608 |
+
summary += `|-----------|-------|\n`;
|
| 609 |
+
summary += `| **Name** | ${org.name} |\n`;
|
| 610 |
+
summary += `| **Sector** | ${org.sector} |\n`;
|
| 611 |
+
summary += `| **Size** | ${org.size} |\n`;
|
| 612 |
+
summary += `| **Headquarters** | ${org.headquarters?.city || "Unknown"}, ${org.headquarters?.country || "Unknown"} |\n`;
|
| 613 |
+
summary += `| **EU Presence** | ${org.euPresence ? "✅ Yes" : "❌ No"} |\n`;
|
| 614 |
+
summary += `| **AI Maturity Level** | ${org.aiMaturityLevel} |\n`;
|
| 615 |
+
summary += `| **Primary Role** | ${org.primaryRole} (per Article 3) |\n`;
|
| 616 |
+
summary += `| **Jurisdictions** | ${org.jurisdiction?.join(", ") || "Unknown"} |\n`;
|
| 617 |
+
if (org.contact?.website) {
|
| 618 |
+
summary += `| **Website** | ${org.contact.website} |\n`;
|
| 619 |
+
}
|
| 620 |
+
summary += `\n`;
|
| 621 |
+
|
| 622 |
+
// Regulatory Context
|
| 623 |
+
if (orgData.regulatoryContext) {
|
| 624 |
+
const reg = orgData.regulatoryContext;
|
| 625 |
+
summary += `### 📋 Regulatory Context\n\n`;
|
| 626 |
+
summary += `- **Quality Management System (Article 17):** ${reg.hasQualityManagementSystem ? "✅ Implemented" : "⚠️ Not Implemented"}\n`;
|
| 627 |
+
summary += `- **Risk Management System (Article 9):** ${reg.hasRiskManagementSystem ? "✅ Implemented" : "⚠️ Not Implemented"}\n`;
|
| 628 |
+
if (reg.existingCertifications?.length > 0) {
|
| 629 |
+
summary += `- **Certifications:** ${reg.existingCertifications.join(", ")}\n`;
|
| 630 |
+
}
|
| 631 |
+
if (!org.euPresence) {
|
| 632 |
+
summary += `- **Authorized Representative (Article 22):** ${reg.hasAuthorizedRepresentative ? "✅ Appointed" : "⚠️ Required for non-EU entities"}\n`;
|
| 633 |
+
}
|
| 634 |
+
summary += `\n`;
|
| 635 |
+
}
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// ================== AI SYSTEMS ANALYSIS ==================
|
| 639 |
+
if (aiData?.systems && aiData.systems.length > 0) {
|
| 640 |
+
summary += `## 🤖 AI Systems Analysis\n\n`;
|
| 641 |
+
|
| 642 |
+
// Risk Summary Table
|
| 643 |
+
const riskSummary = aiData.riskSummary;
|
| 644 |
+
summary += `### Risk Distribution\n\n`;
|
| 645 |
+
summary += `| Risk Category | Count | Status |\n`;
|
| 646 |
+
summary += `|---------------|-------|--------|\n`;
|
| 647 |
+
if (riskSummary.unacceptableRiskCount > 0) {
|
| 648 |
+
summary += `| 🔴 **Unacceptable Risk** | ${riskSummary.unacceptableRiskCount} | ⛔ PROHIBITED |\n`;
|
| 649 |
+
}
|
| 650 |
+
summary += `| 🟠 **High Risk** | ${riskSummary.highRiskCount} | Requires Conformity Assessment |\n`;
|
| 651 |
+
summary += `| 🟡 **Limited Risk** | ${riskSummary.limitedRiskCount} | Transparency Obligations |\n`;
|
| 652 |
+
summary += `| 🟢 **Minimal Risk** | ${riskSummary.minimalRiskCount} | No Specific Obligations |\n`;
|
| 653 |
+
summary += `| **Total** | ${riskSummary.totalCount} | |\n\n`;
|
| 654 |
+
|
| 655 |
+
// Detailed System Analysis
|
| 656 |
+
summary += `### Detailed System Analysis\n\n`;
|
| 657 |
+
|
| 658 |
+
for (const sys of aiData.systems) {
|
| 659 |
+
const riskEmoji =
|
| 660 |
+
sys.riskClassification.category === "High"
|
| 661 |
+
? "🟠"
|
| 662 |
+
: sys.riskClassification.category === "Limited"
|
| 663 |
+
? "🟡"
|
| 664 |
+
: sys.riskClassification.category === "Unacceptable"
|
| 665 |
+
? "🔴"
|
| 666 |
+
: "🟢";
|
| 667 |
+
|
| 668 |
+
summary += `#### ${riskEmoji} ${sys.system.name}\n\n`;
|
| 669 |
+
summary += `**Risk Classification:** ${sys.riskClassification.category} Risk (Score: ${sys.riskClassification.riskScore}/100)\n\n`;
|
| 670 |
+
|
| 671 |
+
// Purpose and Description
|
| 672 |
+
summary += `**Intended Purpose:** ${sys.system.intendedPurpose}\n\n`;
|
| 673 |
+
|
| 674 |
+
// Classification Reasoning
|
| 675 |
+
if (sys.riskClassification.justification) {
|
| 676 |
+
summary += `**Classification Reasoning:**\n> ${sys.riskClassification.justification}\n\n`;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
// Annex III Category for High-Risk
|
| 680 |
+
if (
|
| 681 |
+
sys.riskClassification.category === "High" &&
|
| 682 |
+
sys.riskClassification.annexIIICategory
|
| 683 |
+
) {
|
| 684 |
+
summary += `**Annex III Category:** ${sys.riskClassification.annexIIICategory}\n\n`;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// Technical Details
|
| 688 |
+
summary += `**Technical Details:**\n`;
|
| 689 |
+
summary += `- AI Technology: ${sys.technicalDetails.aiTechnology?.join(", ") || "Not specified"}\n`;
|
| 690 |
+
summary += `- Data Processed: ${sys.technicalDetails.dataProcessed?.join(", ") || "Not specified"}\n`;
|
| 691 |
+
summary += `- Deployment: ${sys.technicalDetails.deploymentModel || "Not specified"}\n`;
|
| 692 |
+
summary += `- Human Oversight: ${sys.technicalDetails.humanOversight?.enabled ? "✅ Enabled" : "⚠️ Not enabled"}\n`;
|
| 693 |
+
if (sys.technicalDetails.humanOversight?.description) {
|
| 694 |
+
summary += ` - *${sys.technicalDetails.humanOversight.description}*\n`;
|
| 695 |
+
}
|
| 696 |
+
summary += `\n`;
|
| 697 |
+
|
| 698 |
+
// Compliance Status
|
| 699 |
+
summary += `**Compliance Status:**\n`;
|
| 700 |
+
summary += `- Conformity Assessment: ${sys.complianceStatus.conformityAssessmentStatus}\n`;
|
| 701 |
+
summary += `- Technical Documentation: ${sys.complianceStatus.hasTechnicalDocumentation ? "✅" : "❌"}\n`;
|
| 702 |
+
summary += `- EU Database Registration: ${sys.complianceStatus.registeredInEUDatabase ? "✅" : "❌"}\n`;
|
| 703 |
+
summary += `- Post-Market Monitoring: ${sys.complianceStatus.hasPostMarketMonitoring ? "✅" : "❌"}\n`;
|
| 704 |
+
if (sys.complianceStatus.complianceDeadline) {
|
| 705 |
+
summary += `- **Deadline:** ${sys.complianceStatus.complianceDeadline}\n`;
|
| 706 |
+
}
|
| 707 |
+
if (sys.complianceStatus.estimatedComplianceEffort) {
|
| 708 |
+
summary += `- **Estimated Effort:** ${sys.complianceStatus.estimatedComplianceEffort}\n`;
|
| 709 |
+
}
|
| 710 |
+
summary += `\n`;
|
| 711 |
+
|
| 712 |
+
// Regulatory References
|
| 713 |
+
if (sys.riskClassification.regulatoryReferences?.length > 0) {
|
| 714 |
+
summary += `**Applicable Articles:** ${sys.riskClassification.regulatoryReferences.join(", ")}\n\n`;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
summary += `---\n\n`;
|
| 718 |
+
}
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
// ================== COMPLIANCE ASSESSMENT ==================
|
| 722 |
+
if (assessData?.assessment) {
|
| 723 |
+
const assess = assessData.assessment;
|
| 724 |
+
|
| 725 |
+
summary += `## 📊 Compliance Assessment Results\n\n`;
|
| 726 |
+
|
| 727 |
+
// Score Card
|
| 728 |
+
const scoreEmoji =
|
| 729 |
+
assess.overallScore >= 80
|
| 730 |
+
? "🟢"
|
| 731 |
+
: assess.overallScore >= 60
|
| 732 |
+
? "🟡"
|
| 733 |
+
: assess.overallScore >= 40
|
| 734 |
+
? "🟠"
|
| 735 |
+
: "🔴";
|
| 736 |
+
summary += `### Overall Score: ${scoreEmoji} ${assess.overallScore}/100\n`;
|
| 737 |
+
summary += `**Risk Level:** ${assess.riskLevel}\n\n`;
|
| 738 |
+
|
| 739 |
+
// Compliance by Article
|
| 740 |
+
if (
|
| 741 |
+
assess.complianceByArticle &&
|
| 742 |
+
Object.keys(assess.complianceByArticle).length > 0
|
| 743 |
+
) {
|
| 744 |
+
summary += `### Compliance by EU AI Act Article\n\n`;
|
| 745 |
+
summary += `| Article | Status | Issues |\n`;
|
| 746 |
+
summary += `|---------|--------|--------|\n`;
|
| 747 |
+
for (const [article, statusData] of Object.entries(
|
| 748 |
+
assess.complianceByArticle,
|
| 749 |
+
)) {
|
| 750 |
+
const articleStatus = statusData as {
|
| 751 |
+
compliant: boolean;
|
| 752 |
+
gaps?: string[];
|
| 753 |
+
};
|
| 754 |
+
const icon = articleStatus.compliant ? "✅" : "❌";
|
| 755 |
+
const issues = articleStatus.gaps?.length
|
| 756 |
+
? articleStatus.gaps.length + " gap(s)"
|
| 757 |
+
: "None";
|
| 758 |
+
summary += `| ${article} | ${icon} | ${issues} |\n`;
|
| 759 |
+
}
|
| 760 |
+
summary += `\n`;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
// Gap Analysis
|
| 764 |
+
if (assess.gaps && assess.gaps.length > 0) {
|
| 765 |
+
summary += `### 🔍 Gap Analysis\n\n`;
|
| 766 |
+
|
| 767 |
+
// Group by severity
|
| 768 |
+
const critical = assess.gaps.filter(
|
| 769 |
+
(g: any) => g.severity === "CRITICAL",
|
| 770 |
+
);
|
| 771 |
+
const high = assess.gaps.filter((g: any) => g.severity === "HIGH");
|
| 772 |
+
const medium = assess.gaps.filter(
|
| 773 |
+
(g: any) => g.severity === "MEDIUM",
|
| 774 |
+
);
|
| 775 |
+
const low = assess.gaps.filter((g: any) => g.severity === "LOW");
|
| 776 |
+
|
| 777 |
+
if (critical.length > 0) {
|
| 778 |
+
summary += `#### 🔴 Critical Gaps (${critical.length})\n\n`;
|
| 779 |
+
for (const gap of critical) {
|
| 780 |
+
summary += `**${gap.category}** - ${gap.articleReference || "General"}\n`;
|
| 781 |
+
summary += `> ${gap.description}\n`;
|
| 782 |
+
if (gap.currentState)
|
| 783 |
+
summary += `> *Current:* ${gap.currentState}\n`;
|
| 784 |
+
if (gap.requiredState)
|
| 785 |
+
summary += `> *Required:* ${gap.requiredState}\n`;
|
| 786 |
+
if (gap.deadline) summary += `> ⏰ Deadline: ${gap.deadline}\n`;
|
| 787 |
+
summary += `\n`;
|
| 788 |
+
}
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
if (high.length > 0) {
|
| 792 |
+
summary += `#### 🟠 High Priority Gaps (${high.length})\n\n`;
|
| 793 |
+
for (const gap of high) {
|
| 794 |
+
summary += `**${gap.category}** - ${gap.articleReference || "General"}\n`;
|
| 795 |
+
summary += `> ${gap.description}\n`;
|
| 796 |
+
if (gap.deadline) summary += `> ⏰ Deadline: ${gap.deadline}\n`;
|
| 797 |
+
summary += `\n`;
|
| 798 |
+
}
|
| 799 |
+
}
|
| 800 |
+
|
| 801 |
+
if (medium.length > 0) {
|
| 802 |
+
summary += `#### 🟡 Medium Priority Gaps (${medium.length})\n\n`;
|
| 803 |
+
for (const gap of medium.slice(0, 5)) {
|
| 804 |
+
summary += `- **${gap.category}:** ${gap.description}\n`;
|
| 805 |
+
}
|
| 806 |
+
if (medium.length > 5) {
|
| 807 |
+
summary += `- *...and ${medium.length - 5} more medium-priority gaps*\n`;
|
| 808 |
+
}
|
| 809 |
+
summary += `\n`;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
if (low.length > 0) {
|
| 813 |
+
summary += `#### 🟢 Low Priority Gaps (${low.length})\n\n`;
|
| 814 |
+
summary += `*${low.length} low-priority gaps identified - see detailed report*\n\n`;
|
| 815 |
+
}
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
// Recommendations
|
| 819 |
+
if (assess.recommendations && assess.recommendations.length > 0) {
|
| 820 |
+
summary += `### 💡 Priority Recommendations\n\n`;
|
| 821 |
+
|
| 822 |
+
// Sort by priority
|
| 823 |
+
const sortedRecs = [...assess.recommendations].sort(
|
| 824 |
+
(a: any, b: any) => a.priority - b.priority,
|
| 825 |
+
);
|
| 826 |
+
|
| 827 |
+
for (const rec of sortedRecs.slice(0, 5)) {
|
| 828 |
+
summary += `#### ${rec.priority}. ${rec.title}\n`;
|
| 829 |
+
summary += `*${rec.articleReference || "General Compliance"}*\n\n`;
|
| 830 |
+
summary += `${rec.description}\n\n`;
|
| 831 |
+
|
| 832 |
+
if (rec.implementationSteps && rec.implementationSteps.length > 0) {
|
| 833 |
+
summary += `**Implementation Steps:**\n`;
|
| 834 |
+
for (
|
| 835 |
+
let i = 0;
|
| 836 |
+
i < Math.min(rec.implementationSteps.length, 5);
|
| 837 |
+
i++
|
| 838 |
+
) {
|
| 839 |
+
summary += `${i + 1}. ${rec.implementationSteps[i]}\n`;
|
| 840 |
+
}
|
| 841 |
+
summary += `\n`;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
if (rec.estimatedEffort) {
|
| 845 |
+
summary += `**Estimated Effort:** ${rec.estimatedEffort}\n`;
|
| 846 |
+
}
|
| 847 |
+
if (rec.expectedOutcome) {
|
| 848 |
+
summary += `**Expected Outcome:** ${rec.expectedOutcome}\n`;
|
| 849 |
+
}
|
| 850 |
+
summary += `\n`;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
if (sortedRecs.length > 5) {
|
| 854 |
+
summary += `*...and ${sortedRecs.length - 5} additional recommendations*\n\n`;
|
| 855 |
+
}
|
| 856 |
+
}
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// ================== KEY COMPLIANCE DEADLINES ==================
|
| 860 |
+
if (aiData?.complianceDeadlines) {
|
| 861 |
+
summary += `## 📅 Key Compliance Deadlines\n\n`;
|
| 862 |
+
summary += `| Deadline | Requirement |\n`;
|
| 863 |
+
summary += `|----------|-------------|\n`;
|
| 864 |
+
summary += `| **February 2, 2025** | Prohibited AI practices ban (Article 5) |\n`;
|
| 865 |
+
summary += `| **August 2, 2025** | GPAI model obligations (Article 53) |\n`;
|
| 866 |
+
summary += `| **${aiData.complianceDeadlines.limitedRisk}** | Limited-risk transparency (Article 50) |\n`;
|
| 867 |
+
summary += `| **${aiData.complianceDeadlines.highRisk}** | High-risk AI full compliance |\n`;
|
| 868 |
+
summary += `\n`;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
// ================== DOCUMENTATION TEMPLATES ==================
|
| 872 |
+
if (assessData?.documentation) {
|
| 873 |
+
const docs = assessData.documentation;
|
| 874 |
+
summary += `## 📝 Generated Documentation Templates\n\n`;
|
| 875 |
+
summary += `The following EU AI Act compliance documentation templates have been generated:\n\n`;
|
| 876 |
+
|
| 877 |
+
const docList = [
|
| 878 |
+
{
|
| 879 |
+
name: "Risk Management System",
|
| 880 |
+
field: "riskManagementTemplate",
|
| 881 |
+
article: "Article 9",
|
| 882 |
+
},
|
| 883 |
+
{
|
| 884 |
+
name: "Technical Documentation",
|
| 885 |
+
field: "technicalDocumentation",
|
| 886 |
+
article: "Article 11, Annex IV",
|
| 887 |
+
},
|
| 888 |
+
{
|
| 889 |
+
name: "Conformity Assessment",
|
| 890 |
+
field: "conformityAssessment",
|
| 891 |
+
article: "Article 43",
|
| 892 |
+
},
|
| 893 |
+
{
|
| 894 |
+
name: "Transparency Notice",
|
| 895 |
+
field: "transparencyNotice",
|
| 896 |
+
article: "Article 50",
|
| 897 |
+
},
|
| 898 |
+
{
|
| 899 |
+
name: "Quality Management System",
|
| 900 |
+
field: "qualityManagementSystem",
|
| 901 |
+
article: "Article 17",
|
| 902 |
+
},
|
| 903 |
+
{
|
| 904 |
+
name: "Human Oversight Procedure",
|
| 905 |
+
field: "humanOversightProcedure",
|
| 906 |
+
article: "Article 14",
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
name: "Data Governance Policy",
|
| 910 |
+
field: "dataGovernancePolicy",
|
| 911 |
+
article: "Article 10",
|
| 912 |
+
},
|
| 913 |
+
{
|
| 914 |
+
name: "Incident Reporting Procedure",
|
| 915 |
+
field: "incidentReportingProcedure",
|
| 916 |
+
article: "General",
|
| 917 |
+
},
|
| 918 |
+
];
|
| 919 |
+
|
| 920 |
+
summary += `| Document | Article Reference | Status |\n`;
|
| 921 |
+
summary += `|----------|-------------------|--------|\n`;
|
| 922 |
+
for (const doc of docList) {
|
| 923 |
+
const hasDoc = (docs as any)[doc.field];
|
| 924 |
+
summary += `| ${doc.name} | ${doc.article} | ${hasDoc ? "✅ Generated" : "⚪ Not generated"} |\n`;
|
| 925 |
+
}
|
| 926 |
+
summary += `\n`;
|
| 927 |
+
|
| 928 |
+
// Show first template as example
|
| 929 |
+
const firstTemplate =
|
| 930 |
+
docs.riskManagementTemplate ||
|
| 931 |
+
docs.technicalDocumentation ||
|
| 932 |
+
docs.transparencyNotice;
|
| 933 |
+
if (firstTemplate) {
|
| 934 |
+
summary += `### 📄 Sample Template: Risk Management System (Article 9)\n\n`;
|
| 935 |
+
summary += `<details>\n<summary>Click to expand template</summary>\n\n`;
|
| 936 |
+
summary += `${firstTemplate.substring(0, 2000)}${firstTemplate.length > 2000 ? "\n\n*...template truncated for display...*" : ""}\n`;
|
| 937 |
+
summary += `\n</details>\n\n`;
|
| 938 |
+
}
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// ================== AI REASONING ==================
|
| 942 |
+
if (assessData?.reasoning) {
|
| 943 |
+
summary += `## 🧠 Assessment Reasoning\n\n`;
|
| 944 |
+
summary += `<details>\n<summary>Click to expand AI analysis reasoning</summary>\n\n`;
|
| 945 |
+
summary += `${assessData.reasoning}\n`;
|
| 946 |
+
summary += `\n</details>\n\n`;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
summary += `---\n\n`;
|
| 950 |
+
summary += `*Report generated on ${new Date().toISOString()}*\n\n`;
|
| 951 |
+
summary += `**Disclaimer:** This report is for informational purposes only and does not constitute legal advice. Consult with qualified legal professionals for official compliance guidance.\n`;
|
| 952 |
+
|
| 953 |
+
// Stream the comprehensive summary
|
| 954 |
+
for (const char of summary) {
|
| 955 |
+
res.write(
|
| 956 |
+
`data: ${JSON.stringify({ type: "text", content: char })}\n\n`,
|
| 957 |
+
);
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
// Send final done message
|
| 962 |
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
| 963 |
+
res.end();
|
| 964 |
+
} catch (error) {
|
| 965 |
+
console.error("Chat error:", error);
|
| 966 |
+
|
| 967 |
+
// Try to send error via stream if headers already sent
|
| 968 |
+
if (res.headersSent) {
|
| 969 |
+
res.write(
|
| 970 |
+
`data: ${JSON.stringify({
|
| 971 |
+
type: "error",
|
| 972 |
+
error: error instanceof Error ? error.message : "Unknown error",
|
| 973 |
+
})}\n\n`,
|
| 974 |
+
);
|
| 975 |
+
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
| 976 |
+
res.end();
|
| 977 |
+
} else {
|
| 978 |
+
res.status(500).json({
|
| 979 |
+
error: "Internal server error",
|
| 980 |
+
message: error instanceof Error ? error.message : "Unknown error",
|
| 981 |
+
});
|
| 982 |
+
}
|
| 983 |
+
}
|
| 984 |
+
});
|
| 985 |
+
|
| 986 |
+
// Tool status endpoint
|
| 987 |
+
app.get("/api/tools", async (_req, res) => {
|
| 988 |
+
try {
|
| 989 |
+
// Use default GPT-OSS (free, no API key needed) just to list tools
|
| 990 |
+
const agent = createAgent({
|
| 991 |
+
modelName: "gpt-oss",
|
| 992 |
+
apiKeys: {
|
| 993 |
+
modalEndpointUrl:
|
| 994 |
+
"https://vasilis--gpt-oss-vllm-inference-serve.modal.run",
|
| 995 |
+
},
|
| 996 |
+
});
|
| 997 |
+
const tools = await agent.getTools();
|
| 998 |
+
|
| 999 |
+
res.json({
|
| 1000 |
+
tools: tools.map((tool: any) => ({
|
| 1001 |
+
name: tool.name,
|
| 1002 |
+
description: tool.description,
|
| 1003 |
+
})),
|
| 1004 |
+
});
|
| 1005 |
+
} catch (error) {
|
| 1006 |
+
console.error("Tools error:", error);
|
| 1007 |
+
res.status(500).json({ error: "Failed to fetch tools" });
|
| 1008 |
+
}
|
| 1009 |
+
});
|
| 1010 |
+
|
| 1011 |
+
// ============================================================================
|
| 1012 |
+
// DIRECT TOOL ENDPOINTS - For ChatGPT Apps and direct API calls
|
| 1013 |
+
// ============================================================================
|
| 1014 |
+
|
| 1015 |
+
/**
|
| 1016 |
+
* Direct endpoint for discover_organization tool
|
| 1017 |
+
* Used by ChatGPT Apps via Gradio MCP server
|
| 1018 |
+
*/
|
| 1019 |
+
app.post("/api/tools/discover_organization", async (req, res) => {
|
| 1020 |
+
try {
|
| 1021 |
+
const { organizationName, domain, context } = req.body;
|
| 1022 |
+
|
| 1023 |
+
if (!organizationName) {
|
| 1024 |
+
return res.status(400).json({ error: "organizationName is required" });
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
console.log(`[API] discover_organization called for: ${organizationName}`);
|
| 1028 |
+
|
| 1029 |
+
// Read API keys from headers (from Gradio UI), fallback to server env (HF Spaces secret)
|
| 1030 |
+
const tavilyApiKey =
|
| 1031 |
+
(req.headers["x-tavily-api-key"] as string) ||
|
| 1032 |
+
process.env.TAVILY_API_KEY ||
|
| 1033 |
+
undefined;
|
| 1034 |
+
if (tavilyApiKey) {
|
| 1035 |
+
console.log(
|
| 1036 |
+
`[API] Using Tavily API key from: ${req.headers["x-tavily-api-key"] ? "request header" : "server env (HF Spaces secret)"}`,
|
| 1037 |
+
);
|
| 1038 |
+
} else {
|
| 1039 |
+
console.log(`[API] No Tavily API key - will use AI model fallback`);
|
| 1040 |
+
}
|
| 1041 |
+
const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss";
|
| 1042 |
+
const apiKeys = {
|
| 1043 |
+
modalEndpointUrl:
|
| 1044 |
+
(req.headers["x-modal-endpoint-url"] as string) || undefined,
|
| 1045 |
+
openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined,
|
| 1046 |
+
xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined,
|
| 1047 |
+
anthropicApiKey:
|
| 1048 |
+
(req.headers["x-anthropic-api-key"] as string) || undefined,
|
| 1049 |
+
googleApiKey: (req.headers["x-google-api-key"] as string) || undefined,
|
| 1050 |
+
};
|
| 1051 |
+
|
| 1052 |
+
const result = await discoverOrganization({
|
| 1053 |
+
organizationName,
|
| 1054 |
+
domain: domain || undefined,
|
| 1055 |
+
context: context || undefined,
|
| 1056 |
+
model: modelName,
|
| 1057 |
+
apiKeys,
|
| 1058 |
+
tavilyApiKey,
|
| 1059 |
+
});
|
| 1060 |
+
|
| 1061 |
+
console.log(
|
| 1062 |
+
`[API] discover_organization completed for: ${organizationName}`,
|
| 1063 |
+
);
|
| 1064 |
+
res.json(result);
|
| 1065 |
+
} catch (error) {
|
| 1066 |
+
console.error("discover_organization error:", error);
|
| 1067 |
+
res.status(500).json({
|
| 1068 |
+
error: true,
|
| 1069 |
+
message: error instanceof Error ? error.message : "Unknown error",
|
| 1070 |
+
});
|
| 1071 |
+
}
|
| 1072 |
+
});
|
| 1073 |
+
|
| 1074 |
+
/**
|
| 1075 |
+
* Direct endpoint for discover_ai_services tool
|
| 1076 |
+
* Used by ChatGPT Apps via Gradio MCP server
|
| 1077 |
+
*/
|
| 1078 |
+
app.post("/api/tools/discover_ai_services", async (req, res) => {
|
| 1079 |
+
try {
|
| 1080 |
+
const { organizationContext, systemNames, scope, context } = req.body;
|
| 1081 |
+
|
| 1082 |
+
console.log(
|
| 1083 |
+
`[API] discover_ai_services called, systemNames: ${JSON.stringify(systemNames)}`,
|
| 1084 |
+
);
|
| 1085 |
+
|
| 1086 |
+
// Read API keys from headers (from Gradio UI), fallback to server env (HF Spaces secret)
|
| 1087 |
+
const tavilyApiKey =
|
| 1088 |
+
(req.headers["x-tavily-api-key"] as string) ||
|
| 1089 |
+
process.env.TAVILY_API_KEY ||
|
| 1090 |
+
undefined;
|
| 1091 |
+
if (tavilyApiKey) {
|
| 1092 |
+
console.log(
|
| 1093 |
+
`[API] Using Tavily API key from: ${req.headers["x-tavily-api-key"] ? "request header" : "server env (HF Spaces secret)"}`,
|
| 1094 |
+
);
|
| 1095 |
+
} else {
|
| 1096 |
+
console.log(`[API] No Tavily API key - will use AI model fallback`);
|
| 1097 |
+
}
|
| 1098 |
+
const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss";
|
| 1099 |
+
const apiKeys = {
|
| 1100 |
+
modalEndpointUrl:
|
| 1101 |
+
(req.headers["x-modal-endpoint-url"] as string) || undefined,
|
| 1102 |
+
openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined,
|
| 1103 |
+
xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined,
|
| 1104 |
+
anthropicApiKey:
|
| 1105 |
+
(req.headers["x-anthropic-api-key"] as string) || undefined,
|
| 1106 |
+
googleApiKey: (req.headers["x-google-api-key"] as string) || undefined,
|
| 1107 |
+
};
|
| 1108 |
+
|
| 1109 |
+
const result = await discoverAIServices({
|
| 1110 |
+
organizationContext: organizationContext || undefined,
|
| 1111 |
+
systemNames: systemNames || undefined,
|
| 1112 |
+
scope: scope || undefined,
|
| 1113 |
+
context: context || undefined,
|
| 1114 |
+
model: modelName,
|
| 1115 |
+
apiKeys,
|
| 1116 |
+
tavilyApiKey,
|
| 1117 |
+
});
|
| 1118 |
+
|
| 1119 |
+
console.log(
|
| 1120 |
+
`[API] discover_ai_services completed, found ${result.systems?.length || 0} systems`,
|
| 1121 |
+
);
|
| 1122 |
+
res.json(result);
|
| 1123 |
+
} catch (error) {
|
| 1124 |
+
console.error("discover_ai_services error:", error);
|
| 1125 |
+
res.status(500).json({
|
| 1126 |
+
error: true,
|
| 1127 |
+
message: error instanceof Error ? error.message : "Unknown error",
|
| 1128 |
+
});
|
| 1129 |
+
}
|
| 1130 |
+
});
|
| 1131 |
+
|
| 1132 |
+
/**
|
| 1133 |
+
* Direct endpoint for assess_compliance tool
|
| 1134 |
+
* Used by ChatGPT Apps via Gradio MCP server
|
| 1135 |
+
*
|
| 1136 |
+
* Note: This endpoint sets env vars for the MCP tool to read.
|
| 1137 |
+
* The main /api/chat endpoint uses direct API key passing instead.
|
| 1138 |
+
*/
|
| 1139 |
+
app.post("/api/tools/assess_compliance", async (req, res) => {
|
| 1140 |
+
try {
|
| 1141 |
+
const {
|
| 1142 |
+
organizationContext,
|
| 1143 |
+
aiServicesContext,
|
| 1144 |
+
focusAreas,
|
| 1145 |
+
generateDocumentation,
|
| 1146 |
+
} = req.body;
|
| 1147 |
+
|
| 1148 |
+
console.log(
|
| 1149 |
+
`[API] assess_compliance called, generateDocumentation: ${generateDocumentation}`,
|
| 1150 |
+
);
|
| 1151 |
+
|
| 1152 |
+
// Read model selection and API keys from headers (from Gradio UI), fallback to server env (HF Spaces secret)
|
| 1153 |
+
const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss";
|
| 1154 |
+
const tavilyApiKey =
|
| 1155 |
+
(req.headers["x-tavily-api-key"] as string) ||
|
| 1156 |
+
process.env.TAVILY_API_KEY ||
|
| 1157 |
+
undefined;
|
| 1158 |
+
if (tavilyApiKey) {
|
| 1159 |
+
console.log(
|
| 1160 |
+
`[API] Using Tavily API key from: ${req.headers["x-tavily-api-key"] ? "request header" : "server env (HF Spaces secret)"}`,
|
| 1161 |
+
);
|
| 1162 |
+
} else {
|
| 1163 |
+
console.log(`[API] No Tavily API key - will use AI model fallback`);
|
| 1164 |
+
}
|
| 1165 |
+
const apiKeys = {
|
| 1166 |
+
modalEndpointUrl:
|
| 1167 |
+
(req.headers["x-modal-endpoint-url"] as string) || undefined,
|
| 1168 |
+
openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined,
|
| 1169 |
+
xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined,
|
| 1170 |
+
anthropicApiKey:
|
| 1171 |
+
(req.headers["x-anthropic-api-key"] as string) || undefined,
|
| 1172 |
+
googleApiKey: (req.headers["x-google-api-key"] as string) || undefined,
|
| 1173 |
+
};
|
| 1174 |
+
|
| 1175 |
+
// For GPT-OSS, use default Modal endpoint if not provided
|
| 1176 |
+
if (modelName === "gpt-oss" && !apiKeys.modalEndpointUrl) {
|
| 1177 |
+
apiKeys.modalEndpointUrl =
|
| 1178 |
+
"https://vasilis--gpt-oss-vllm-inference-serve.modal.run";
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
const result = await assessCompliance({
|
| 1182 |
+
organizationContext: organizationContext || undefined,
|
| 1183 |
+
aiServicesContext: aiServicesContext || undefined,
|
| 1184 |
+
focusAreas: focusAreas || undefined,
|
| 1185 |
+
generateDocumentation: generateDocumentation !== false, // Default true
|
| 1186 |
+
model: modelName,
|
| 1187 |
+
apiKeys,
|
| 1188 |
+
tavilyApiKey,
|
| 1189 |
+
});
|
| 1190 |
+
|
| 1191 |
+
console.log(
|
| 1192 |
+
`[API] assess_compliance completed, score: ${result.assessment?.overallScore}`,
|
| 1193 |
+
);
|
| 1194 |
+
res.json(result);
|
| 1195 |
+
} catch (error) {
|
| 1196 |
+
console.error("assess_compliance error:", error);
|
| 1197 |
+
res.status(500).json({
|
| 1198 |
+
error: true,
|
| 1199 |
+
message: error instanceof Error ? error.message : "Unknown error",
|
| 1200 |
+
});
|
| 1201 |
+
}
|
| 1202 |
+
});
|
| 1203 |
+
|
| 1204 |
+
// Start server
|
| 1205 |
+
app.listen(PORT, () => {
|
| 1206 |
+
const PUBLIC_URL = process.env.PUBLIC_URL;
|
| 1207 |
+
const isProduction = process.env.NODE_ENV === "production";
|
| 1208 |
+
|
| 1209 |
+
console.log(`\n🇪🇺 EU AI Act Compliance Agent Server`);
|
| 1210 |
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
|
| 1211 |
+
|
| 1212 |
+
if (isProduction) {
|
| 1213 |
+
console.log(`🌐 Environment: PRODUCTION (HF Spaces)`);
|
| 1214 |
+
console.log(`✓ Gradio UI: ${PUBLIC_URL || "https://*.hf.space"}`);
|
| 1215 |
+
console.log(`✓ API Server: http://localhost:${PORT} (internal only)`);
|
| 1216 |
+
console.log(`\n📡 Internal API Endpoints (used by Gradio):`);
|
| 1217 |
+
} else {
|
| 1218 |
+
console.log(`🛠️ Environment: LOCAL DEVELOPMENT`);
|
| 1219 |
+
console.log(`✓ Server running on http://localhost:${PORT}`);
|
| 1220 |
+
console.log(`\n📡 API Endpoints:`);
|
| 1221 |
+
}
|
| 1222 |
+
|
| 1223 |
+
console.log(` • GET /health`);
|
| 1224 |
+
console.log(` • POST /api/chat`);
|
| 1225 |
+
console.log(` • GET /api/tools`);
|
| 1226 |
+
console.log(` • POST /api/tools/discover_organization`);
|
| 1227 |
+
console.log(` • POST /api/tools/discover_ai_services`);
|
| 1228 |
+
console.log(` • POST /api/tools/assess_compliance`);
|
| 1229 |
+
|
| 1230 |
+
if (!isProduction) {
|
| 1231 |
+
console.log(`\n💡 Start Gradio UI: pnpm gradio`);
|
| 1232 |
+
console.log(`💡 Start ChatGPT App: pnpm chatgpt-app`);
|
| 1233 |
+
}
|
| 1234 |
+
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
|
| 1235 |
+
});
|
apps/eu-ai-act-agent/src/types/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Type definitions for EU AI Act Compliance Agent
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export interface ChatMessage {
|
| 6 |
+
role: "user" | "assistant" | "system";
|
| 7 |
+
content: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export interface ChatRequest {
|
| 11 |
+
message: string;
|
| 12 |
+
history: ChatMessage[];
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface ChatResponse {
|
| 16 |
+
type: "text" | "tool_call" | "result" | "done" | "error";
|
| 17 |
+
content?: string;
|
| 18 |
+
tool?: string;
|
| 19 |
+
data?: any;
|
| 20 |
+
error?: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export interface ToolDefinition {
|
| 24 |
+
name: string;
|
| 25 |
+
description: string;
|
| 26 |
+
parameters: Record<string, any>;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
export interface AgentConfig {
|
| 30 |
+
model: string;
|
| 31 |
+
temperature?: number;
|
| 32 |
+
maxTokens?: number;
|
| 33 |
+
maxSteps?: number;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Re-export types from MCP package
|
| 37 |
+
// @ts-ignore - These will be available at runtime after building
|
| 38 |
+
export type {
|
| 39 |
+
OrganizationProfile,
|
| 40 |
+
AIServiceDiscovery,
|
| 41 |
+
ComplianceAssessment,
|
| 42 |
+
} from "../../../eu-ai-act-mcp/dist/types/index.js";
|
| 43 |
+
|
apps/eu-ai-act-agent/start.sh
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# EU AI Act Compliance Agent Startup Script
|
| 4 |
+
# Starts both the API server and Gradio UI
|
| 5 |
+
|
| 6 |
+
set -e
|
| 7 |
+
|
| 8 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 9 |
+
echo "🇪🇺 EU AI Act Compliance Agent"
|
| 10 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 11 |
+
echo ""
|
| 12 |
+
|
| 13 |
+
# Check if .env exists
|
| 14 |
+
if [ ! -f "../../.env" ]; then
|
| 15 |
+
echo "⚠️ Warning: .env file not found"
|
| 16 |
+
echo " Create one from .env.example and add your OPENAI_API_KEY"
|
| 17 |
+
echo ""
|
| 18 |
+
fi
|
| 19 |
+
|
| 20 |
+
# Check Node.js
|
| 21 |
+
if ! command -v node &> /dev/null; then
|
| 22 |
+
echo "❌ Node.js not found. Please install Node.js 18+"
|
| 23 |
+
exit 1
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
# Check Python
|
| 27 |
+
if ! command -v python3 &> /dev/null; then
|
| 28 |
+
echo "❌ Python 3 not found. Please install Python 3.9+"
|
| 29 |
+
exit 1
|
| 30 |
+
fi
|
| 31 |
+
|
| 32 |
+
echo "✓ Node.js: $(node --version)"
|
| 33 |
+
echo "✓ Python: $(python3 --version)"
|
| 34 |
+
echo ""
|
| 35 |
+
|
| 36 |
+
# Check if uv is installed
|
| 37 |
+
if ! command -v uv &> /dev/null; then
|
| 38 |
+
echo "📦 Installing uv (fast Python package manager)..."
|
| 39 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 40 |
+
echo ""
|
| 41 |
+
echo "⚠️ Please restart your terminal and run this script again"
|
| 42 |
+
exit 0
|
| 43 |
+
fi
|
| 44 |
+
|
| 45 |
+
# Create virtual environment if it doesn't exist
|
| 46 |
+
if [ ! -d ".venv" ]; then
|
| 47 |
+
echo "📦 Creating virtual environment with Python 3.13..."
|
| 48 |
+
uv venv --python python3.13
|
| 49 |
+
echo ""
|
| 50 |
+
fi
|
| 51 |
+
|
| 52 |
+
# Activate virtual environment
|
| 53 |
+
source .venv/bin/activate
|
| 54 |
+
|
| 55 |
+
# Install Python dependencies if needed
|
| 56 |
+
if ! python -c "import gradio" 2>/dev/null; then
|
| 57 |
+
echo "📦 Installing Python dependencies with uv..."
|
| 58 |
+
uv pip install -r requirements.txt
|
| 59 |
+
echo ""
|
| 60 |
+
fi
|
| 61 |
+
|
| 62 |
+
# Build MCP server if needed
|
| 63 |
+
if [ ! -d "../../packages/eu-ai-act-mcp/dist" ]; then
|
| 64 |
+
echo "🔨 Building MCP server..."
|
| 65 |
+
cd ../../
|
| 66 |
+
pnpm --filter @eu-ai-act/mcp-server build
|
| 67 |
+
cd apps/eu-ai-act-agent
|
| 68 |
+
echo ""
|
| 69 |
+
fi
|
| 70 |
+
|
| 71 |
+
echo "🚀 Starting EU AI Act Compliance Agent..."
|
| 72 |
+
echo ""
|
| 73 |
+
echo "📡 API Server will start on: http://localhost:3001"
|
| 74 |
+
echo "🎨 Gradio UI will start on: http://localhost:7860"
|
| 75 |
+
echo ""
|
| 76 |
+
echo "Press Ctrl+C to stop both servers"
|
| 77 |
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
| 78 |
+
echo ""
|
| 79 |
+
|
| 80 |
+
# Function to cleanup on exit
|
| 81 |
+
cleanup() {
|
| 82 |
+
echo ""
|
| 83 |
+
echo "🛑 Shutting down servers..."
|
| 84 |
+
kill $API_PID $GRADIO_PID $CHATGPT_PID 2>/dev/null
|
| 85 |
+
exit 0
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
trap cleanup INT TERM
|
| 89 |
+
|
| 90 |
+
# Start API server in background
|
| 91 |
+
echo "Starting API server..."
|
| 92 |
+
pnpm dev > /tmp/eu-ai-act-api.log 2>&1 &
|
| 93 |
+
API_PID=$!
|
| 94 |
+
|
| 95 |
+
# Wait for API to be ready
|
| 96 |
+
echo "Waiting for API server to start..."
|
| 97 |
+
sleep 3
|
| 98 |
+
|
| 99 |
+
# Start Gradio UI in background (using virtual environment Python)
|
| 100 |
+
echo "Starting Gradio UI..."
|
| 101 |
+
python src/gradio_app.py > /tmp/eu-ai-act-gradio.log 2>&1 &
|
| 102 |
+
GRADIO_PID=$!
|
| 103 |
+
|
| 104 |
+
# Start ChatGPT App in background (using virtual environment Python)
|
| 105 |
+
echo "Starting ChatGPT App..."
|
| 106 |
+
python src/chatgpt_app.py > /tmp/eu-ai-act-chatgpt.log 2>&1 &
|
| 107 |
+
CHATGPT_PID=$!
|
| 108 |
+
|
| 109 |
+
# Wait for apps to be ready
|
| 110 |
+
sleep 3
|
| 111 |
+
|
| 112 |
+
echo ""
|
| 113 |
+
echo "✅ All servers are running!"
|
| 114 |
+
echo ""
|
| 115 |
+
echo "🌐 Open your browser to:"
|
| 116 |
+
echo " Gradio UI: http://localhost:7860"
|
| 117 |
+
echo " ChatGPT App: http://localhost:7861"
|
| 118 |
+
echo ""
|
| 119 |
+
echo "📋 Logs:"
|
| 120 |
+
echo " API: tail -f /tmp/eu-ai-act-api.log"
|
| 121 |
+
echo " Gradio: tail -f /tmp/eu-ai-act-gradio.log"
|
| 122 |
+
echo " ChatGPT App: tail -f /tmp/eu-ai-act-chatgpt.log"
|
| 123 |
+
echo ""
|
| 124 |
+
|
| 125 |
+
# Wait for all processes
|
| 126 |
+
wait $API_PID $GRADIO_PID $CHATGPT_PID
|
| 127 |
+
|
apps/eu-ai-act-agent/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "../../tooling/typescript/base.json",
|
| 3 |
+
"compilerOptions": {
|
| 4 |
+
"outDir": "./dist",
|
| 5 |
+
"rootDir": "./src",
|
| 6 |
+
"lib": ["ES2022"],
|
| 7 |
+
"target": "ES2022",
|
| 8 |
+
"module": "ES2022",
|
| 9 |
+
"moduleResolution": "bundler",
|
| 10 |
+
"resolveJsonModule": true,
|
| 11 |
+
"allowSyntheticDefaultImports": true,
|
| 12 |
+
"esModuleInterop": true,
|
| 13 |
+
"skipLibCheck": true,
|
| 14 |
+
"strict": true,
|
| 15 |
+
"noUnusedLocals": true,
|
| 16 |
+
"noUnusedParameters": true,
|
| 17 |
+
"noFallthroughCasesInSwitch": true
|
| 18 |
+
},
|
| 19 |
+
"include": ["src/**/*"],
|
| 20 |
+
"exclude": ["node_modules", "dist"]
|
| 21 |
+
}
|
| 22 |
+
|
apps/eu-ai-act-agent/tsup.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "tsup";
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
entry: ["src/server.ts"],
|
| 5 |
+
format: ["esm"],
|
| 6 |
+
dts: false, // Disable dts generation to avoid module resolution issues
|
| 7 |
+
sourcemap: true,
|
| 8 |
+
clean: true,
|
| 9 |
+
minify: false,
|
| 10 |
+
external: ["express", "dotenv"],
|
| 11 |
+
noExternal: [], // Bundle everything except external
|
| 12 |
+
bundle: true,
|
| 13 |
+
});
|
| 14 |
+
|
apps/eu-ai-act-agent/tsx
ADDED
|
File without changes
|
apps/eu-ai-act-agent/uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
biome.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
| 3 |
+
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
| 4 |
+
"linter": {
|
| 5 |
+
"enabled": true,
|
| 6 |
+
"rules": {
|
| 7 |
+
"recommended": true,
|
| 8 |
+
"complexity": {
|
| 9 |
+
"noForEach": "off",
|
| 10 |
+
"noUselessFragments": "off"
|
| 11 |
+
},
|
| 12 |
+
"correctness": {
|
| 13 |
+
"useExhaustiveDependencies": "off"
|
| 14 |
+
},
|
| 15 |
+
"suspicious": {
|
| 16 |
+
"noExplicitAny": "off"
|
| 17 |
+
},
|
| 18 |
+
"style": {
|
| 19 |
+
"noParameterAssign": "error",
|
| 20 |
+
"useAsConstAssertion": "error",
|
| 21 |
+
"useDefaultParameterLast": "error",
|
| 22 |
+
"useEnumInitializers": "error",
|
| 23 |
+
"useSelfClosingElements": "error",
|
| 24 |
+
"useSingleVarDeclarator": "error",
|
| 25 |
+
"noUnusedTemplateLiteral": "error",
|
| 26 |
+
"useNumberNamespace": "error",
|
| 27 |
+
"noInferrableTypes": "error",
|
| 28 |
+
"noUselessElse": "error"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"formatter": {
|
| 33 |
+
"enabled": true,
|
| 34 |
+
"includes": [
|
| 35 |
+
"**",
|
| 36 |
+
"!**/node_modules/**/*",
|
| 37 |
+
"!**/*.config.*",
|
| 38 |
+
"!**/*.json",
|
| 39 |
+
"!**/tsconfig.json",
|
| 40 |
+
"!**/.turbo"
|
| 41 |
+
]
|
| 42 |
+
}
|
| 43 |
+
}
|
modal/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modal Deployment for GPT-OSS vLLM
|
| 2 |
+
|
| 3 |
+
Deploy OpenAI's GPT-OSS models (20B or 120B) on [Modal.com](https://modal.com) with vLLM for efficient inference.
|
| 4 |
+
|
| 5 |
+
## 🚀 Quick Start
|
| 6 |
+
|
| 7 |
+
### 1. Install Modal CLI
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
# Install the Modal Python package
|
| 11 |
+
pip install modal
|
| 12 |
+
|
| 13 |
+
# Authenticate with Modal (opens browser)
|
| 14 |
+
modal setup
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
If `modal setup` doesn't work, try:
|
| 18 |
+
```bash
|
| 19 |
+
python -m modal setup
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### 2. Create a Modal Account
|
| 23 |
+
|
| 24 |
+
1. Go to [modal.com](https://modal.com)
|
| 25 |
+
2. Create a free account
|
| 26 |
+
3. Run `modal setup` to authenticate
|
| 27 |
+
|
| 28 |
+
### 3. Deploy the GPT-OSS Model
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
# Navigate to the modal directory
|
| 32 |
+
cd modal
|
| 33 |
+
|
| 34 |
+
# Test the server (spins up a temporary instance)
|
| 35 |
+
modal run gpt_oss_inference.py
|
| 36 |
+
|
| 37 |
+
# Deploy to production (creates a persistent endpoint)
|
| 38 |
+
modal deploy gpt_oss_inference.py
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
## 📋 Configuration
|
| 42 |
+
|
| 43 |
+
### GPU Selection (Cost Optimization)
|
| 44 |
+
|
| 45 |
+
Edit `gpt_oss_inference.py` to choose your GPU tier:
|
| 46 |
+
|
| 47 |
+
```python
|
| 48 |
+
# Choose your GPU - uncomment the one you want:
|
| 49 |
+
GPU_CONFIG = "A10G" # ~$0.76/hr - RECOMMENDED for budget ✅
|
| 50 |
+
# GPU_CONFIG = "L4" # ~$0.59/hr - Cheapest option
|
| 51 |
+
# GPU_CONFIG = "A100" # ~$1.79/hr - More headroom
|
| 52 |
+
# GPU_CONFIG = "H100" # ~$3.95/hr - Maximum performance
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### GPU Pricing Comparison
|
| 56 |
+
|
| 57 |
+
| GPU | VRAM | Price/hr | Best For |
|
| 58 |
+
| --------- | ---- | ---------- | -------------------------------- |
|
| 59 |
+
| L4 | 24GB | ~$0.59 | Tightest budget (may be tight) |
|
| 60 |
+
| **A10G** | 24GB | **~$0.76** | **Best value for GPT-OSS 20B** ✅ |
|
| 61 |
+
| A100 40GB | 40GB | ~$1.79 | More headroom |
|
| 62 |
+
| A100 80GB | 80GB | ~$2.78 | Both 20B and 120B |
|
| 63 |
+
| H100 | 80GB | ~$3.95 | Maximum performance |
|
| 64 |
+
|
| 65 |
+
### Model Selection
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
# 20B model - faster, fits on A10G/L4
|
| 69 |
+
MODEL_NAME = "openai/gpt-oss-20b"
|
| 70 |
+
|
| 71 |
+
# 120B model - needs A100 80GB or H100
|
| 72 |
+
MODEL_NAME = "openai/gpt-oss-120b"
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### Performance Tuning
|
| 76 |
+
|
| 77 |
+
```python
|
| 78 |
+
# FAST_BOOT = True - Faster startup, less memory (use for smaller GPUs)
|
| 79 |
+
# FAST_BOOT = False - Slower startup, faster inference
|
| 80 |
+
FAST_BOOT = True
|
| 81 |
+
|
| 82 |
+
# Data type - GPT-OSS MXFP4 quantization REQUIRES bfloat16 (float16 not supported)
|
| 83 |
+
# The Marlin kernel warning on A10G/L4 is expected and can be ignored
|
| 84 |
+
USE_FLOAT16 = False # Must be False for GPT-OSS (MXFP4 only supports bfloat16)
|
| 85 |
+
|
| 86 |
+
# Maximum model length (context window) - reduce to speed up startup
|
| 87 |
+
MAX_MODEL_LEN = 32768 # 32k tokens (can increase to 131072 if needed)
|
| 88 |
+
|
| 89 |
+
# Keep container warm longer to avoid cold starts
|
| 90 |
+
SCALEDOWN_WINDOW = 5 * MINUTES # Reduced from 10 minutes for faster warm starts
|
| 91 |
+
|
| 92 |
+
# Maximum concurrent requests (reduce for smaller GPUs)
|
| 93 |
+
MAX_INPUTS = 50
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
#### Startup Time Optimization
|
| 97 |
+
|
| 98 |
+
The following optimizations are enabled by default to reduce the ~1 minute startup time:
|
| 99 |
+
|
| 100 |
+
- **`--max-model-len 65536`**: Limits context window to 64k tokens (faster startup, can increase to 131072 if needed)
|
| 101 |
+
- **`--disable-custom-all-reduce`**: Disabled for single GPU (reduces startup overhead)
|
| 102 |
+
- **`--enable-prefix-caching`**: Enables prefix caching for faster subsequent requests
|
| 103 |
+
- **`--load-format auto`**: Auto-detects best loading format for faster model loading
|
| 104 |
+
- **Reduced scaledown window**: Keeps container warm for 5 minutes instead of 10 (faster warm starts)
|
| 105 |
+
|
| 106 |
+
Note: `--dtype bfloat16` is required for GPT-OSS (MXFP4 quantization only supports bf16)
|
| 107 |
+
|
| 108 |
+
## 🔧 Commands
|
| 109 |
+
|
| 110 |
+
| Command | Description |
|
| 111 |
+
| --------------------------------------- | ---------------------------- |
|
| 112 |
+
| `modal run gpt_oss_inference.py` | Test with a temporary server |
|
| 113 |
+
| `modal deploy gpt_oss_inference.py` | Deploy to production |
|
| 114 |
+
| `modal app stop gpt-oss-vllm-inference` | Stop the deployed app |
|
| 115 |
+
| `modal app logs gpt-oss-vllm-inference` | View deployment logs |
|
| 116 |
+
| `modal volume ls` | List cached volumes |
|
| 117 |
+
|
| 118 |
+
## 🌐 API Usage
|
| 119 |
+
|
| 120 |
+
Once deployed, the server exposes an OpenAI-compatible API:
|
| 121 |
+
|
| 122 |
+
### Endpoint URL
|
| 123 |
+
|
| 124 |
+
After deployment, Modal will provide a URL like:
|
| 125 |
+
```
|
| 126 |
+
https://your-workspace--gpt-oss-vllm-inference-serve.modal.run
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
### Making Requests
|
| 130 |
+
|
| 131 |
+
```python
|
| 132 |
+
import openai
|
| 133 |
+
|
| 134 |
+
client = openai.OpenAI(
|
| 135 |
+
base_url="https://your-workspace--gpt-oss-vllm-inference-serve.modal.run/v1",
|
| 136 |
+
api_key="not-needed" # Modal handles auth via the URL
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
response = client.chat.completions.create(
|
| 140 |
+
model="llm",
|
| 141 |
+
messages=[
|
| 142 |
+
{"role": "system", "content": "You are a helpful assistant."},
|
| 143 |
+
{"role": "user", "content": "Hello!"}
|
| 144 |
+
]
|
| 145 |
+
)
|
| 146 |
+
print(response.choices[0].message.content)
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
### cURL Example
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
curl -X POST "https://your-workspace--gpt-oss-vllm-inference-serve.modal.run/v1/chat/completions" \
|
| 153 |
+
-H "Content-Type: application/json" \
|
| 154 |
+
-d '{
|
| 155 |
+
"model": "llm",
|
| 156 |
+
"messages": [
|
| 157 |
+
{"role": "user", "content": "Hello!"}
|
| 158 |
+
]
|
| 159 |
+
}'
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
## 💰 Pricing
|
| 163 |
+
|
| 164 |
+
Modal charges per second of usage:
|
| 165 |
+
- **A10G GPU**: ~$0.76/hour (recommended) ✅
|
| 166 |
+
- **L4 GPU**: ~$0.59/hour (cheapest)
|
| 167 |
+
- **A100 40GB**: ~$1.79/hour
|
| 168 |
+
- **H100 GPU**: ~$3.95/hour (fastest)
|
| 169 |
+
- No charges when idle (scale to zero)
|
| 170 |
+
- First $30/month is free
|
| 171 |
+
|
| 172 |
+
## 📦 Model Details
|
| 173 |
+
|
| 174 |
+
### GPT-OSS 20B
|
| 175 |
+
- MoE architecture with efficient inference
|
| 176 |
+
- MXFP4 quantization for MoE layers (~10-15GB VRAM)
|
| 177 |
+
- Attention sink support for longer contexts
|
| 178 |
+
- **Fits on A10G, L4, A100, or H100** ✅
|
| 179 |
+
|
| 180 |
+
### GPT-OSS 120B
|
| 181 |
+
- Larger model with more capabilities
|
| 182 |
+
- Same quantization and architecture (~40-50GB VRAM)
|
| 183 |
+
- **Requires A100 80GB or H100**
|
| 184 |
+
|
| 185 |
+
## 🔍 Troubleshooting
|
| 186 |
+
|
| 187 |
+
### Authentication Issues
|
| 188 |
+
```bash
|
| 189 |
+
# Re-authenticate
|
| 190 |
+
modal token new
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
### GPU Availability
|
| 194 |
+
If your selected GPU is not available, Modal will queue your request. Tips:
|
| 195 |
+
- **A10G and L4** typically have better availability than H100
|
| 196 |
+
- Try different regions
|
| 197 |
+
- Use off-peak hours
|
| 198 |
+
- Change `GPU_CONFIG` to a different tier
|
| 199 |
+
|
| 200 |
+
### Marlin Kernel Warning
|
| 201 |
+
If you see: `You are running Marlin kernel with bf16 on GPUs before SM90`:
|
| 202 |
+
- **This warning can be safely ignored** - GPT-OSS uses MXFP4 quantization which **requires bfloat16**
|
| 203 |
+
- float16 is NOT supported for MXFP4 quantization (will cause a validation error)
|
| 204 |
+
- The warning is just a performance suggestion, but we cannot use fp16 for this model
|
| 205 |
+
- For optimal performance, use H100 (SM90+) which is optimized for bf16
|
| 206 |
+
|
| 207 |
+
### Startup Time Optimization
|
| 208 |
+
If startup takes ~1 minute:
|
| 209 |
+
- ✅ **Already optimized** - The code includes several optimizations:
|
| 210 |
+
- Uses `float16` instead of `bfloat16` for faster loading
|
| 211 |
+
- Limits context window to 32k tokens (faster memory allocation)
|
| 212 |
+
- Disables custom all-reduce for single GPU
|
| 213 |
+
- Enables prefix caching
|
| 214 |
+
- Uses auto load format detection
|
| 215 |
+
- To reduce startup further, you can:
|
| 216 |
+
- Increase `SCALEDOWN_WINDOW` to keep container warm longer (costs more)
|
| 217 |
+
- Use a larger GPU (A100/H100) for faster model loading
|
| 218 |
+
- Reduce `MAX_MODEL_LEN` if you don't need full context window
|
| 219 |
+
|
| 220 |
+
### Cache Issues
|
| 221 |
+
```bash
|
| 222 |
+
# Clear vLLM cache
|
| 223 |
+
modal volume rm vllm-cache
|
| 224 |
+
modal volume create vllm-cache
|
| 225 |
+
|
| 226 |
+
# Clear HuggingFace cache
|
| 227 |
+
modal volume rm huggingface-cache
|
| 228 |
+
modal volume create huggingface-cache
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
## 📚 Resources
|
| 232 |
+
|
| 233 |
+
- [Modal Documentation](https://modal.com/docs/guide)
|
| 234 |
+
- [vLLM Documentation](https://docs.vllm.ai/)
|
| 235 |
+
- [GPT-OSS on HuggingFace](https://huggingface.co/openai/gpt-oss-20b)
|
| 236 |
+
- [Modal Examples](https://modal.com/docs/examples)
|
| 237 |
+
|
modal/deploy.sh
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# =============================================================================
|
| 4 |
+
# Modal GPT-OSS Deployment Script
|
| 5 |
+
# =============================================================================
|
| 6 |
+
|
| 7 |
+
set -e
|
| 8 |
+
|
| 9 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 10 |
+
|
| 11 |
+
echo "🚀 Modal GPT-OSS vLLM Deployment"
|
| 12 |
+
echo "================================"
|
| 13 |
+
echo ""
|
| 14 |
+
|
| 15 |
+
# Check if modal is installed
|
| 16 |
+
if ! command -v modal &> /dev/null; then
|
| 17 |
+
echo "❌ Modal CLI not found. Installing..."
|
| 18 |
+
pip install modal
|
| 19 |
+
echo ""
|
| 20 |
+
fi
|
| 21 |
+
|
| 22 |
+
# Check if authenticated
|
| 23 |
+
echo "📋 Checking Modal authentication..."
|
| 24 |
+
if ! modal token info &> /dev/null 2>&1; then
|
| 25 |
+
echo "❌ Not authenticated with Modal."
|
| 26 |
+
echo ""
|
| 27 |
+
echo "Please run: modal setup"
|
| 28 |
+
echo "Or set your token with: modal token set --token-id <ID> --token-secret <SECRET>"
|
| 29 |
+
echo ""
|
| 30 |
+
exit 1
|
| 31 |
+
fi
|
| 32 |
+
|
| 33 |
+
echo "✅ Authenticated with Modal"
|
| 34 |
+
echo ""
|
| 35 |
+
|
| 36 |
+
# Show options
|
| 37 |
+
echo "What would you like to do?"
|
| 38 |
+
echo ""
|
| 39 |
+
echo " 1) Test the server (temporary deployment)"
|
| 40 |
+
echo " 2) Deploy to production"
|
| 41 |
+
echo " 3) Stop the deployed app"
|
| 42 |
+
echo " 4) View logs"
|
| 43 |
+
echo " 5) Exit"
|
| 44 |
+
echo ""
|
| 45 |
+
read -p "Enter choice [1-5]: " choice
|
| 46 |
+
|
| 47 |
+
case $choice in
|
| 48 |
+
1)
|
| 49 |
+
echo ""
|
| 50 |
+
echo "🧪 Running test deployment..."
|
| 51 |
+
cd "$SCRIPT_DIR"
|
| 52 |
+
modal run gpt_oss_inference.py
|
| 53 |
+
;;
|
| 54 |
+
2)
|
| 55 |
+
echo ""
|
| 56 |
+
echo "🚀 Deploying to production..."
|
| 57 |
+
cd "$SCRIPT_DIR"
|
| 58 |
+
modal deploy gpt_oss_inference.py
|
| 59 |
+
echo ""
|
| 60 |
+
echo "✅ Deployment complete!"
|
| 61 |
+
echo ""
|
| 62 |
+
echo "Your endpoint URL will be displayed above."
|
| 63 |
+
;;
|
| 64 |
+
3)
|
| 65 |
+
echo ""
|
| 66 |
+
echo "🛑 Stopping app..."
|
| 67 |
+
modal app stop gpt-oss-vllm-inference
|
| 68 |
+
echo "✅ App stopped"
|
| 69 |
+
;;
|
| 70 |
+
4)
|
| 71 |
+
echo ""
|
| 72 |
+
echo "📜 Fetching logs..."
|
| 73 |
+
modal app logs gpt-oss-vllm-inference
|
| 74 |
+
;;
|
| 75 |
+
5)
|
| 76 |
+
echo "👋 Goodbye!"
|
| 77 |
+
exit 0
|
| 78 |
+
;;
|
| 79 |
+
*)
|
| 80 |
+
echo "❌ Invalid choice"
|
| 81 |
+
exit 1
|
| 82 |
+
;;
|
| 83 |
+
esac
|
| 84 |
+
|
modal/gpt_oss_inference.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
GPT-OSS Model Deployment on Modal with vLLM
|
| 3 |
+
|
| 4 |
+
This script deploys OpenAI's GPT-OSS models (20B or 120B) on Modal.com
|
| 5 |
+
with vLLM for efficient inference.
|
| 6 |
+
|
| 7 |
+
Usage:
|
| 8 |
+
# First time setup - pre-download model weights (run once, takes ~5-10 min)
|
| 9 |
+
modal run gpt_oss_inference.py::download_model
|
| 10 |
+
|
| 11 |
+
# Test the server locally
|
| 12 |
+
modal run gpt_oss_inference.py
|
| 13 |
+
|
| 14 |
+
# Deploy to production
|
| 15 |
+
modal deploy gpt_oss_inference.py
|
| 16 |
+
|
| 17 |
+
Performance Tips:
|
| 18 |
+
1. Run download_model first to cache weights in the volume
|
| 19 |
+
2. Reduce MAX_MODEL_LEN for faster startup (8k is sufficient for most use cases)
|
| 20 |
+
3. Keep FAST_BOOT=True for cheaper GPUs (A10G, L4)
|
| 21 |
+
4. Increase SCALEDOWN_WINDOW to reduce cold starts during demos
|
| 22 |
+
|
| 23 |
+
Based on: https://modal.com/docs/examples/gpt_oss_inference
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import json
|
| 27 |
+
import time
|
| 28 |
+
from datetime import datetime, timezone
|
| 29 |
+
from typing import Any
|
| 30 |
+
|
| 31 |
+
import aiohttp
|
| 32 |
+
import modal
|
| 33 |
+
|
| 34 |
+
# =============================================================================
|
| 35 |
+
# Container Image Configuration
|
| 36 |
+
# =============================================================================
|
| 37 |
+
|
| 38 |
+
# Enable HF Transfer for faster model downloads (5-10x faster)
|
| 39 |
+
vllm_image = (
|
| 40 |
+
modal.Image.from_registry(
|
| 41 |
+
"nvidia/cuda:12.8.1-devel-ubuntu22.04",
|
| 42 |
+
add_python="3.12",
|
| 43 |
+
)
|
| 44 |
+
.entrypoint([])
|
| 45 |
+
.env({"HF_HUB_ENABLE_HF_TRANSFER": "1"}) # Enable fast downloads
|
| 46 |
+
.uv_pip_install(
|
| 47 |
+
"vllm==0.11.0",
|
| 48 |
+
"huggingface_hub[hf_transfer]==0.35.0",
|
| 49 |
+
"flashinfer-python==0.3.1",
|
| 50 |
+
)
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# =============================================================================
|
| 54 |
+
# Model Configuration
|
| 55 |
+
# =============================================================================
|
| 56 |
+
|
| 57 |
+
# Choose the model size - 20B is faster, 120B has more capabilities
|
| 58 |
+
MODEL_NAME = "openai/gpt-oss-20b" # or "openai/gpt-oss-120b"
|
| 59 |
+
MODEL_REVISION = "d666cf3b67006cf8227666739edf25164aaffdeb"
|
| 60 |
+
|
| 61 |
+
# =============================================================================
|
| 62 |
+
# GPU Configuration - CHOOSE YOUR GPU TIER
|
| 63 |
+
# =============================================================================
|
| 64 |
+
#
|
| 65 |
+
# Modal GPU Pricing (approximate, per hour):
|
| 66 |
+
# ┌─────────────┬──────────┬────────────────────────────────────────────┐
|
| 67 |
+
# │ GPU │ Price/hr │ Notes │
|
| 68 |
+
# ├─────────────┼──────────┼────────────────────────────────────────────┤
|
| 69 |
+
# │ T4 (16GB) │ ~$0.25 │ ❌ Too small for GPT-OSS │
|
| 70 |
+
# │ L4 (24GB) │ ~$0.59 │ ⚠️ Tight fit, may work with 20B │
|
| 71 |
+
# │ A10G (24GB) │ ~$0.76 │ ✅ Good balance for 20B model │
|
| 72 |
+
# │ A100 40GB │ ~$1.79 │ ✅ Comfortable for 20B │
|
| 73 |
+
# │ A100 80GB │ ~$2.78 │ ✅ Works for both 20B and 120B │
|
| 74 |
+
# │ H100 (80GB) │ ~$3.95 │ ✅ Best performance, both models │
|
| 75 |
+
# └─────────────┴──────────┴────────────────────────────────────────────┘
|
| 76 |
+
#
|
| 77 |
+
# GPT-OSS 20B with MXFP4 quantization needs ~10-15GB VRAM
|
| 78 |
+
# GPT-OSS 120B needs ~40-50GB VRAM
|
| 79 |
+
|
| 80 |
+
# Choose your GPU - uncomment the one you want to use:
|
| 81 |
+
GPU_CONFIG = "A100-40GB" # ~$0.76/hr - RECOMMENDED for budget (works with 20B)
|
| 82 |
+
# GPU_CONFIG = "L4" # ~$0.59/hr - Cheapest option (may be tight)
|
| 83 |
+
# GPU_CONFIG = "A100" # ~$1.79/hr - More headroom (40GB version)
|
| 84 |
+
# GPU_CONFIG = "H100" # ~$3.95/hr - Maximum performance
|
| 85 |
+
|
| 86 |
+
# =============================================================================
|
| 87 |
+
# Volume Configuration for Caching
|
| 88 |
+
# =============================================================================
|
| 89 |
+
|
| 90 |
+
# Cache for HuggingFace model weights
|
| 91 |
+
hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
|
| 92 |
+
|
| 93 |
+
# Cache for vLLM compilation artifacts
|
| 94 |
+
vllm_cache_vol = modal.Volume.from_name("vllm-cache", create_if_missing=True)
|
| 95 |
+
|
| 96 |
+
# =============================================================================
|
| 97 |
+
# Performance Configuration
|
| 98 |
+
# =============================================================================
|
| 99 |
+
|
| 100 |
+
MINUTES = 60 # Helper constant
|
| 101 |
+
|
| 102 |
+
# FAST_BOOT = True: Faster startup but slower inference
|
| 103 |
+
# FAST_BOOT = False: Slower startup but faster inference (recommended for production)
|
| 104 |
+
FAST_BOOT = True # Use True for cheaper GPUs to reduce startup memory
|
| 105 |
+
|
| 106 |
+
# CUDA graph capture sizes for optimized inference
|
| 107 |
+
CUDA_GRAPH_CAPTURE_SIZES = [1, 2, 4, 8, 16, 24, 32]
|
| 108 |
+
|
| 109 |
+
# Data type configuration
|
| 110 |
+
# NOTE: GPT-OSS uses MXFP4 quantization which REQUIRES bfloat16 - float16 is NOT supported
|
| 111 |
+
# The Marlin kernel warning on A10G/L4 is expected and can be ignored
|
| 112 |
+
USE_FLOAT16 = False # Must be False for GPT-OSS (MXFP4 only supports bfloat16)
|
| 113 |
+
|
| 114 |
+
# Maximum model length (context window) - SIGNIFICANTLY REDUCED for faster startup
|
| 115 |
+
# The KV cache allocation is proportional to context length, so smaller = much faster startup
|
| 116 |
+
# For EU AI Act assessments, 8k-16k tokens is more than enough
|
| 117 |
+
# GPT-OSS 20B supports up to 128k tokens, but we only need ~8k for our use case
|
| 118 |
+
MAX_MODEL_LEN = 16384 # 16k tokens - sufficient for compliance assessments, 4x faster startup
|
| 119 |
+
|
| 120 |
+
# Server configuration
|
| 121 |
+
VLLM_PORT = 8000
|
| 122 |
+
N_GPU = 1 # Number of GPUs for tensor parallelism
|
| 123 |
+
MAX_INPUTS = 50 # Reduced for smaller GPUs
|
| 124 |
+
|
| 125 |
+
# Keep container warm longer to avoid cold starts (costs more but faster response)
|
| 126 |
+
# For hackathon demo: 10 minutes to reduce cold starts during presentation
|
| 127 |
+
SCALEDOWN_WINDOW = 10 * MINUTES # Increased for demo stability
|
| 128 |
+
|
| 129 |
+
# =============================================================================
|
| 130 |
+
# Modal App Definition
|
| 131 |
+
# =============================================================================
|
| 132 |
+
|
| 133 |
+
app = modal.App("gpt-oss-vllm-inference")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
# Select GPU based on GPU_CONFIG
|
| 137 |
+
_GPU_MAP = {
|
| 138 |
+
"T4": "T4",
|
| 139 |
+
"L4": "L4",
|
| 140 |
+
"A10G": "A10G",
|
| 141 |
+
"A100": "A100:40GB",
|
| 142 |
+
"A100-80GB": "A100:80GB",
|
| 143 |
+
"H100": "H100",
|
| 144 |
+
}
|
| 145 |
+
SELECTED_GPU = _GPU_MAP.get(GPU_CONFIG, "A10G")
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# =============================================================================
|
| 149 |
+
# Pre-download Model Weights (reduces warm start time significantly)
|
| 150 |
+
# =============================================================================
|
| 151 |
+
|
| 152 |
+
@app.function(
|
| 153 |
+
image=vllm_image,
|
| 154 |
+
volumes={"/root/.cache/huggingface": hf_cache_vol},
|
| 155 |
+
timeout=30 * MINUTES,
|
| 156 |
+
)
|
| 157 |
+
def download_model():
|
| 158 |
+
"""
|
| 159 |
+
Pre-download the model weights to the volume cache.
|
| 160 |
+
Run this once with: modal run gpt_oss_inference.py::download_model
|
| 161 |
+
This will cache the weights and make subsequent starts much faster.
|
| 162 |
+
"""
|
| 163 |
+
from huggingface_hub import snapshot_download
|
| 164 |
+
|
| 165 |
+
print(f"📥 Downloading model weights for {MODEL_NAME}...")
|
| 166 |
+
print(f" Revision: {MODEL_REVISION}")
|
| 167 |
+
|
| 168 |
+
snapshot_download(
|
| 169 |
+
MODEL_NAME,
|
| 170 |
+
revision=MODEL_REVISION,
|
| 171 |
+
local_dir=f"/root/.cache/huggingface/hub/models--{MODEL_NAME.replace('/', '--')}",
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
print("✅ Model weights downloaded and cached!")
|
| 175 |
+
print(" Future container starts will use the cached weights.")
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
@app.function(
|
| 179 |
+
image=vllm_image,
|
| 180 |
+
gpu=SELECTED_GPU,
|
| 181 |
+
scaledown_window=SCALEDOWN_WINDOW,
|
| 182 |
+
timeout=30 * MINUTES,
|
| 183 |
+
volumes={
|
| 184 |
+
"/root/.cache/huggingface": hf_cache_vol,
|
| 185 |
+
"/root/.cache/vllm": vllm_cache_vol,
|
| 186 |
+
},
|
| 187 |
+
)
|
| 188 |
+
@modal.concurrent(max_inputs=MAX_INPUTS)
|
| 189 |
+
@modal.web_server(port=VLLM_PORT, startup_timeout=30 * MINUTES)
|
| 190 |
+
def serve():
|
| 191 |
+
"""Start the vLLM server with GPT-OSS model."""
|
| 192 |
+
import subprocess
|
| 193 |
+
|
| 194 |
+
cmd = [
|
| 195 |
+
"vllm",
|
| 196 |
+
"serve",
|
| 197 |
+
"--uvicorn-log-level=info",
|
| 198 |
+
MODEL_NAME,
|
| 199 |
+
"--revision",
|
| 200 |
+
MODEL_REVISION,
|
| 201 |
+
"--served-model-name",
|
| 202 |
+
"llm", # Serve model as "llm" - this is what clients expect
|
| 203 |
+
"--host",
|
| 204 |
+
"0.0.0.0",
|
| 205 |
+
"--port",
|
| 206 |
+
str(VLLM_PORT),
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
# enforce-eager disables both Torch compilation and CUDA graph capture
|
| 210 |
+
# default is no-enforce-eager. see the --compilation-config flag for tighter control
|
| 211 |
+
cmd += ["--enforce-eager" if FAST_BOOT else "--no-enforce-eager"]
|
| 212 |
+
|
| 213 |
+
if not FAST_BOOT: # CUDA graph capture is only used with `--no-enforce-eager`
|
| 214 |
+
cmd += [
|
| 215 |
+
"-O.cudagraph_capture_sizes="
|
| 216 |
+
+ str(CUDA_GRAPH_CAPTURE_SIZES).replace(" ", "")
|
| 217 |
+
]
|
| 218 |
+
|
| 219 |
+
# Data type optimization: use float16 for A10G/L4 (SM86) to avoid Marlin kernel warning
|
| 220 |
+
# bf16 is optimized for SM90+ (H100), fp16 is better for Ampere architecture
|
| 221 |
+
if USE_FLOAT16:
|
| 222 |
+
cmd += ["--dtype", "float16"]
|
| 223 |
+
else:
|
| 224 |
+
cmd += ["--dtype", "bfloat16"]
|
| 225 |
+
|
| 226 |
+
# Limit context length to speed up startup and reduce memory allocation
|
| 227 |
+
cmd += ["--max-model-len", str(MAX_MODEL_LEN)]
|
| 228 |
+
|
| 229 |
+
# Disable custom all-reduce for single GPU (reduces startup overhead)
|
| 230 |
+
if N_GPU == 1:
|
| 231 |
+
cmd += ["--disable-custom-all-reduce"]
|
| 232 |
+
|
| 233 |
+
# Enable prefix caching for faster subsequent requests
|
| 234 |
+
cmd += ["--enable-prefix-caching"]
|
| 235 |
+
|
| 236 |
+
# Trust remote code for GPT-OSS models
|
| 237 |
+
cmd += ["--trust-remote-code"]
|
| 238 |
+
|
| 239 |
+
# Optimize loading format for faster startup
|
| 240 |
+
cmd += ["--load-format", "auto"] # Auto-detect best format
|
| 241 |
+
|
| 242 |
+
# assume multiple GPUs are for splitting up large matrix multiplications
|
| 243 |
+
cmd += ["--tensor-parallel-size", str(N_GPU)]
|
| 244 |
+
|
| 245 |
+
# Additional optimizations for faster startup and inference
|
| 246 |
+
# Disable usage stats collection to speed up startup
|
| 247 |
+
cmd += ["--disable-log-stats"]
|
| 248 |
+
|
| 249 |
+
# Use swap space if needed (helps with memory pressure on smaller GPUs)
|
| 250 |
+
cmd += ["--swap-space", "4"] # 4GB swap space
|
| 251 |
+
|
| 252 |
+
print(f"Starting vLLM server with command: {' '.join(cmd)}")
|
| 253 |
+
|
| 254 |
+
subprocess.Popen(" ".join(cmd), shell=True)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
# =============================================================================
|
| 258 |
+
# Local Test Entrypoint
|
| 259 |
+
# =============================================================================
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@app.local_entrypoint()
|
| 263 |
+
async def test(test_timeout=30 * MINUTES, user_content=None, twice=True):
|
| 264 |
+
"""
|
| 265 |
+
Test the deployed server with a sample prompt.
|
| 266 |
+
|
| 267 |
+
Args:
|
| 268 |
+
test_timeout: Maximum time to wait for server health
|
| 269 |
+
user_content: Custom prompt to send (default: SVD explanation)
|
| 270 |
+
twice: Whether to send a second request
|
| 271 |
+
"""
|
| 272 |
+
url = serve.get_web_url()
|
| 273 |
+
|
| 274 |
+
system_prompt = {
|
| 275 |
+
"role": "system",
|
| 276 |
+
"content": f"""You are ChatModal, a large language model trained by Modal.
|
| 277 |
+
Knowledge cutoff: 2024-06
|
| 278 |
+
Current date: {datetime.now(timezone.utc).date()}
|
| 279 |
+
Reasoning: low
|
| 280 |
+
# Valid channels: analysis, commentary, final. Channel must be included for every message.
|
| 281 |
+
Calls to these tools must go to the commentary channel: 'functions'.""",
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
if user_content is None:
|
| 285 |
+
user_content = "Explain what the Singular Value Decomposition is."
|
| 286 |
+
|
| 287 |
+
messages = [ # OpenAI chat format
|
| 288 |
+
system_prompt,
|
| 289 |
+
{"role": "user", "content": user_content},
|
| 290 |
+
]
|
| 291 |
+
|
| 292 |
+
async with aiohttp.ClientSession(base_url=url) as session:
|
| 293 |
+
print(f"Running health check for server at {url}")
|
| 294 |
+
async with session.get("/health", timeout=test_timeout - 1 * MINUTES) as resp:
|
| 295 |
+
up = resp.status == 200
|
| 296 |
+
assert up, f"Failed health check for server at {url}"
|
| 297 |
+
print(f"Successful health check for server at {url}")
|
| 298 |
+
|
| 299 |
+
print(f"Sending messages to {url}:", *messages, sep="\n\t")
|
| 300 |
+
await _send_request(session, "llm", messages)
|
| 301 |
+
|
| 302 |
+
if twice:
|
| 303 |
+
messages[0]["content"] += "\nTalk like a pirate, matey."
|
| 304 |
+
print(f"Re-sending messages to {url}:", *messages, sep="\n\t")
|
| 305 |
+
await _send_request(session, "llm", messages)
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
async def _send_request(
|
| 309 |
+
session: aiohttp.ClientSession, model: str, messages: list
|
| 310 |
+
) -> None:
|
| 311 |
+
"""Send a streaming request to the vLLM server."""
|
| 312 |
+
# `stream=True` tells an OpenAI-compatible backend to stream chunks
|
| 313 |
+
payload: dict[str, Any] = {"messages": messages, "model": model, "stream": True}
|
| 314 |
+
|
| 315 |
+
headers = {"Content-Type": "application/json", "Accept": "text/event-stream"}
|
| 316 |
+
|
| 317 |
+
t = time.perf_counter()
|
| 318 |
+
async with session.post(
|
| 319 |
+
"/v1/chat/completions", json=payload, headers=headers, timeout=10 * MINUTES
|
| 320 |
+
) as resp:
|
| 321 |
+
async for raw in resp.content:
|
| 322 |
+
resp.raise_for_status()
|
| 323 |
+
# extract new content and stream it
|
| 324 |
+
line = raw.decode().strip()
|
| 325 |
+
if not line or line == "data: [DONE]":
|
| 326 |
+
continue
|
| 327 |
+
if line.startswith("data: "): # SSE prefix
|
| 328 |
+
line = line[len("data: ") :]
|
| 329 |
+
|
| 330 |
+
chunk = json.loads(line)
|
| 331 |
+
assert (
|
| 332 |
+
chunk["object"] == "chat.completion.chunk"
|
| 333 |
+
) # or something went horribly wrong
|
| 334 |
+
delta = chunk["choices"][0]["delta"]
|
| 335 |
+
|
| 336 |
+
if "content" in delta:
|
| 337 |
+
print(delta["content"], end="") # print the content as it comes in
|
| 338 |
+
elif "reasoning_content" in delta:
|
| 339 |
+
print(delta["reasoning_content"], end="")
|
| 340 |
+
elif not delta:
|
| 341 |
+
print()
|
| 342 |
+
else:
|
| 343 |
+
raise ValueError(f"Unsupported response delta: {delta}")
|
| 344 |
+
print("")
|
| 345 |
+
print(f"Time to Last Token: {time.perf_counter() - t:.2f} seconds")
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# =============================================================================
|
| 349 |
+
# Utility Functions
|
| 350 |
+
# =============================================================================
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
def get_endpoint_url() -> str:
|
| 354 |
+
"""Get the deployed endpoint URL."""
|
| 355 |
+
return serve.get_web_url()
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
if __name__ == "__main__":
|
| 359 |
+
print("Run this script with Modal:")
|
| 360 |
+
print(" modal run gpt_oss_inference.py # Test the server")
|
| 361 |
+
print(" modal deploy gpt_oss_inference.py # Deploy to production")
|
| 362 |
+
|
modal/requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modal CLI and dependencies
|
| 2 |
+
modal>=0.64.0
|
| 3 |
+
aiohttp>=3.9.0
|
| 4 |
+
|