drosatos commited on
Commit
9434d3d
·
0 Parent(s):

Deploy ChatGPT MCP Server

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +6 -0
  2. .github/FUNDING.yml +3 -0
  3. .github/ISSUE_TEMPLATE/bug_report.yml +37 -0
  4. .github/ISSUE_TEMPLATE/feature_request.yml +29 -0
  5. .github/renovate.json +13 -0
  6. .github/workflows/ci.yml +67 -0
  7. .github/workflows/deploy-hf-space.yml +38 -0
  8. .gitignore +56 -0
  9. .npmrc +5 -0
  10. .nvmrc +1 -0
  11. .vscode/extensions.json +8 -0
  12. .vscode/launch.json +15 -0
  13. .vscode/project.code-workspace +36 -0
  14. .vscode/settings.json +7 -0
  15. DEPLOYMENT.md +335 -0
  16. Dockerfile +57 -0
  17. README.md +55 -0
  18. RUN_LOCAL.sh +54 -0
  19. SUBMISSION.md +120 -0
  20. apps/eu-ai-act-agent/.gitignore +52 -0
  21. apps/eu-ai-act-agent/.python-version +2 -0
  22. apps/eu-ai-act-agent/API.md +579 -0
  23. apps/eu-ai-act-agent/ARCHITECTURE.md +674 -0
  24. apps/eu-ai-act-agent/DEPLOYMENT.md +302 -0
  25. apps/eu-ai-act-agent/Dockerfile +61 -0
  26. apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp +57 -0
  27. apps/eu-ai-act-agent/EXAMPLES.md +517 -0
  28. apps/eu-ai-act-agent/QUICKSTART.md +371 -0
  29. apps/eu-ai-act-agent/README.md +502 -0
  30. apps/eu-ai-act-agent/biome.json +30 -0
  31. apps/eu-ai-act-agent/package.json +47 -0
  32. apps/eu-ai-act-agent/pyproject.toml +24 -0
  33. apps/eu-ai-act-agent/requirements.txt +7 -0
  34. apps/eu-ai-act-agent/src/.mcp_url +1 -0
  35. apps/eu-ai-act-agent/src/agent/index.ts +819 -0
  36. apps/eu-ai-act-agent/src/agent/prompts.ts +533 -0
  37. apps/eu-ai-act-agent/src/chatgpt_app.py +1410 -0
  38. apps/eu-ai-act-agent/src/gradio_app.py +1502 -0
  39. apps/eu-ai-act-agent/src/server.ts +1235 -0
  40. apps/eu-ai-act-agent/src/types/index.ts +43 -0
  41. apps/eu-ai-act-agent/start.sh +127 -0
  42. apps/eu-ai-act-agent/tsconfig.json +22 -0
  43. apps/eu-ai-act-agent/tsup.config.ts +14 -0
  44. apps/eu-ai-act-agent/tsx +0 -0
  45. apps/eu-ai-act-agent/uv.lock +0 -0
  46. biome.json +43 -0
  47. modal/README.md +237 -0
  48. modal/deploy.sh +84 -0
  49. modal/gpt_oss_inference.py +362 -0
  50. 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
+