mishrabp commited on
Commit
e4743bc
·
verified ·
1 Parent(s): ff602a8

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +22 -0
  2. Dockerfile +22 -0
  3. README.md +27 -10
  4. pyproject.toml +201 -0
  5. src/accessibility_v1/Dockerfile +34 -0
  6. src/accessibility_v1/app.py +187 -0
  7. src/accessibility_v1/mcp/axe.min.js +0 -0
  8. src/accessibility_v1/mcp/server.py +373 -0
  9. src/accessibility_v1/notebook/accessibility.ipynb +768 -0
  10. src/accessibility_v1/notebook/output/accessibility_audit_20251112_061159.html +252 -0
  11. src/accessibility_v1/output/accessibility_dashboard_20251112_065648.html +171 -0
  12. src/accessibility_v1/output/accessibility_dashboard_20251112_070331.html +171 -0
  13. src/accessibility_v1/output/accessibility_dashboard_20251112_070840.html +171 -0
  14. src/accessibility_v1/output/accessibility_dashboard_20251112_071804.html +188 -0
  15. src/accessibility_v1/output/accessibility_dashboard_20251112_072100.html +188 -0
  16. src/accessibility_v1/output/accessibility_dashboard_20251211_051059.html +188 -0
  17. src/accessibility_v1/output/accessibility_dashboard_20251211_051323.html +188 -0
  18. src/accessibility_v1/output/accessibility_dashboard_20251211_052404.html +188 -0
  19. src/accessibility_v1/templates/dashboard_template.html +188 -0
  20. src/accessibility_v2/app.py +27 -0
  21. src/accessibility_v2/layers/action.py +20 -0
  22. src/accessibility_v2/layers/cognition.py +46 -0
  23. src/accessibility_v2/layers/perception.py +33 -0
  24. src/accessibility_v2/patterns/orchestrator.py +30 -0
  25. src/accessibility_v2/tools/axe.min.js +0 -0
  26. src/accessibility_v2/tools/web_auditor.py +88 -0
  27. src/chatbot_v1/Dockerfile +35 -0
  28. src/chatbot_v1/README.md +223 -0
  29. src/chatbot_v1/aagents/__init__.py +0 -0
  30. src/chatbot_v1/aagents/input_validation_agent.py +58 -0
  31. src/chatbot_v1/aagents/orchestrator_agent.py +96 -0
  32. src/chatbot_v1/app.py +317 -0
  33. src/chatbot_v1/core/__init__.py +4 -0
  34. src/chatbot_v1/core/model.py +36 -0
  35. src/chatbot_v1/prompts/economic_news.txt +27 -0
  36. src/chatbot_v1/prompts/entertainment_updates.txt +26 -0
  37. src/chatbot_v1/prompts/india_news.txt +26 -0
  38. src/chatbot_v1/prompts/market_sentiment.txt +34 -0
  39. src/chatbot_v1/prompts/news_headlines.txt +28 -0
  40. src/chatbot_v1/prompts/odia_news.txt +26 -0
  41. src/chatbot_v1/prompts/trade_recommendation.txt +40 -0
  42. src/chatbot_v1/prompts/upcoming_earnings.txt +27 -0
  43. src/chatbot_v1/trace_config.py +37 -0
  44. src/chatbot_v2/Dockerfile +35 -0
  45. src/chatbot_v2/README.md +223 -0
  46. src/chatbot_v2/app.py +274 -0
  47. src/chatbot_v2/layers/__init__.py +0 -0
  48. src/chatbot_v2/layers/action.py +71 -0
  49. src/chatbot_v2/layers/cognition.py +111 -0
  50. src/chatbot_v2/layers/memory.py +20 -0
.gitattributes CHANGED
@@ -33,3 +33,25 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ src/finadvisor/3030_Module04_Demonstration01_v1.0.pdf filter=lfs diff=lfs merge=lfs -text
37
+ src/image-generator/src/image-generator/generated_images/06767ab4-8205-478b-9387-fd94f3929969.png filter=lfs diff=lfs merge=lfs -text
38
+ src/image-generator/src/image-generator/generated_images/1214aea0-d1d9-4125-b619-2cb81fa54d7e.png filter=lfs diff=lfs merge=lfs -text
39
+ src/image-generator/src/image-generator/generated_images/1629816f-fbfd-4e2d-ac0c-18b39172832c.png filter=lfs diff=lfs merge=lfs -text
40
+ src/image-generator/src/image-generator/generated_images/26c96916-52a4-44c8-8f5f-437bba078205.png filter=lfs diff=lfs merge=lfs -text
41
+ src/image-generator/src/image-generator/generated_images/272868d3-16fc-474b-9398-c8bc6dffcc21.png filter=lfs diff=lfs merge=lfs -text
42
+ src/image-generator/src/image-generator/generated_images/3f645a42-5fd2-4c79-bd4a-62710d905fbb.png filter=lfs diff=lfs merge=lfs -text
43
+ src/image-generator/src/image-generator/generated_images/52665623-7fa0-426b-91ca-21f418471674.png filter=lfs diff=lfs merge=lfs -text
44
+ src/image-generator/src/image-generator/generated_images/6490bab7-6c83-4f6c-b138-cc962a806817.png filter=lfs diff=lfs merge=lfs -text
45
+ src/image-generator/src/image-generator/generated_images/6869b3cb-e2fa-4224-9b2f-3229a84e49a9.png filter=lfs diff=lfs merge=lfs -text
46
+ src/image-generator/src/image-generator/generated_images/6cb30ef4-6089-4899-a6d2-43217a762069.png filter=lfs diff=lfs merge=lfs -text
47
+ src/image-generator/src/image-generator/generated_images/6f8885b5-f4b6-4286-96a5-c27e57cf84e1.png filter=lfs diff=lfs merge=lfs -text
48
+ src/image-generator/src/image-generator/generated_images/a38ab0db-de95-4d0b-9b9a-6bb97a73e364.png filter=lfs diff=lfs merge=lfs -text
49
+ src/image-generator/src/image-generator/generated_images/a5c56b0d-235e-469c-953f-3a18a988e99f.png filter=lfs diff=lfs merge=lfs -text
50
+ src/image-generator/src/image-generator/generated_images/ab435c70-86b5-4262-9e73-2170f5de5dd1.png filter=lfs diff=lfs merge=lfs -text
51
+ src/image-generator/src/image-generator/generated_images/b0dff864-d52b-43a5-8cca-217c967b9faa.png filter=lfs diff=lfs merge=lfs -text
52
+ src/image-generator/src/image-generator/generated_images/d0466afb-74e1-4d9c-9894-a2adbbe847c3.png filter=lfs diff=lfs merge=lfs -text
53
+ src/image-generator/src/image-generator/generated_images/d4d748a3-0a48-4e3e-a9d9-b05ad7ae9c2f.png filter=lfs diff=lfs merge=lfs -text
54
+ src/image-generator/src/image-generator/generated_images/e92b2025-8bd4-49a5-9ea0-f3ae9d545947.png filter=lfs diff=lfs merge=lfs -text
55
+ src/image-generator/src/image-generator/generated_images/efa13dc8-abde-4985-a0d9-92122b1a9136.png filter=lfs diff=lfs merge=lfs -text
56
+ src/image-generator/src/image-generator/generated_images/f54ad375-d70e-49ef-94b5-7d3320037da8.png filter=lfs diff=lfs merge=lfs -text
57
+ src/interview-assistant/data/interview_rag_db/chroma.sqlite3 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ FROM python:3.12-slim
3
+
4
+ WORKDIR /app
5
+
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ git \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ COPY pyproject.toml .
11
+ RUN pip install --no-cache-dir .
12
+
13
+ COPY src/mcp-rag-secure ./src/mcp-rag-secure
14
+
15
+ ENV PYTHONPATH=/app/src
16
+
17
+ # Create directory for ChromaDB
18
+ RUN mkdir -p src/mcp-rag-secure/chroma_db && chmod 777 src/mcp-rag-secure/chroma_db
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["python", "src/mcp-rag-secure/server.py", "--transport", "sse", "--port", "7860", "--host", "0.0.0.0"]
README.md CHANGED
@@ -1,10 +1,27 @@
1
- ---
2
- title: Mcp Rag Secure
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ---
3
+ title: MCP Secure RAG
4
+ emoji: 🔒
5
+ colorFrom: pink
6
+ colorTo: red
7
+ sdk: docker
8
+ pinned: false
9
+ ---
10
+
11
+ # MCP Secure Multi-Tenant RAG Server
12
+
13
+ This is a Model Context Protocol (MCP) server for secure, tenant-isolated Retrieval-Augmented Generation.
14
+
15
+ ## Tools
16
+ - `ingest_document`: Add documents with strict tenant ID metadata.
17
+ - `query_knowledge_base`: Query documents filtered by tenant ID.
18
+ - `delete_tenant_data`: Wipe data for a specific tenant.
19
+
20
+ ## Security
21
+ - Uses ChromaDB for vector storage.
22
+ - All operations require a `tenant_id` to ensure data isolation.
23
+
24
+ ## Running Locally
25
+ ```bash
26
+ python src/mcp-rag-secure/server.py
27
+ ```
pyproject.toml ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "agenticai"
3
+ version = "0.1.0"
4
+ description = "Agentic AI project"
5
+ readme = "README.md"
6
+ requires-python = "==3.12.*"
7
+
8
+ dependencies = [
9
+ # =======================
10
+ # LLM PROVIDERS / SDKs
11
+ # =======================
12
+ "openai>=2.8.1",
13
+ "openai-agents>=0.5.1",
14
+ "anthropic>=0.49.0",
15
+ "langchain-openai>=1.0.3",
16
+ "langchain-anthropic>=1.1.0",
17
+ "langchain_huggingface>=1.1.0",
18
+ "langchain_ollama>=1.0.0",
19
+ "langchain_google_genai>=3.0.3",
20
+ "langchain_groq>=1.0.1",
21
+
22
+
23
+ # =======================
24
+ # LANGCHAIN / LANGGRAPH
25
+ # =======================
26
+ "langchain>=1.0.7",
27
+ "langchain-community>=0.4.1",
28
+ "langgraph>=1.0.3",
29
+ "langgraph-checkpoint-sqlite>=3.0.0",
30
+ "langsmith>=0.4.43",
31
+ "langchain-text-splitters>=1.0.0",
32
+ "langchain-chroma>=1.0.0",
33
+ "html2text>=2025.4.15",
34
+ "traceloop-sdk>=0.33.0",
35
+
36
+ # =======================
37
+ # MICROSOFT AGENT FRAMEWORK
38
+ # =======================
39
+ #"agent-framework==1.0.0b251204",
40
+ #"agent-framework-azure-ai==1.0.0b251204",
41
+ #"azure-ai-projects",
42
+ #"azure-ai-agents",
43
+ #"azure-ai-agents>=1.2.0b5",
44
+ #"agent-framework-azure-ai",
45
+
46
+ # =======================
47
+ # VECTOR DB / INDEXING
48
+ # =======================
49
+ "faiss-cpu>=1.13.0",
50
+ "chromadb>=0.4.0",
51
+ "sentence-transformers>=5.1.2",
52
+ "pymupdf",
53
+ "pypdf>=6.3.0",
54
+ "pypdf2>=3.0.1",
55
+ "arxiv>=2.3.1",
56
+ "wikipedia>=1.4.0",
57
+
58
+ # =======================
59
+ # AUTOGEN
60
+ # =======================
61
+ "autogen-agentchat==0.4.7",
62
+ "autogen-ext[grpc,mcp,ollama,openai]==0.4.7",
63
+ "asyncio",
64
+ "phidata>=2.0.0",
65
+
66
+ # =======================
67
+ # MCP
68
+ # =======================
69
+ "mcp-server-fetch>=2025.1.17",
70
+ "mcp[cli]>=1.21.2",
71
+
72
+ # =======================
73
+ # NETWORKING / UTILITIES
74
+ # =======================
75
+ "psutil>=7.0.0",
76
+ "python-dotenv>=1.0.1",
77
+ "requests>=2.32.3",
78
+ "aiohttp>=3.8.5",
79
+ "httpx>=0.28.1",
80
+ "speedtest-cli>=2.1.3",
81
+ "logfire",
82
+ "google-search-results",
83
+ "smithery>=0.4.4",
84
+ "sendgrid",
85
+
86
+ # =======================
87
+ # WEB SCRAPING
88
+ # =======================
89
+ "playwright>=1.51.0",
90
+ "beautifulsoup4>=4.12.3",
91
+ "lxml>=5.3.1",
92
+
93
+ # =======================
94
+ # FINANCE / NLP
95
+ # =======================
96
+ "yfinance>=0.2.66",
97
+ "textblob>=0.17.1",
98
+ "polygon-api-client>=1.16.3",
99
+
100
+ # =======================
101
+ # VISUAL / UI / PDF
102
+ # =======================
103
+ "plotly>=6.5.0",
104
+ "streamlit>=1.51.0",
105
+ "reportlab>=4.4.5",
106
+ "fastapi",
107
+ "Pillow",
108
+ "python-docx",
109
+ "matplotlib",
110
+ "fpdf",
111
+ "extra-streamlit-components",
112
+ "nest_asyncio",
113
+
114
+ # =======================
115
+ # AUDIO / VIDEO
116
+ # =======================
117
+ "yt_dlp>=2025.11.12",
118
+ "openai-whisper==20240930",
119
+ "numba==0.59.0",
120
+ "llvmlite==0.42.0",
121
+
122
+ # =======================
123
+ # MACHINE LEARNING
124
+ # =======================
125
+ "scikit-learn>=1.7.2",
126
+ "huggingface_hub<=1.3.2",
127
+ "datasets>=4.4.1",
128
+
129
+ # =======================
130
+ # IPYNB SUPPORT
131
+ # =======================
132
+ "ipykernel>=7.1.0",
133
+
134
+ # =======================
135
+ # TOOLS
136
+ # =======================
137
+ "ddgs>=9.9.2",
138
+ "duckduckgo_search",
139
+ "azure-identity>=1.25.1",
140
+ "azure-mgmt-resource>=23.0.1",
141
+ "azure-mgmt-compute>=30.3.0",
142
+ "azure-mgmt-monitor>=6.0.2",
143
+ "azure-monitor-query>=1.2.0",
144
+ "PyGithub>=2.1.1",
145
+
146
+ # =======================
147
+ # OBSERVABILITY
148
+ # =======================
149
+ "openinference-instrumentation-autogen>=0.1.0",
150
+ "openinference-instrumentation-openai>=0.1.15",
151
+ "opentelemetry-sdk>=1.20.0",
152
+ "opentelemetry-exporter-otlp>=1.20.0",
153
+ "opentelemetry-api>=1.20.0",
154
+
155
+ # =======================
156
+ # Google Authentication
157
+ # =======================
158
+ "google-auth>=2.22.0",
159
+ "google-auth-oauthlib>=0.4.6",
160
+ "google-auth-httplib2>=0.1.0",
161
+ "autoflake>=1.5.0",
162
+
163
+ ]
164
+
165
+ [dependency-groups]
166
+ dev = [
167
+ "pytest>=8.3.3",
168
+ "ipykernel>=7.1.0",
169
+ "pytest-asyncio",
170
+ ]
171
+
172
+ # ============================================================
173
+ # BUILD SYSTEM
174
+ # ============================================================
175
+ # Defines how to build the project.
176
+ # We use setuptools as the build backend, ensuring consistent packaging.
177
+ [build-system]
178
+ requires = ["setuptools>=80.9.0"]
179
+ build-backend = "setuptools.build_meta"
180
+
181
+ # ============================================================
182
+ # PACKAGING & DISCOVERY
183
+ # ============================================================
184
+ # Tells setuptools where to find the source code.
185
+ # This makes 'common' and 'src' importable when installed (pip install -e .).
186
+ [tool.setuptools.packages.find]
187
+ where = ["."] # Look in the project root
188
+ include = ["common*", "src*"] # Treat 'common' and 'src' folders as packages
189
+
190
+
191
+ # ============================================================
192
+ # PYTEST SETTINGS
193
+ # ============================================================
194
+ # Configures the test runner to automatically find code.
195
+ [tool.pytest.ini_options]
196
+ # Adds 'src' and 'common' to the python path during tests.
197
+ # This allows tests to import modules (e.g., 'import travel_agent')
198
+ # just like the apps do locally, preventing ModuleNotFoundError.
199
+ pythonpath = ["src", "common"]
200
+ testpaths = ["tests"] # Only look for tests in the 'tests' directory
201
+ addopts = "-q" # Run in quiet mode (less verbose output)
src/accessibility_v1/Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile
2
+ FROM python:3.11-slim
3
+
4
+ apt-get update
5
+ apt-get install -y \
6
+ libnss3 \
7
+ libnspr4 \
8
+ libasound2 \
9
+ libatk1.0-0 \
10
+ libx11-xcb1 \
11
+ libxcomposite1 \
12
+ libxdamage1 \
13
+ libxrandr2 \
14
+ libgbm1 \
15
+ libpango-1.0-0 \
16
+ libcups2 \
17
+ libxss1 \
18
+ libxshmfence1
19
+
20
+
21
+ WORKDIR /app
22
+
23
+ COPY requirements.txt /app/
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # copy server and axe
27
+ COPY server.py /app/
28
+ COPY axe.min.js /app/
29
+
30
+ # install playwright browsers
31
+ RUN playwright install --with-deps
32
+
33
+ EXPOSE 8000
34
+ CMD ["python", "server.py"]
src/accessibility_v1/app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import os
3
+ import nest_asyncio
4
+ import asyncio
5
+ import json
6
+ from datetime import datetime
7
+ import streamlit as st
8
+ from agents import Agent, Runner, trace, OpenAIChatCompletionsModel
9
+ from agents.mcp import MCPServerStdio
10
+
11
+ import aiohttp
12
+ from xml.etree import ElementTree as ET
13
+ from openai import AsyncOpenAI
14
+ import urllib.parse
15
+ import ipaddress
16
+ import socket
17
+
18
+ # Allow nested async in Jupyter/Streamlit
19
+ nest_asyncio.apply()
20
+ nest_asyncio.apply()
21
+
22
+ TEMPLATE_PATH = os.path.abspath("templates/dashboard_template.html")
23
+
24
+
25
+ def is_safe_url(url: str) -> bool:
26
+ """
27
+ Basic SSRF protection: allow only http(s) URLs whose resolved IPs are not
28
+ private, loopback, link-local, or reserved.
29
+ """
30
+ try:
31
+ parsed = urllib.parse.urlparse(url)
32
+ if parsed.scheme not in ("http", "https"):
33
+ return False
34
+ if not parsed.hostname:
35
+ return False
36
+ # Resolve hostname to IPs
37
+ addrinfo_list = socket.getaddrinfo(parsed.hostname, None)
38
+ for family, _, _, _, sockaddr in addrinfo_list:
39
+ if family == socket.AF_INET:
40
+ ip_str = sockaddr[0]
41
+ elif family == socket.AF_INET6:
42
+ ip_str = sockaddr[0]
43
+ else:
44
+ continue
45
+ ip_obj = ipaddress.ip_address(ip_str)
46
+ if (
47
+ ip_obj.is_private
48
+ or ip_obj.is_loopback
49
+ or ip_obj.is_link_local
50
+ or ip_obj.is_reserved
51
+ or ip_obj.is_multicast
52
+ ):
53
+ return False
54
+ return True
55
+ except Exception:
56
+ # On any error parsing or resolving, consider the URL unsafe
57
+ return False
58
+
59
+
60
+ async def fetch_sitemap_urls(base_url: str):
61
+ if not is_safe_url(base_url):
62
+ raise ValueError(f"Unsafe or invalid URL provided: {base_url}")
63
+ sitemap_url = base_url.rstrip("/") + "/sitemap.xml"
64
+ urls = []
65
+ async with aiohttp.ClientSession() as session:
66
+ try:
67
+ async with session.get(sitemap_url) as resp:
68
+ if resp.status != 200:
69
+ return [base_url]
70
+ xml_text = await resp.text()
71
+ root = ET.fromstring(xml_text)
72
+ urls = [elem.text for elem in root.findall(".//{*}loc")]
73
+ except:
74
+ return [base_url]
75
+ return urls if urls else [base_url]
76
+
77
+
78
+ async def run_accessibility_audit(base_url: str):
79
+ script_path = os.path.abspath("mcp/server.py")
80
+ if not os.path.exists(script_path):
81
+ st.error(f"MCP server not found: {script_path}")
82
+ return None
83
+
84
+ params = {"command": "uv", "args": ["run", script_path]}
85
+ # model = "gpt-4.1-mini"
86
+
87
+ GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
88
+ google_api_key = os.getenv('GOOGLE_API_KEY')
89
+ gemini_client = AsyncOpenAI(base_url=GEMINI_BASE_URL, api_key=google_api_key)
90
+ model = OpenAIChatCompletionsModel(model="gemini-2.0-flash", openai_client=gemini_client)
91
+
92
+ try:
93
+ async with MCPServerStdio(params=params, client_session_timeout_seconds=180) as accessibility_server:
94
+ urls_to_audit = await fetch_sitemap_urls(base_url)
95
+ audit_results = {}
96
+ page_summaries = {}
97
+
98
+ progress_placeholder = st.empty() # dynamic progress updates
99
+
100
+ audit_instructions = (
101
+ "You are an AI assistant specialized in ADA/WCAG compliance. "
102
+ "Audit a webpage and produce a Markdown report including all rules with columns: "
103
+ "Level, Rule, Pass/Fail, Reason, Recommendation."
104
+ )
105
+
106
+ for idx, url in enumerate(urls_to_audit, start=1):
107
+ progress_placeholder.info(f"🔹 Auditing page {idx}/{len(urls_to_audit)}: {url}")
108
+ audit_agent = Agent(
109
+ name="accessibility_agent",
110
+ instructions=audit_instructions,
111
+ model=model,
112
+ mcp_servers=[accessibility_server]
113
+ )
114
+ with trace(f"audit_{url}"):
115
+ result = await Runner.run(audit_agent, f"Audit {url} for ADA/WCAG compliance.")
116
+ markdown_output = result.final_output if result and result.final_output else ""
117
+ audit_results[url] = markdown_output
118
+
119
+ # Compute per-page summary
120
+ passed = failed = warning = 0
121
+ for line in markdown_output.splitlines():
122
+ if "|" in line:
123
+ parts = [p.strip() for p in line.split("|")]
124
+ if len(parts) >= 5:
125
+ status = parts[2].lower()
126
+ if "pass" in status:
127
+ passed += 1
128
+ elif "fail" in status:
129
+ failed += 1
130
+ elif "warn" in status or "warning" in status:
131
+ warning += 1
132
+ page_summaries[url] = {"pass": passed, "fail": failed, "warning": warning}
133
+
134
+ # Prepare JSON data for template
135
+ audit_json = []
136
+ for page, md in audit_results.items():
137
+ rows = []
138
+ for line in md.splitlines():
139
+ if "|" in line:
140
+ parts = [p.strip() for p in line.split("|")]
141
+ if len(parts) >= 5:
142
+ rows.append({
143
+ "level": parts[0],
144
+ "rule": parts[1],
145
+ "status": parts[2],
146
+ "reason": parts[3],
147
+ "recommendation": parts[4],
148
+ })
149
+ audit_json.append({
150
+ "page": page,
151
+ "rows": rows,
152
+ "summary": page_summaries.get(page, {"pass": 0, "fail": 0, "warning": 0})
153
+ })
154
+
155
+ # Load template
156
+ with open(TEMPLATE_PATH, "r", encoding="utf-8") as f:
157
+ template_html = f.read()
158
+
159
+ html_content = template_html.replace("<!--AUDIT_JSON_PLACEHOLDER-->", json.dumps(audit_json))
160
+
161
+ # Save HTML report
162
+ output_dir = os.path.abspath("output")
163
+ os.makedirs(output_dir, exist_ok=True)
164
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
165
+ output_file = os.path.join(output_dir, f"accessibility_dashboard_{timestamp}.html")
166
+ with open(output_file, "w", encoding="utf-8") as f:
167
+ f.write(html_content)
168
+
169
+ progress_placeholder.success(f"✅ Accessibility audit complete! Report saved to `{output_file}`.")
170
+
171
+ return html_content
172
+
173
+ except Exception as e:
174
+ st.error(f"Error running audit: {e}")
175
+ return None
176
+
177
+
178
+ # ------------------- Streamlit UI -------------------
179
+ st.set_page_config(page_title="Accessibility Dashboard", layout="wide")
180
+ st.title("🌐 Site Accessibility Audit Dashboard")
181
+
182
+ site_url = st.text_input("Enter the website URL", "https://oauthapp.azurewebsites.net")
183
+
184
+ if st.button("Run Audit") and site_url:
185
+ html_output = asyncio.run(run_accessibility_audit(site_url))
186
+ if html_output:
187
+ st.components.v1.html(html_output, height=900, scrolling=True)
src/accessibility_v1/mcp/axe.min.js ADDED
The diff for this file is too large to render. See raw diff
 
src/accessibility_v1/mcp/server.py ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mcp/server.py
2
+ import os
3
+ import asyncio
4
+ import json
5
+ from typing import Any, Dict, List, Optional
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from playwright.async_api import async_playwright, Page
9
+ import textwrap
10
+ import math
11
+
12
+
13
+
14
+ mcp = FastMCP("accessibility_server")
15
+
16
+ # Path to axe script (must exist next to this file or set AXE_JS_PATH env)
17
+ DEFAULT_AXE_PATH = os.path.join(os.path.dirname(__file__), "axe.min.js")
18
+ AXE_JS_PATH = os.environ.get("AXE_JS_PATH", DEFAULT_AXE_PATH)
19
+
20
+ # ---------- Helper: start browser, load page ----------
21
+ async def _open_page(url: str, timeout: int = 30000, wait_until: str = "load"):
22
+ """
23
+ Launch Playwright chromium, open url, return (playwright, browser, context, page).
24
+ Caller must close browser and playwright.
25
+ """
26
+ playwright = await async_playwright().start()
27
+ browser = await playwright.chromium.launch(args=["--no-sandbox"], headless=True)
28
+ context = await browser.new_context()
29
+ page = await context.new_page()
30
+ await page.goto(url, timeout=timeout, wait_until=wait_until)
31
+ # allow some dynamic content to settle
32
+ await asyncio.sleep(0.5)
33
+ return playwright, browser, context, page
34
+
35
+ # ---------- Helper: inject axe and run ----------
36
+ async def _ensure_axe(page: Page):
37
+ if not os.path.exists(AXE_JS_PATH):
38
+ raise FileNotFoundError(f"axe.min.js not found at {AXE_JS_PATH}. Download axe-core and place it there.")
39
+ with open(AXE_JS_PATH, "r", encoding="utf-8") as f:
40
+ axe_source = f.read()
41
+ # Add axe to page
42
+ await page.add_init_script(axe_source)
43
+ # also evaluate once to be safe
44
+ await page.evaluate("() => { window.__axe_injected = typeof axe !== 'undefined'; }")
45
+ ok = await page.evaluate("() => typeof axe !== 'undefined'")
46
+ if not ok:
47
+ # attempt to inject directly
48
+ await page.evaluate(axe_source + "\n() => {}")
49
+ # final check
50
+ ok2 = await page.evaluate("() => typeof axe !== 'undefined'")
51
+ if not ok2:
52
+ raise RuntimeError("Failed to inject axe into page")
53
+
54
+ async def _run_axe(page: Page, tags: Optional[List[str]] = None, rules: Optional[Dict[str, Any]] = None):
55
+ """
56
+ Run axe.run with wcag2a and wcag2aa tags by default.
57
+ Returns axe JSON result.
58
+ """
59
+ tags = tags or ["wcag2a", "wcag2aa"]
60
+ config = {"runOnly": {"type": "tag", "values": tags}}
61
+ if rules:
62
+ config["rules"] = rules
63
+ # axe.run returns a Promise; evaluate and return JSON-serializable result
64
+ result = await page.evaluate(
65
+ """(conf) => {
66
+ return axe.run(document, conf);
67
+ }""",
68
+ config,
69
+ )
70
+ return result
71
+
72
+ # ---------- Custom JS checks (run inside page) ----------
73
+ HEADINGS_JS = """() => {
74
+ const headings = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6')).map(h => ({tag:h.tagName.toLowerCase(), text:h.innerText.trim().slice(0,200)}));
75
+ const hasH1 = headings.some(h=>h.tag==='h1');
76
+ let last = 0;
77
+ const orderProblems = [];
78
+ for (let i=0;i<headings.length;i++){
79
+ const lvl = parseInt(headings[i].tag.slice(1));
80
+ if (last !== 0 && (lvl - last) > 1) orderProblems.push({index:i, tag:headings[i].tag, prev:last});
81
+ last = lvl;
82
+ }
83
+ return {headings, hasH1, orderProblems};
84
+ }"""
85
+
86
+ LANG_JS = """() => {
87
+ const html = document.documentElement;
88
+ const lang = html ? html.getAttribute('lang') : null;
89
+ return {lang, dir: html ? html.getAttribute('dir') : null};
90
+ }"""
91
+
92
+ IMAGES_JS = """() => {
93
+ const imgs = Array.from(document.images).map(i => ({src:i.currentSrc||i.src, alt:i.getAttribute('alt'), role:i.getAttribute('role'), ariaHidden:i.getAttribute('aria-hidden')}));
94
+ const missingAlt = imgs.filter(i => (i.alt === null || i.alt === '') && i.ariaHidden !== 'true');
95
+ return {count: imgs.length, missingAlt, sample: missingAlt.slice(0,10)};
96
+ }"""
97
+
98
+ FORMS_JS = """() => {
99
+ const inputs = Array.from(document.querySelectorAll('input,textarea,select')).map(el=>{
100
+ const id = el.id;
101
+ const label = id ? (document.querySelector("label[for='"+id+"']") ? document.querySelector("label[for='"+id+"']").innerText : null) : (el.closest('label') ? el.closest('label').innerText : null);
102
+ const ariaLabel = el.getAttribute('aria-label');
103
+ const ariaLabelledBy = el.getAttribute('aria-labelledby');
104
+ const name = el.getAttribute('name');
105
+ return {tag:el.tagName.toLowerCase(), type:el.type||null, id, name, label: label ? label.trim().slice(0,200) : null, ariaLabel, ariaLabelledBy};
106
+ });
107
+ const missing = inputs.filter(i => !i.label && !i.ariaLabel && !i.ariaLabelledBy);
108
+ return {inputsCount: inputs.length, missing, sample: missing.slice(0,10)};
109
+ }"""
110
+
111
+ ARIA_JS = """() => {
112
+ // detect common ARIA misuse: role mismatch, duplicate ids referenced by aria-labelledby, missing required aria attributes
113
+ const issues = [];
114
+ const all = Array.from(document.querySelectorAll('[role], [aria-labelledby], [aria-label], [aria-hidden], [aria-live]'));
115
+ for (const el of all) {
116
+ const role = el.getAttribute('role');
117
+ if (role && role === 'img' && !el.getAttribute('aria-label') && !el.querySelector('img')) {
118
+ issues.push({type:'role-img-missing-name', outer: el.outerHTML.slice(0,200)});
119
+ }
120
+ }
121
+ // duplicate id references
122
+ const labels = {};
123
+ Array.from(document.querySelectorAll('[id]')).forEach(e => { labels[e.id] = (labels[e.id]||0)+1; });
124
+ Object.keys(labels).filter(k => labels[k]>1).forEach(k => issues.push({type:'duplicate-id', id:k, count:labels[k]}));
125
+ return {count: all.length, issues: issues.slice(0,50)};
126
+ }"""
127
+
128
+ KEYBOARD_JS = """() => {
129
+ // collect focusable elements
130
+ const focusable = Array.from(document.querySelectorAll('a[href], button, input, textarea, select, [tabindex]'))
131
+ .filter(el => !el.hasAttribute('disabled') && el.getAttribute('tabindex') !== '-1');
132
+ // capture natural tab order (by DOM order)
133
+ const tabOrder = focusable.map(el => ({tag: el.tagName.toLowerCase(), id: el.id||null, class: el.className||null, tabindex: el.getAttribute('tabindex')||null}));
134
+ // detect tabindex>0 (bad practice)
135
+ const positiveTabindex = focusable.filter(el => el.hasAttribute('tabindex') && parseInt(el.getAttribute('tabindex')||'0')>0).map(e=>({tag:e.tagName,id:e.id}));
136
+ return {focusableCount: focusable.length, tabOrder: tabOrder.slice(0,200), positiveTabindex};
137
+ }"""
138
+
139
+ VIDEO_JS = """() => {
140
+ const videos = Array.from(document.querySelectorAll('video,audio'));
141
+ const out = [];
142
+ for (const v of videos) {
143
+ const tracks = Array.from(v.querySelectorAll('track')).map(t=>({kind:t.kind, srclang:t.srclang, label:t.label}));
144
+ // look for external transcript links nearby
145
+ let transcript = null;
146
+ const next = v.nextElementSibling;
147
+ if (next && /transcript|caption|captions/i.test(next.innerText||'')) transcript = next.innerText.trim().slice(0,300);
148
+ out.push({tag:v.tagName.toLowerCase(), hasTracks: tracks.length>0, tracks, transcript});
149
+ }
150
+ return {count: videos.length, videos: out};
151
+ }"""
152
+
153
+ CONTRAST_JS = """() => {
154
+ function luminance(r,g,b){
155
+ const a = [r,g,b].map(v=>{ v=v/255; return v<=0.03928? v/12.92: Math.pow((v+0.055)/1.055,2.4);});
156
+ return 0.2126*a[0]+0.7152*a[1]+0.0722*a[2];
157
+ }
158
+ function parseRGB(colorStr){
159
+ const m = colorStr.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/);
160
+ return m ? [parseInt(m[1]),parseInt(m[2]),parseInt(m[3])] : null;
161
+ }
162
+ const candidates = Array.from(document.querySelectorAll('body *')).filter(el => (el.innerText||'').trim().length>0).slice(0,400);
163
+ const issues = [];
164
+ for (const el of candidates){
165
+ const style = window.getComputedStyle(el);
166
+ const color = parseRGB(style.color);
167
+ let bg = null;
168
+ let cur = el;
169
+ while(cur && cur!==document){
170
+ const b = window.getComputedStyle(cur).backgroundColor;
171
+ if (b && b!=='rgba(0, 0, 0, 0)' && b!=='transparent') { bg = parseRGB(b); break; }
172
+ cur = cur.parentElement;
173
+ }
174
+ if (!bg) bg = [255,255,255];
175
+ if (!color) continue;
176
+ const L1 = luminance(color[0],color[1],color[2]);
177
+ const L2 = luminance(bg[0],bg[1],bg[2]);
178
+ const ratio = (Math.max(L1,L2)+0.05)/(Math.min(L1,L2)+0.05);
179
+ const fontSize = parseFloat(style.fontSize) || 12;
180
+ const fontWeight = style.fontWeight;
181
+ const large = fontSize >= 18 || (fontSize >= 14 && (fontWeight === '700' || parseInt(fontWeight||'0')>=700));
182
+ const pass = large ? ratio >= 3.0 : ratio >= 4.5;
183
+ if (!pass) issues.push({text: (el.innerText||'').slice(0,120), ratio: Number(ratio.toFixed(2)), fontSize, large});
184
+ }
185
+ return {checked: candidates.length, issues: issues.slice(0,200)};
186
+ }"""
187
+
188
+ SEMANTICS_JS = """() => {
189
+ // check for landmark elements and semantic usage
190
+ const landmarks = {};
191
+ ['header','main','nav','footer','aside','form'].forEach(k => landmarks[k] = document.querySelectorAll(k).length);
192
+ // detect skip link
193
+ const skip = Array.from(document.querySelectorAll('a[href]')).some(a => /skip|skip to content/i.test(a.innerText||''));
194
+ return {landmarks, hasSkipLink: skip};
195
+ }"""
196
+
197
+ # ---------- Tool definitions ----------
198
+ @mcp.tool()
199
+ async def run_axe_audit(url: str, timeout: int = 30000):
200
+ """
201
+ Run axe-core on the page and return the full axe result (violations, passes, incomplete, etc).
202
+ This is the primary automated scanner and is required for broad WCAG coverage.
203
+ """
204
+ playwright = browser = context = page = None
205
+ try:
206
+ playwright, browser, context, page = await _open_page(url, timeout=timeout)
207
+ await _ensure_axe(page)
208
+ axe_result = await _run_axe(page, tags=["wcag2a", "wcag2aa"])
209
+ return {"url": url, "axe": axe_result}
210
+ except Exception as e:
211
+ return {"url": url, "error": str(e)}
212
+ finally:
213
+ if context:
214
+ await context.close()
215
+ if browser:
216
+ await browser.close()
217
+ if playwright:
218
+ await playwright.stop()
219
+
220
+ # Individual custom checks (wrap the JS above)
221
+ async def _eval_js_on_page(url: str, js: str, timeout: int = 30000):
222
+ playwright = browser = context = page = None
223
+ try:
224
+ playwright, browser, context, page = await _open_page(url, timeout=timeout)
225
+ res = await page.evaluate(js)
226
+ return {"url": url, "result": res}
227
+ except Exception as e:
228
+ return {"url": url, "error": str(e)}
229
+ finally:
230
+ if context:
231
+ await context.close()
232
+ if browser:
233
+ await browser.close()
234
+ if playwright:
235
+ await playwright.stop()
236
+
237
+ @mcp.tool()
238
+ async def check_headings(url: str):
239
+ return await _eval_js_on_page(url, HEADINGS_JS)
240
+
241
+ @mcp.tool()
242
+ async def check_language(url: str):
243
+ return await _eval_js_on_page(url, LANG_JS)
244
+
245
+ @mcp.tool()
246
+ async def check_images(url: str):
247
+ return await _eval_js_on_page(url, IMAGES_JS)
248
+
249
+ @mcp.tool()
250
+ async def check_forms(url: str):
251
+ return await _eval_js_on_page(url, FORMS_JS)
252
+
253
+ @mcp.tool()
254
+ async def check_aria(url: str):
255
+ return await _eval_js_on_page(url, ARIA_JS)
256
+
257
+ @mcp.tool()
258
+ async def check_keyboard(url: str):
259
+ return await _eval_js_on_page(url, KEYBOARD_JS)
260
+
261
+ @mcp.tool()
262
+ async def check_videos(url: str):
263
+ return await _eval_js_on_page(url, VIDEO_JS)
264
+
265
+ @mcp.tool()
266
+ async def check_contrast(url: str):
267
+ return await _eval_js_on_page(url, CONTRAST_JS)
268
+
269
+ @mcp.tool()
270
+ async def check_semantics(url: str):
271
+ return await _eval_js_on_page(url, SEMANTICS_JS)
272
+
273
+ # Aggregator / Full audit
274
+ @mcp.tool()
275
+ async def full_audit(url: str, timeout: int = 60000):
276
+ """
277
+ Runs axe + all custom checks and returns a comprehensive report with suggested remediation hints.
278
+ Optimized to reuse a single browser session.
279
+ """
280
+ playwright = browser = context = page = None
281
+ results: Dict[str, Any] = {}
282
+
283
+ try:
284
+ # Launch browser once
285
+ playwright, browser, context, page = await _open_page(url, timeout=timeout)
286
+
287
+ # 1. Run Axe (needs injection)
288
+ try:
289
+ await _ensure_axe(page)
290
+ axe_result = await _run_axe(page, tags=["wcag2a", "wcag2aa"])
291
+ results["axe"] = {"url": url, "axe": axe_result}
292
+ except Exception as e:
293
+ results["axe"] = {"error": str(e)}
294
+
295
+ # 2. Run all custom JS checks sequentially on the SAME page
296
+ # This avoids spawning 10+ browser instances
297
+ check_definitions = {
298
+ "headings": HEADINGS_JS,
299
+ "language": LANG_JS,
300
+ "images": IMAGES_JS,
301
+ "forms": FORMS_JS,
302
+ "aria": ARIA_JS,
303
+ "keyboard": KEYBOARD_JS,
304
+ "videos": VIDEO_JS,
305
+ "contrast": CONTRAST_JS,
306
+ "semantics": SEMANTICS_JS
307
+ }
308
+
309
+ for key, js_code in check_definitions.items():
310
+ try:
311
+ res = await page.evaluate(js_code)
312
+ results[key] = {"url": url, "result": res}
313
+ except Exception as e:
314
+ results[key] = {"error": str(e)}
315
+
316
+ except Exception as e:
317
+ return {"url": url, "error": f"Fatal browser error: {str(e)}"}
318
+ finally:
319
+ if context:
320
+ await context.close()
321
+ if browser:
322
+ await browser.close()
323
+ if playwright:
324
+ await playwright.stop()
325
+
326
+ # Build summary: count axe violations of level 'serious' or 'critical' (if present), map to quick remediation hints
327
+ summary = {"url": url, "total_axe_violations": 0, "by_impact": {}, "quick_fixes": []}
328
+ try:
329
+ axe_out = results.get("axe", {}).get("axe") or results.get("axe", {}).get("result")
330
+ if axe_out and isinstance(axe_out, dict):
331
+ violations = axe_out.get("violations", [])
332
+ summary["total_axe_violations"] = len(violations)
333
+ by_impact = {}
334
+ for v in violations:
335
+ impact = v.get("impact", "unknown")
336
+ by_impact[impact] = by_impact.get(impact, 0) + 1
337
+ # add concise remediation hint
338
+ summary["quick_fixes"].append({
339
+ "id": v.get("id"),
340
+ "impact": impact,
341
+ "help": v.get("help"),
342
+ "nodes_sample": [n.get("html")[:200] for n in v.get("nodes", [])[:3]],
343
+ "why": v.get("description")
344
+ })
345
+ summary["by_impact"] = by_impact
346
+ except Exception:
347
+ pass
348
+
349
+ # augment summary with simple counts from custom checks
350
+ try:
351
+ summary["missing_alt_count"] = len(results.get("images", {}).get("result", {}).get("missingAlt", [])) if results.get("images", {}).get("result") else None
352
+ except Exception:
353
+ summary["missing_alt_count"] = None
354
+ try:
355
+ summary["forms_missing_labels"] = len(results.get("forms", {}).get("result", {}).get("missing", [])) if results.get("forms", {}).get("result") else None
356
+ except Exception:
357
+ summary["forms_missing_labels"] = None
358
+ try:
359
+ summary["contrast_issues"] = len(results.get("contrast", {}).get("result", {}).get("issues", [])) if results.get("contrast", {}).get("result") else None
360
+ except Exception:
361
+ summary["contrast_issues"] = None
362
+
363
+ # Return combined report
364
+ return {"url": url, "summary": summary, "results": results}
365
+
366
+ import sys
367
+
368
+ # Run MCP server via stdio transport
369
+ if __name__ == "__main__":
370
+ print("Starting Accessibility MCP server (stdio transport).", file=sys.stderr)
371
+ if not os.path.exists(AXE_JS_PATH):
372
+ print(f"WARNING: axe.min.js not found at {AXE_JS_PATH}. Download axe-core (axe.min.js) and place it beside this file or set AXE_JS_PATH.", file=sys.stderr)
373
+ mcp.run(transport="stdio")
src/accessibility_v1/notebook/accessibility.ipynb ADDED
@@ -0,0 +1,768 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 2,
6
+ "id": "2dd4443a",
7
+ "metadata": {},
8
+ "outputs": [
9
+ {
10
+ "data": {
11
+ "text/plain": [
12
+ "True"
13
+ ]
14
+ },
15
+ "execution_count": 2,
16
+ "metadata": {},
17
+ "output_type": "execute_result"
18
+ }
19
+ ],
20
+ "source": [
21
+ "from dotenv import load_dotenv\n",
22
+ "from agents import Agent, Runner, trace\n",
23
+ "from agents.mcp import MCPServerStdio\n",
24
+ "import os\n",
25
+ "from IPython.display import display, Markdown\n",
26
+ "\n",
27
+ "load_dotenv(override=True)"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": 11,
33
+ "id": "76b37f4a",
34
+ "metadata": {},
35
+ "outputs": [
36
+ {
37
+ "name": "stdout",
38
+ "output_type": "stream",
39
+ "text": [
40
+ "\n",
41
+ "✅ Available MCP Tools:\n",
42
+ "- run_axe_audit: \n",
43
+ " Run axe-core on the page and return the full axe result (violations, passes, incomplete, etc).\n",
44
+ " This is the primary automated scanner and is required for broad WCAG coverage.\n",
45
+ " \n",
46
+ "- check_headings: \n",
47
+ "- check_language: \n",
48
+ "- check_images: \n",
49
+ "- check_forms: \n",
50
+ "- check_aria: \n",
51
+ "- check_keyboard: \n",
52
+ "- check_videos: \n",
53
+ "- check_contrast: \n",
54
+ "- check_semantics: \n",
55
+ "- full_audit: \n",
56
+ " Runs axe + all custom checks and returns a comprehensive report with suggested remediation hints.\n",
57
+ " \n"
58
+ ]
59
+ },
60
+ {
61
+ "data": {
62
+ "text/plain": [
63
+ "[Tool(name='run_axe_audit', description='\\n Run axe-core on the page and return the full axe result (violations, passes, incomplete, etc).\\n This is the primary automated scanner and is required for broad WCAG coverage.\\n ', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}, 'timeout': {'default': 30000, 'title': 'Timeout', 'type': 'integer'}}, 'required': ['url'], 'title': 'run_axe_auditArguments', 'type': 'object'}, annotations=None),\n",
64
+ " Tool(name='check_headings', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_headingsArguments', 'type': 'object'}, annotations=None),\n",
65
+ " Tool(name='check_language', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_languageArguments', 'type': 'object'}, annotations=None),\n",
66
+ " Tool(name='check_images', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_imagesArguments', 'type': 'object'}, annotations=None),\n",
67
+ " Tool(name='check_forms', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_formsArguments', 'type': 'object'}, annotations=None),\n",
68
+ " Tool(name='check_aria', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_ariaArguments', 'type': 'object'}, annotations=None),\n",
69
+ " Tool(name='check_keyboard', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_keyboardArguments', 'type': 'object'}, annotations=None),\n",
70
+ " Tool(name='check_videos', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_videosArguments', 'type': 'object'}, annotations=None),\n",
71
+ " Tool(name='check_contrast', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_contrastArguments', 'type': 'object'}, annotations=None),\n",
72
+ " Tool(name='check_semantics', description='', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}}, 'required': ['url'], 'title': 'check_semanticsArguments', 'type': 'object'}, annotations=None),\n",
73
+ " Tool(name='full_audit', description='\\n Runs axe + all custom checks and returns a comprehensive report with suggested remediation hints.\\n ', inputSchema={'properties': {'url': {'title': 'Url', 'type': 'string'}, 'timeout': {'default': 60000, 'title': 'Timeout', 'type': 'integer'}}, 'required': ['url'], 'title': 'full_auditArguments', 'type': 'object'}, annotations=None)]"
74
+ ]
75
+ },
76
+ "execution_count": 11,
77
+ "metadata": {},
78
+ "output_type": "execute_result"
79
+ }
80
+ ],
81
+ "source": [
82
+ "import os\n",
83
+ "import nest_asyncio\n",
84
+ "from agents.mcp import MCPServerStdio\n",
85
+ "\n",
86
+ "nest_asyncio.apply()\n",
87
+ "\n",
88
+ "async def list_tools():\n",
89
+ " script_path = os.path.abspath(\"../mcp/server.py\")\n",
90
+ " params = {\"command\": \"uv\", \"args\": [\"run\", script_path]}\n",
91
+ "\n",
92
+ " async with MCPServerStdio(params=params, client_session_timeout_seconds=60) as server:\n",
93
+ " tools = await server.list_tools()\n",
94
+ "\n",
95
+ " print(\"\\n✅ Available MCP Tools:\")\n",
96
+ " for t in tools:\n",
97
+ " # Tool object → use attributes instead of dict access\n",
98
+ " print(f\"- {t.name}: {t.description}\")\n",
99
+ "\n",
100
+ " return tools\n",
101
+ "\n",
102
+ "await list_tools()\n"
103
+ ]
104
+ },
105
+ {
106
+ "cell_type": "code",
107
+ "execution_count": 14,
108
+ "id": "0462c3cb",
109
+ "metadata": {},
110
+ "outputs": [
111
+ {
112
+ "name": "stdout",
113
+ "output_type": "stream",
114
+ "text": [
115
+ "🚀 Launching Accessibility MCP server at: /home/azureuser/ws/agenticai/projects/accessibility/mcp/server.py\n",
116
+ "✅ Connected to MCP server. Listing available tools...\n",
117
+ "🔧 11 tools loaded:\n",
118
+ " - run_axe_audit: \n",
119
+ " Run axe-core on the page and return the full axe result (violations, passes, incomplete, etc).\n",
120
+ " This is the primary automated scanner and is required for broad WCAG coverage.\n",
121
+ " \n",
122
+ " - check_headings: \n",
123
+ " - check_language: \n",
124
+ " - check_images: \n",
125
+ " - check_forms: \n",
126
+ " - check_aria: \n",
127
+ " - check_keyboard: \n",
128
+ " - check_videos: \n",
129
+ " - check_contrast: \n",
130
+ " - check_semantics: \n",
131
+ " - full_audit: \n",
132
+ " Runs axe + all custom checks and returns a comprehensive report with suggested remediation hints.\n",
133
+ " \n"
134
+ ]
135
+ },
136
+ {
137
+ "data": {
138
+ "text/markdown": [
139
+ "# Accessibility Audit Report for oauthapp.azurewebsites.net\n",
140
+ "\n",
141
+ "## Compliance Summary\n",
142
+ "- **WCAG 2.1 Level A and AA Violations:** 0 detected\n",
143
+ "- **Color Contrast Issues:** None found (all text passes AA contrast ratio criteria)\n",
144
+ "- **Missing Alt Attributes on Images:** None\n",
145
+ "- **Forms Without Labels:** None (no input forms detected)\n",
146
+ "- **ARIA Issues:** Some role=\"img\" SVG elements missing accessible names, and one duplicate id attribute.\n",
147
+ "- **Keyboard Accessibility:** No critical issues, 24 focusable elements with a logical tab order.\n",
148
+ "- **Headings:** Proper use of headings with a single H1 and logically structured H2s.\n",
149
+ "- **Language:** Valid lang attribute (`en`) specified on the HTML element.\n",
150
+ "- **Videos:** None present, so no caption requirements.\n",
151
+ "- **Landmarks:** Site makes use of landmarks: header, main, nav, footer, aside.\n",
152
+ "\n",
153
+ "## Detailed Findings\n",
154
+ "\n",
155
+ "### Positive Findings\n",
156
+ "- Document has meaningful and properly structured headings (1 H1, followed by H2s).\n",
157
+ "- All images have valid alt text, providing alternative descriptions.\n",
158
+ "- Color contrast ratios meet or exceed WCAG 2.1 AA standards (4.5:1 for normal text, 3:1 for large text).\n",
159
+ "- The document contains a valid `<title>` element for proper page identification.\n",
160
+ "- The document's `<html>` element includes a valid `lang=\"en\"` attribute.\n",
161
+ "- Navigation and structural landmarks (header, nav, main, footer, aside) are present for assistive technology.\n",
162
+ "- Keyboard focus order is logical and functional.\n",
163
+ "- No form inputs lacking labels (no input fields found).\n",
164
+ "- No use of deprecated or disallowed elements like `<blink>` or `<marquee>`.\n",
165
+ "- No meta-refresh or zoom disabling issues detected.\n",
166
+ "\n",
167
+ "### Accessibility Issues\n",
168
+ "\n",
169
+ "1. **SVG Elements with `role=\"img\"` Missing Accessible Names:**\n",
170
+ " - Multiple Font Awesome SVG icons used as images have the `role=\"img\"` attribute but lack accessible names (`aria-label` or `<title>`).\n",
171
+ " - This can be problematic for screen reader users because these images do not have descriptive alternative text.\n",
172
+ " - Example elements affected include icons like plus, book, code-branch, key, sign-in-alt, unlock-alt, users, bars, etc.\n",
173
+ "\n",
174
+ "2. **Duplicate ID Attribute:**\n",
175
+ " - The ID `docIcon` is present on multiple SVG elements, which violates the uniqueness requirement for IDs in HTML.\n",
176
+ " - Duplicate IDs can cause issues in assistive technologies and scripting.\n",
177
+ "\n",
178
+ "3. **Missing Skip Link:**\n",
179
+ " - No \"skip to content\" link was detected. While not required, it is a strong recommended practice to help keyboard and screen reader users bypass repetitive navigation.\n",
180
+ "\n",
181
+ "## Recommendations\n",
182
+ "\n",
183
+ "### Fix SVG Accessibility\n",
184
+ "\n",
185
+ "- Add accessible names to SVG icons with `role=\"img\"`.\n",
186
+ "- Methods:\n",
187
+ " - Add a `<title>` element inside the SVG describing the icon (e.g., `<title>Expand</title>`).\n",
188
+ " - Or add `aria-label=\"Description of icon\"` attribute directly on the SVG.\n",
189
+ "- Ensure that decorative SVGs use `aria-hidden=\"true\"` and no `role=\"img\"`, if they convey no meaningful content.\n",
190
+ "\n",
191
+ "### Fix Duplicate IDs\n",
192
+ "\n",
193
+ "- Rename one of the duplicated `docIcon` IDs to a unique ID to meet HTML specifications.\n",
194
+ "- Confirm all IDs on the page are unique.\n",
195
+ "\n",
196
+ "### Add a Skip Link\n",
197
+ "\n",
198
+ "- Add a visually hidden but keyboard-accessible \"Skip to main content\" link at the top of the page.\n",
199
+ "- Example:\n",
200
+ " ```html\n",
201
+ " <a href=\"#main-content\" class=\"skip-link\">Skip to main content</a>\n",
202
+ " ```\n",
203
+ "- Ensure the main content container has an `id=\"main-content\"`.\n",
204
+ "\n",
205
+ "---\n",
206
+ "\n",
207
+ "## Overall Compliance Rating: **High**\n",
208
+ "\n",
209
+ "The page demonstrates strong compliance with WCAG 2.1 Level A and AA criteria. Only minor ARIA and duplicate ID issues were found, which are relatively simple to fix. Addressing these will further improve accessibility and screen reader experience.\n",
210
+ "\n",
211
+ "---\n",
212
+ "\n",
213
+ "If you want, I can provide sample code snippets or detailed instructions for the fixes."
214
+ ],
215
+ "text/plain": [
216
+ "<IPython.core.display.Markdown object>"
217
+ ]
218
+ },
219
+ "metadata": {},
220
+ "output_type": "display_data"
221
+ }
222
+ ],
223
+ "source": [
224
+ "import os\n",
225
+ "import nest_asyncio\n",
226
+ "import asyncio\n",
227
+ "from agents import Agent, Runner, trace\n",
228
+ "from agents.mcp import MCPServerStdio\n",
229
+ "from IPython.display import Markdown, display\n",
230
+ "\n",
231
+ "# Allow nested async in Jupyter\n",
232
+ "nest_asyncio.apply()\n",
233
+ "\n",
234
+ "async def run_agent():\n",
235
+ " # ---------------------------------------------------------------------\n",
236
+ " # Setup\n",
237
+ " # ---------------------------------------------------------------------\n",
238
+ " script_path = os.path.abspath(\"../mcp/server.py\")\n",
239
+ " if not os.path.exists(script_path):\n",
240
+ " raise FileNotFoundError(f\"MCP server script not found at: {script_path}\")\n",
241
+ "\n",
242
+ " print(f\"🚀 Launching Accessibility MCP server at: {script_path}\")\n",
243
+ "\n",
244
+ " params = {\"command\": \"uv\", \"args\": [\"run\", script_path]}\n",
245
+ "\n",
246
+ " instructions = (\n",
247
+ " \"You are an AI assistant specialized in ADA and WCAG 2.2 compliance. \"\n",
248
+ " \"Your job is to evaluate webpages using the available MCP tools. \"\n",
249
+ " \"Provide a compliance rating, identify accessibility issues, \"\n",
250
+ " \"and suggest detailed fixes in Markdown format.\"\n",
251
+ " )\n",
252
+ "\n",
253
+ " request = \"Audit oauthapp.azurewebsites.net for ADA and WCAG compliance and summarize findings in Markdown.\"\n",
254
+ "\n",
255
+ " model = \"gpt-4.1-mini\"\n",
256
+ "\n",
257
+ " # ---------------------------------------------------------------------\n",
258
+ " # Run the agent with MCP server\n",
259
+ " # ---------------------------------------------------------------------\n",
260
+ " try:\n",
261
+ " async with MCPServerStdio(params=params, client_session_timeout_seconds=90) as accessibility_server:\n",
262
+ " print(\"✅ Connected to MCP server. Listing available tools...\")\n",
263
+ " tools = await accessibility_server.list_tools()\n",
264
+ " print(f\"🔧 {len(tools)} tools loaded:\")\n",
265
+ " for tool in tools:\n",
266
+ " print(f\" - {tool.name}: {tool.description}\")\n",
267
+ "\n",
268
+ " agent = Agent(\n",
269
+ " name=\"accessibility_agent\",\n",
270
+ " instructions=instructions,\n",
271
+ " model=model,\n",
272
+ " mcp_servers=[accessibility_server],\n",
273
+ " )\n",
274
+ "\n",
275
+ " with trace(\"accessibility_agent\"):\n",
276
+ " result = await Runner.run(agent, request)\n",
277
+ "\n",
278
+ " # -----------------------------------------------------------------\n",
279
+ " # Display or fallback if model output is empty\n",
280
+ " # -----------------------------------------------------------------\n",
281
+ " if not result or not result.final_output:\n",
282
+ " display(Markdown(\"⚠️ **No audit results returned.** Please check the MCP server logs.\"))\n",
283
+ " else:\n",
284
+ " display(Markdown(result.final_output))\n",
285
+ "\n",
286
+ " except asyncio.TimeoutError:\n",
287
+ " print(\"⏱️ MCP server timed out. Try increasing `client_session_timeout_seconds`.\")\n",
288
+ " except Exception as e:\n",
289
+ " print(f\"❌ Unexpected error: {e}\")\n",
290
+ "\n",
291
+ "# Run inside notebook\n",
292
+ "await run_agent()\n"
293
+ ]
294
+ },
295
+ {
296
+ "cell_type": "code",
297
+ "execution_count": 18,
298
+ "id": "33a8d58c",
299
+ "metadata": {},
300
+ "outputs": [
301
+ {
302
+ "name": "stdout",
303
+ "output_type": "stream",
304
+ "text": [
305
+ "🚀 Launching Accessibility MCP server at: /home/azureuser/ws/agenticai/projects/accessibility/mcp/server.py\n",
306
+ "✅ Connected to MCP server.\n",
307
+ "🔧 Pages to audit: 5\n",
308
+ "📄 Auditing: https://oauthapp.azurewebsites.net/\n"
309
+ ]
310
+ },
311
+ {
312
+ "data": {
313
+ "text/markdown": [
314
+ "### Audit for https://oauthapp.azurewebsites.net/"
315
+ ],
316
+ "text/plain": [
317
+ "<IPython.core.display.Markdown object>"
318
+ ]
319
+ },
320
+ "metadata": {},
321
+ "output_type": "display_data"
322
+ },
323
+ {
324
+ "data": {
325
+ "text/markdown": [
326
+ "# Accessibility Audit Report for https://oauthapp.azurewebsites.net/\n",
327
+ "\n",
328
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
329
+ "|-------|-------------------------------|-----------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|\n",
330
+ "| A | html-has-lang | Pass | The `<html>` element has a lang attribute set to \"en\". | No action needed. |\n",
331
+ "| A | valid-lang | Pass | The lang attribute has a valid value \"en\". | No action needed. |\n",
332
+ "| A | document-title | Pass | The document contains a non-empty `<title>` element. | No action needed. |\n",
333
+ "| A | image-alt | Pass | All images have alternate text provided. | No action needed. |\n",
334
+ "| A | label | Pass | No form inputs requiring labels found. | No action needed. |\n",
335
+ "| A | link-name | Pass | All links have discernible text. | No action needed. |\n",
336
+ "| A | button-name | Pass | All buttons have discernible text. | No action needed. |\n",
337
+ "| A | aria-hidden-body | Pass | No `aria-hidden=\"true\"` on the `<body>`. | No action needed. |\n",
338
+ "| A | aria-hidden-focus | Pass | ARIA hidden elements are not focusable and contain no focusable children. | No action needed. |\n",
339
+ "| AA | color-contrast | Pass | Contrast ratios meet or exceed WCAG 2 AA minimum threshold. | No action needed. |\n",
340
+ "| A | headings | Pass | The page has a single `<h1>` and properly structured `<h2>` headings, no order problems. | No action needed. |\n",
341
+ "| A | keyboard | Pass | All interactive elements are focusable and tabbable in a logical order. | No action needed. |\n",
342
+ "| A | list | Pass | Lists are correctly structured with only allowed immediate children. | No action needed. |\n",
343
+ "| A | listitem | Pass | All `<li>` elements are contained inside `<ul>` or `<ol>`. | No action needed. |\n",
344
+ "| A | meta-viewport | Pass | Meta viewport tag does not disable zooming/scaling. | No action needed. |\n",
345
+ "| A | nested-interactive | Pass | There are no nested interactive controls. | No action needed. |\n",
346
+ "| A | scrollable-region-focusable | Pass | Scrollable regions have keyboard access. | No action needed. |\n",
347
+ "| A | semantics | Pass | Page uses landmarks such as header, main, nav, footer, aside correctly. | Consider adding a \"Skip to content\" link for improved navigation by keyboard/screen reader users. |\n",
348
+ "\n",
349
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
350
+ "|-------|-------------------------------|-----------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|\n",
351
+ "| A | aria-role-img-alt | Fail | Multiple `<svg>` elements with role=\"img\" lack accessible names. | Provide accessible names using `aria-label` or `aria-labelledby` for all SVG images with `role=\"img\"`.|\n",
352
+ "\n",
353
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
354
+ "|-------|-------------------------------|-----------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|\n",
355
+ "| A | duplicate-id | Fail | The ID \"docIcon\" is used more than once in the DOM. | Ensure all ID attributes are unique in the document to avoid conflicts during navigation and assistive technology usage.|\n",
356
+ "\n",
357
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
358
+ "|-------|-------------------------------|-----------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|\n",
359
+ "| A | has-skip-link | Fail | The page does not contain any mechanism to skip repetitive content (such as a \"Skip to content\" link). | Add a \"Skip to content\" link as an immediate child of the body for keyboard users to bypass navigation.|\n",
360
+ "\n",
361
+ "---\n",
362
+ "\n",
363
+ "### Summary:\n",
364
+ "- The site meets many core WCAG 2.1 Level A and AA accessibility requirements including language declaration, image alt text, headings structure, contrast, keyboard accessibility, and ARIA usage in most areas.\n",
365
+ "- Major issues to address:\n",
366
+ " - Provide accessible names for decorative or functional SVG images flagged with role=\"img\".\n",
367
+ " - Ensure all ID attributes are unique, fixing duplicate ID \"docIcon\".\n",
368
+ " - Add a skip link to improve keyboard navigation for screen reader users.\n",
369
+ "\n",
370
+ "Addressing these will improve the site's accessibility and compliance with WCAG 2.1 A and AA standards."
371
+ ],
372
+ "text/plain": [
373
+ "<IPython.core.display.Markdown object>"
374
+ ]
375
+ },
376
+ "metadata": {},
377
+ "output_type": "display_data"
378
+ },
379
+ {
380
+ "name": "stdout",
381
+ "output_type": "stream",
382
+ "text": [
383
+ "📄 Auditing: https://oauthapp.azurewebsites.net/code\n"
384
+ ]
385
+ },
386
+ {
387
+ "data": {
388
+ "text/markdown": [
389
+ "### Audit for https://oauthapp.azurewebsites.net/code"
390
+ ],
391
+ "text/plain": [
392
+ "<IPython.core.display.Markdown object>"
393
+ ]
394
+ },
395
+ "metadata": {},
396
+ "output_type": "display_data"
397
+ },
398
+ {
399
+ "data": {
400
+ "text/markdown": [
401
+ "# ADA/WCAG 2.2 Compliance Audit Report for https://oauthapp.azurewebsites.net/code\n",
402
+ "\n",
403
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
404
+ "|-------|-------------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|\n",
405
+ "| A | Document has lang attribute | Pass | The `<html>` element has a valid `lang=\"en\"` attribute. | None |\n",
406
+ "| A | Document has a non-empty title | Pass | Document contains a non-empty `<title>` element. | None |\n",
407
+ "| A | Proper use of headings hierarchy | Fail | No `<h1>` heading present; page starts with `<h2>` and has subsequents `<h3>` headings. | Include one `<h1>` heading for the main page title to improve document structure and navigation. |\n",
408
+ "| A | Images have alternative text | Pass | All images have meaningful alt attributes. | None |\n",
409
+ "| A | ARIA role and attributes valid | Fail | Multiple `<svg>` elements with role=\"img\" are missing accessible names. Also, there is a duplicate `id=\"docIcon\"`. | Provide accessible names or aria-labels for decorative SVG elements or mark them as aria-hidden if purely decorative. Ensure all IDs are unique. |\n",
410
+ "| A | Lists are structured correctly | Fail | The ordered list (`<ol>`) contains direct children that are `<pre>` elements, which is invalid. | Replace invalid direct children with `<li>` and wrap any code blocks inside proper `<li>` elements. |\n",
411
+ "| AA | Contrast ratio between text and background | Pass | All checked elements meet the minimum WCAG 2.1 AA contrast ratio. | None |\n",
412
+ "| AA | Keyboard focus and tab order | Pass | Keyboard focusable elements exist and follow a proper tab order. | None |\n",
413
+ "| AA | Landmark regions and semantic tags | Pass | The page includes landmarks such as `<header>`, `<main>`, `<nav>`, `<footer>`, and `<aside>`. | None |\n",
414
+ "| AA | Mechanism to bypass blocks of content | Fail | No skip link or similar method to bypass repeated navigation elements found. | Add a \"Skip to main content\" link or equivalent to facilitate keyboard and screen reader users. |\n",
415
+ "\n",
416
+ "---\n",
417
+ "\n",
418
+ "## Summary and Recommendations\n",
419
+ "\n",
420
+ "- The page is mostly compliant with WCAG 2.2 Level A and AA in many areas including language, alternative text for images, color contrast, keyboard accessibility, and semantic landmarks.\n",
421
+ "- Improvements are needed for proper heading structure by adding a main heading `<h1>`.\n",
422
+ "- ARIA usage should be reviewed to ensure all elements with roles have accessible names, especially decorative SVG icons, and to fix duplicate IDs.\n",
423
+ "- List structures should be corrected by ensuring that only `<li>` elements are direct children of `<ul>` or `<ol>`. Any other elements such as `<pre>` should be nested inside `<li>`.\n",
424
+ "- The site should include a mechanism (e.g., skip navigation link) to allow users to bypass repeated blocks, enhancing keyboard and screen reader navigation.\n",
425
+ "\n",
426
+ "Addressing these issues will enhance accessibility and compliance with WCAG 2.2 guidelines."
427
+ ],
428
+ "text/plain": [
429
+ "<IPython.core.display.Markdown object>"
430
+ ]
431
+ },
432
+ "metadata": {},
433
+ "output_type": "display_data"
434
+ },
435
+ {
436
+ "name": "stdout",
437
+ "output_type": "stream",
438
+ "text": [
439
+ "📄 Auditing: https://oauthapp.azurewebsites.net/credential\n"
440
+ ]
441
+ },
442
+ {
443
+ "data": {
444
+ "text/markdown": [
445
+ "### Audit for https://oauthapp.azurewebsites.net/credential"
446
+ ],
447
+ "text/plain": [
448
+ "<IPython.core.display.Markdown object>"
449
+ ]
450
+ },
451
+ "metadata": {},
452
+ "output_type": "display_data"
453
+ },
454
+ {
455
+ "data": {
456
+ "text/markdown": [
457
+ "# ADA/WCAG 2.2 Compliance Audit Report for https://oauthapp.azurewebsites.net/credential\n",
458
+ "\n",
459
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
460
+ "|-------|--------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------|\n",
461
+ "| A | HTML document has a lang attribute | Pass | The `<html>` element includes a valid `lang=\"en\"` attribute. | No action needed. |\n",
462
+ "| A | Document has a non-empty title | Pass | The document contains a non-empty `<title>` element. | No action needed. |\n",
463
+ "| A | Lists are structured correctly | Fail | An ordered list (`<ol>`) contains invalid direct children (`<pre>` element) instead of only `<li>` elements. | Fix lists to contain only permitted elements directly under `<ul>` or `<ol>`, i.e., `<li>`. |\n",
464
+ "| A | All `<img>` elements have alt text | Pass | All images have valid non-empty alt attributes. | No action needed. |\n",
465
+ "| A | No form fields missing labels | Pass | No input fields in forms or visible labels missing. | No action needed. |\n",
466
+ "| A | ARIA roles and attributes valid | Fail | Multiple SVG icons with role=\"img\" are missing accessible names; duplicate ID \"docIcon\" found twice. | Add accessible names to SVG icons via `aria-label` or `title`, and ensure unique IDs. |\n",
467
+ "| A | Keyboard focus and tab order usable | Pass | Keyboard focusable elements exist, and tab order appears logical without positive tabindex issues. | No action needed. |\n",
468
+ "| AA | Color contrast ratio compliance | Pass | All text and UI components meet minimum contrast ratios (4.5:1 for normal text). | No action needed. |\n",
469
+ "| AA | Landmarks and page structure | Pass | Page uses multiple landmark elements including header, main, nav, footer, and aside. | Add a \"Skip to content\" link for improved accessibility and easier navigation for keyboard users.|\n",
470
+ "| AAA | No videos requiring captions | Pass | No videos present on the page to analyze captioning. | No action needed. |\n",
471
+ "| AA | Headings hierarchy | Pass | Headings levels are used properly; no H1 but appropriate use of H2 and H3 found. | Consider adding an H1 heading for better semantic structure. |\n",
472
+ "\n",
473
+ "## Summary:\n",
474
+ "- The page mostly meets WCAG 2.1 Level A and AA requirements.\n",
475
+ "- There is one serious violation related to list semantics: an `<ol>` contains a non-permitted direct child (`<pre>` element). This must be corrected.\n",
476
+ "- ARIA issues include multiple SVG images with role=\"img\" missing accessible names and duplicate ID usage, posing accessibility problems for screen reader users. These should be addressed by adding accessible names and ensuring IDs are unique.\n",
477
+ "- The page lacks a skip link to allow keyboard users to bypass repetitive navigation.\n",
478
+ "- No contrast issues were detected.\n",
479
+ "- Headings structure is fair but lacks an H1.\n",
480
+ "\n",
481
+ "---\n",
482
+ "\n",
483
+ "### Recommendations Summary:\n",
484
+ "- Correct list structure by ensuring `<ul>` and `<ol>` contain only `<li>`, `<script>`, or `<template>` as direct children.\n",
485
+ "- Provide accessible names (via `aria-label` or `<title>`) for all SVG icons that have `role=\"img\"`.\n",
486
+ "- Remove duplicate element IDs (e.g., \"docIcon\") so all IDs are unique.\n",
487
+ "- Add a visible \"Skip to content\" link at the top of the page.\n",
488
+ "- Consider introducing an H1 heading to improve document outline.\n",
489
+ "\n",
490
+ "This will improve the page's accessibility for users with disabilities and align it more closely with WCAG 2.2 guidelines."
491
+ ],
492
+ "text/plain": [
493
+ "<IPython.core.display.Markdown object>"
494
+ ]
495
+ },
496
+ "metadata": {},
497
+ "output_type": "display_data"
498
+ },
499
+ {
500
+ "name": "stdout",
501
+ "output_type": "stream",
502
+ "text": [
503
+ "📄 Auditing: https://oauthapp.azurewebsites.net/implicit\n"
504
+ ]
505
+ },
506
+ {
507
+ "data": {
508
+ "text/markdown": [
509
+ "### Audit for https://oauthapp.azurewebsites.net/implicit"
510
+ ],
511
+ "text/plain": [
512
+ "<IPython.core.display.Markdown object>"
513
+ ]
514
+ },
515
+ "metadata": {},
516
+ "output_type": "display_data"
517
+ },
518
+ {
519
+ "data": {
520
+ "text/markdown": [
521
+ "# ADA/WCAG 2.2 Compliance Audit Report for https://oauthapp.azurewebsites.net/implicit\n",
522
+ "\n",
523
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
524
+ "|-------|-----------------------------------|-----------|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|\n",
525
+ "| A | HTML lang attribute (WCAG 3.1.1) | Pass | The `<html>` element has a valid `lang=\"en\"` attribute. | None |\n",
526
+ "| A | Document Title (WCAG 2.4.2) | Pass | The document contains a non-empty `<title>` element. | None |\n",
527
+ "| A | Image alternative text (WCAG 1.1.1) | Pass | All images have appropriate alt attributes. | None |\n",
528
+ "| A | Keyboard navigation (WCAG 2.1.1) | Pass | Page elements are keyboard focusable with logical tab order. | None |\n",
529
+ "| A | List semantics (WCAG 1.3.1) | Fail | A `<ol>` list contains direct children `<pre>` which is invalid (only `<li>`, `<script>`, or `<template>` allowed). | Correct the list structure to ensure only `<li>`, `<script>`, or `<template>` tags are direct children of lists. |\n",
530
+ "| A | ARIA role img missing accessible name (WCAG 4.1.2) | Fail | Multiple `<svg>` elements with role `img` are missing accessible names. | Provide accessible names (using `aria-label` or `aria-labelledby`) for all `<svg>` elements with `img` role. |\n",
531
+ "| AA | Color contrast (WCAG 1.4.3) | Pass | All text and interactive element color contrasts meet minimum 4.5:1 or 3:1 ratio as appropriate. | None |\n",
532
+ "| AA | Semantic landmarks (WCAG 1.3.1) | Pass | Page includes landmarks like `header`, `main`, `nav`, `footer`, and `aside`. | Add a skip link mechanism for bypassing navigation to main content to improve accessibility. |\n",
533
+ "| AA | Headings structure (WCAG 2.4.6) | Pass | Headings have good structure; no H1 is present but no order problems detected. | Consider adding a main `<h1>` heading for page structure clarity. |\n",
534
+ "\n",
535
+ "## Summary\n",
536
+ "- The page mostly complies with core WCAG 2.1 A and AA criteria including language declaration, color contrast, keyboard navigation, and form labeling.\n",
537
+ "- There is a structural accessibility failure regarding list markup — lists contain disallowed direct children.\n",
538
+ "- There are multiple SVG graphics missing accessible names which could impact screen reader users.\n",
539
+ "- No issues found with color contrast or keyboard accessibility.\n",
540
+ "- Semantic landmarks are present but page lacks a skip link so users must tab through navigation repeatedly.\n",
541
+ "\n",
542
+ "## Recommendations\n",
543
+ "1. Fix list markup so that lists only contain directly `<li>`, `<script>`, or `<template>` elements.\n",
544
+ "2. Add accessible names to all SVG elements with `role=\"img\"` to ensure screen readers announce meaningful labels.\n",
545
+ "3. Add a skip navigation link to allow keyboard and screen reader users to bypass repetitive content.\n",
546
+ "4. Consider adding an `<h1>` for main page title to better support assistive technology users.\n",
547
+ "\n",
548
+ "This report is based on automated scanning combined with structural analysis as of system date. Manual testing is recommended for comprehensive compliance assurance."
549
+ ],
550
+ "text/plain": [
551
+ "<IPython.core.display.Markdown object>"
552
+ ]
553
+ },
554
+ "metadata": {},
555
+ "output_type": "display_data"
556
+ },
557
+ {
558
+ "name": "stdout",
559
+ "output_type": "stream",
560
+ "text": [
561
+ "📄 Auditing: https://oauthapp.azurewebsites.net/password\n"
562
+ ]
563
+ },
564
+ {
565
+ "data": {
566
+ "text/markdown": [
567
+ "### Audit for https://oauthapp.azurewebsites.net/password"
568
+ ],
569
+ "text/plain": [
570
+ "<IPython.core.display.Markdown object>"
571
+ ]
572
+ },
573
+ "metadata": {},
574
+ "output_type": "display_data"
575
+ },
576
+ {
577
+ "data": {
578
+ "text/markdown": [
579
+ "# ADA/WCAG Compliance Audit Report for https://oauthapp.azurewebsites.net/password\n",
580
+ "\n",
581
+ "| Level | Rule | Pass/Fail | Reason | Recommendation |\n",
582
+ "|-------|-----------------------------------|-----------|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|\n",
583
+ "| A | list | Fail | An ordered list (<ol>) contains a <pre> element as a direct child, which is not allowed. | Ensure <ul> and <ol> elements only contain <li>, <script>, or <template> elements as direct children.|\n",
584
+ "| A | role-img-alt | Fail | Multiple <svg> icons with role=\"img\" are missing accessible names (alt text equivalent). | Provide accessible names for all <svg> elements with role=\"img\", e.g., aria-label or <title> element.|\n",
585
+ "| A | duplicate-id | Fail | Duplicate ID \"docIcon\" found on multiple elements. | Ensure all id attributes are unique on the page. |\n",
586
+ "| A | html-has-lang | Pass | The <html> element has a valid lang attribute set to \"en\". | None needed. |\n",
587
+ "| A | image-alt | Pass | All <img> elements have alt attributes with valid values. | None needed. |\n",
588
+ "| A | forms-label | Pass | No forms or form fields present that are missing labels. | None needed. |\n",
589
+ "| A | button-name | Pass | Buttons have discernible text or accessible names. | None needed. |\n",
590
+ "| AA | color-contrast | Pass | Text and UI components meet the minimum contrast ratio required by WCAG 2.1 AA. | None needed. |\n",
591
+ "| A | keyboard | Pass | Keyboard navigation works; all interactive elements are focusable in correct order. | None needed. |\n",
592
+ "| A | headings | Pass | Proper heading structure is present with no order problems, but no <h1> found. | Recommend adding a single <h1> heading for improved semantic structure and accessibility. |\n",
593
+ "| A | semantics-landmarks | Pass | Page contains multiple landmark elements like header, main, nav, and footer. | Consider adding a \"skip to content\" link to facilitate bypassing repetitive navigation. |\n",
594
+ "| AA | meta-viewport | Pass | Meta viewport tag correctly configured without disabling zoom. | None needed. |\n",
595
+ "\n",
596
+ "## Summary\n",
597
+ "The page is mostly compliant with fundamental WCAG 2.1 A and AA requirements but has several accessibility issues that must be addressed:\n",
598
+ "\n",
599
+ "- The key violation is improper list structure where a <pre> element exists directly inside an <ol>. This breaks semantic list requirement.\n",
600
+ "- Numerous SVG icons lack accessible names which impacts screen reader users.\n",
601
+ "- Duplicate IDs on the page violate unique ID constraints, which can cause accessibility and scripting issues.\n",
602
+ "- The page misses a primary <h1> heading which is recommended for semantic clarity and accessibility.\n",
603
+ "- Adding a skip link is recommended to allow keyboard users to bypass navigation easily.\n",
604
+ "\n",
605
+ "Addressing these issues will enhance accessibility for users relying on assistive technologies and keyboard navigation.\n",
606
+ "\n",
607
+ "---\n",
608
+ "\n",
609
+ "For detailed guidance, refer to the WCAG 2.2 checklist and [Deque University Axe rules](https://dequeuniversity.com/rules/axe/4.8/)."
610
+ ],
611
+ "text/plain": [
612
+ "<IPython.core.display.Markdown object>"
613
+ ]
614
+ },
615
+ "metadata": {},
616
+ "output_type": "display_data"
617
+ },
618
+ {
619
+ "name": "stderr",
620
+ "output_type": "stream",
621
+ "text": [
622
+ "Error getting response: Connection error.. (request_id: None)\n"
623
+ ]
624
+ },
625
+ {
626
+ "name": "stdout",
627
+ "output_type": "stream",
628
+ "text": [
629
+ "❌ Unexpected error: Connection error.\n"
630
+ ]
631
+ }
632
+ ],
633
+ "source": [
634
+ "import os\n",
635
+ "import nest_asyncio\n",
636
+ "import asyncio\n",
637
+ "from datetime import datetime\n",
638
+ "from agents import Agent, Runner, trace\n",
639
+ "from agents.mcp import MCPServerStdio\n",
640
+ "from IPython.display import Markdown, display, HTML\n",
641
+ "\n",
642
+ "nest_asyncio.apply()\n",
643
+ "\n",
644
+ "async def run_site_accessibility_dashboard(base_url: str):\n",
645
+ " # ------------------- Setup MCP Server -------------------\n",
646
+ " script_path = os.path.abspath(\"../mcp/server.py\")\n",
647
+ " if not os.path.exists(script_path):\n",
648
+ " raise FileNotFoundError(f\"MCP server script not found at: {script_path}\")\n",
649
+ "\n",
650
+ " print(f\"🚀 Launching Accessibility MCP server at: {script_path}\")\n",
651
+ " params = {\"command\": \"uv\", \"args\": [\"run\", script_path]}\n",
652
+ " model = \"gpt-4.1-mini\"\n",
653
+ "\n",
654
+ " try:\n",
655
+ " async with MCPServerStdio(params=params, client_session_timeout_seconds=120) as accessibility_server:\n",
656
+ " print(\"✅ Connected to MCP server.\")\n",
657
+ "\n",
658
+ " # ------------------- Fetch sitemap -------------------\n",
659
+ " import aiohttp\n",
660
+ " from xml.etree import ElementTree as ET\n",
661
+ " urls_to_audit = [base_url]\n",
662
+ " sitemap_url = base_url.rstrip(\"/\") + \"/sitemap.xml\"\n",
663
+ " try:\n",
664
+ " async with aiohttp.ClientSession() as session:\n",
665
+ " async with session.get(sitemap_url) as resp:\n",
666
+ " if resp.status == 200:\n",
667
+ " xml_text = await resp.text()\n",
668
+ " root = ET.fromstring(xml_text)\n",
669
+ " urls_to_audit = [elem.text for elem in root.findall(\".//{*}loc\")]\n",
670
+ " except Exception as e:\n",
671
+ " print(f\"⚠️ Sitemap fetch error: {e}\")\n",
672
+ " \n",
673
+ " print(f\"🔧 Pages to audit: {len(urls_to_audit)}\")\n",
674
+ "\n",
675
+ " # ------------------- Run Accessibility Audit per Page -------------------\n",
676
+ " audit_results = {}\n",
677
+ " audit_instructions = (\n",
678
+ " \"You are an AI assistant specialized in ADA/WCAG 2.2 compliance. \"\n",
679
+ " \"Audit a webpage and produce a Markdown report including all rules (Pass/Fail) \"\n",
680
+ " \"with columns: Level (A, AA, AAA), Rule, Pass/Fail, Reason, Recommendation.\"\n",
681
+ " )\n",
682
+ "\n",
683
+ " for url in urls_to_audit:\n",
684
+ " print(f\"📄 Auditing: {url}\")\n",
685
+ " audit_agent = Agent(\n",
686
+ " name=\"accessibility_agent\",\n",
687
+ " instructions=audit_instructions,\n",
688
+ " model=model,\n",
689
+ " mcp_servers=[accessibility_server],\n",
690
+ " )\n",
691
+ " with trace(f\"audit_{url}\"):\n",
692
+ " result = await Runner.run(audit_agent, f\"Audit {url} for ADA/WCAG compliance.\")\n",
693
+ " markdown_output = result.final_output if result and result.final_output else f\"⚠️ No results for {url}\"\n",
694
+ " audit_results[url] = markdown_output\n",
695
+ " display(Markdown(f\"### Audit for {url}\"))\n",
696
+ " display(Markdown(markdown_output))\n",
697
+ "\n",
698
+ " # ------------------- HTML Dashboard -------------------\n",
699
+ " html_instructions = (\n",
700
+ " \"You are an AI assistant that converts multiple Markdown accessibility audit reports \"\n",
701
+ " \"into a single responsive HTML dashboard. \"\n",
702
+ " \"Requirements: \\n\"\n",
703
+ " \"- Hero banner with site name\\n\"\n",
704
+ " \"- Filters: All / Passed / Failed / Warnings\\n\"\n",
705
+ " \"- Clicking a filter updates the table dynamically\\n\"\n",
706
+ " \"- Table columns: Level, Rule, Pass/Fail, Reason, Recommendation\\n\"\n",
707
+ " \"- TailwindCSS styling\\n\"\n",
708
+ " \"- Include a summary row with total Pass/Fail counts per page\"\n",
709
+ " )\n",
710
+ "\n",
711
+ " html_agent = Agent(\n",
712
+ " name=\"html_dashboard_agent\",\n",
713
+ " instructions=html_instructions,\n",
714
+ " model=model\n",
715
+ " )\n",
716
+ "\n",
717
+ " html_request = \"Generate a site-wide accessibility dashboard from the following Markdown reports:\\n\\n\"\n",
718
+ " for url, md in audit_results.items():\n",
719
+ " html_request += f\"Page: {url}\\n{md}\\n\\n\"\n",
720
+ "\n",
721
+ " with trace(\"html_dashboard_agent\"):\n",
722
+ " html_result = await Runner.run(html_agent, html_request)\n",
723
+ "\n",
724
+ " html_output = html_result.final_output if html_result and html_result.final_output else \"<p>⚠️ HTML dashboard generation failed.</p>\"\n",
725
+ " display(HTML(html_output))\n",
726
+ "\n",
727
+ " # ------------------- Save HTML -------------------\n",
728
+ " output_dir = os.path.abspath(\"output\")\n",
729
+ " os.makedirs(output_dir, exist_ok=True)\n",
730
+ " timestamp = datetime.now().strftime(\"%Y%m%d_%H%M%S\")\n",
731
+ " output_file = os.path.join(output_dir, f\"accessibility_dashboard_{timestamp}.html\")\n",
732
+ " with open(output_file, \"w\", encoding=\"utf-8\") as f:\n",
733
+ " f.write(html_output)\n",
734
+ "\n",
735
+ " print(f\"✅ Site-wide HTML dashboard saved to: {output_file}\")\n",
736
+ "\n",
737
+ " except asyncio.TimeoutError:\n",
738
+ " print(\"⏱️ MCP server timed out. Increase `client_session_timeout_seconds` if needed.\")\n",
739
+ " except Exception as e:\n",
740
+ " print(f\"❌ Unexpected error: {e}\")\n",
741
+ "\n",
742
+ "# Run dashboard\n",
743
+ "await run_site_accessibility_dashboard(\"https://oauthapp.azurewebsites.net\")\n"
744
+ ]
745
+ }
746
+ ],
747
+ "metadata": {
748
+ "kernelspec": {
749
+ "display_name": "agents",
750
+ "language": "python",
751
+ "name": "python3"
752
+ },
753
+ "language_info": {
754
+ "codemirror_mode": {
755
+ "name": "ipython",
756
+ "version": 3
757
+ },
758
+ "file_extension": ".py",
759
+ "mimetype": "text/x-python",
760
+ "name": "python",
761
+ "nbconvert_exporter": "python",
762
+ "pygments_lexer": "ipython3",
763
+ "version": "3.12.11"
764
+ }
765
+ },
766
+ "nbformat": 4,
767
+ "nbformat_minor": 5
768
+ }
src/accessibility_v1/notebook/output/accessibility_audit_20251112_061159.html ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```html
2
+ <table style="width:100%; border-collapse: collapse; font-family: Arial, sans-serif;">
3
+ <thead>
4
+ <tr style="background:#333; color:#fff; text-align:left;">
5
+ <th style="padding:8px; border: 1px solid #ddd;">Level</th>
6
+ <th style="padding:8px; border: 1px solid #ddd;">Rule</th>
7
+ <th style="padding:8px; border: 1px solid #ddd;">Pass/Fail</th>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <!-- Pass rows -->
12
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
13
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
14
+ <span style="margin-right:16px;">A</span>
15
+ <span style="margin-right:16px;">document-title</span>
16
+ <span style="color:#155724; font-weight:bold;">Pass</span>
17
+ </summary>
18
+ <tr style="display:none;">
19
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
20
+ <strong>Reason:</strong> Page contains a non-empty <code>&lt;title&gt;</code> element<br>
21
+ <strong>Recommendation:</strong> None
22
+ </td>
23
+ </tr>
24
+ </details>
25
+
26
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
27
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
28
+ <span style="margin-right:16px;">A</span>
29
+ <span style="margin-right:16px;">html-has-lang</span>
30
+ <span style="color:#155724; font-weight:bold;">Pass</span>
31
+ </summary>
32
+ <tr style="display:none;">
33
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
34
+ <strong>Reason:</strong> <code>&lt;html&gt;</code> element has a valid <code>lang</code> attribute with value <code>en</code><br>
35
+ <strong>Recommendation:</strong> None
36
+ </td>
37
+ </tr>
38
+ </details>
39
+
40
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
41
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
42
+ <span style="margin-right:16px;">AA</span>
43
+ <span style="margin-right:16px;">color-contrast</span>
44
+ <span style="color:#155724; font-weight:bold;">Pass</span>
45
+ </summary>
46
+ <tr style="display:none;">
47
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
48
+ <strong>Reason:</strong> Text and background colors have sufficient contrast exceeding WCAG 2 AA minimum thresholds<br>
49
+ <strong>Recommendation:</strong> None
50
+ </td>
51
+ </tr>
52
+ </details>
53
+
54
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
55
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
56
+ <span style="margin-right:16px;">A</span>
57
+ <span style="margin-right:16px;">image-alt</span>
58
+ <span style="color:#155724; font-weight:bold;">Pass</span>
59
+ </summary>
60
+ <tr style="display:none;">
61
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
62
+ <strong>Reason:</strong> All 6 images have descriptive alternative text<br>
63
+ <strong>Recommendation:</strong> None
64
+ </td>
65
+ </tr>
66
+ </details>
67
+
68
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
69
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
70
+ <span style="margin-right:16px;">A</span>
71
+ <span style="margin-right:16px;">headings</span>
72
+ <span style="color:#155724; font-weight:bold;">Pass</span>
73
+ </summary>
74
+ <tr style="display:none;">
75
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
76
+ <strong>Reason:</strong> Page has one <code>&lt;h1&gt;</code> and properly structured <code>&lt;h2&gt;</code> headings with no order issues<br>
77
+ <strong>Recommendation:</strong> None
78
+ </td>
79
+ </tr>
80
+ </details>
81
+
82
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
83
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
84
+ <span style="margin-right:16px;">A</span>
85
+ <span style="margin-right:16px;">forms-missing-labels</span>
86
+ <span style="color:#155724; font-weight:bold;">Pass</span>
87
+ </summary>
88
+ <tr style="display:none;">
89
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
90
+ <strong>Reason:</strong> No form inputs were found or missing labels<br>
91
+ <strong>Recommendation:</strong> None
92
+ </td>
93
+ </tr>
94
+ </details>
95
+
96
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
97
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
98
+ <span style="margin-right:16px;">A</span>
99
+ <span style="margin-right:16px;">aria-command-name</span>
100
+ <span style="color:#155724; font-weight:bold;">Pass</span>
101
+ </summary>
102
+ <tr style="display:none;">
103
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
104
+ <strong>Reason:</strong> Elements with ARIA commands have accessible names<br>
105
+ <strong>Recommendation:</strong> None
106
+ </td>
107
+ </tr>
108
+ </details>
109
+
110
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
111
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
112
+ <span style="margin-right:16px;">A</span>
113
+ <span style="margin-right:16px;">button-name</span>
114
+ <span style="color:#155724; font-weight:bold;">Pass</span>
115
+ </summary>
116
+ <tr style="display:none;">
117
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
118
+ <strong>Reason:</strong> All buttons have discernible text<br>
119
+ <strong>Recommendation:</strong> None
120
+ </td>
121
+ </tr>
122
+ </details>
123
+
124
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
125
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
126
+ <span style="margin-right:16px;">A</span>
127
+ <span style="margin-right:16px;">link-name</span>
128
+ <span style="color:#155724; font-weight:bold;">Pass</span>
129
+ </summary>
130
+ <tr style="display:none;">
131
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
132
+ <strong>Reason:</strong> All links have discernible text<br>
133
+ <strong>Recommendation:</strong> None
134
+ </td>
135
+ </tr>
136
+ </details>
137
+
138
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
139
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
140
+ <span style="margin-right:16px;">A</span>
141
+ <span style="margin-right:16px;">bypass</span>
142
+ <span style="color:#155724; font-weight:bold;">Pass</span>
143
+ </summary>
144
+ <tr style="display:none;">
145
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
146
+ <strong>Reason:</strong> Page structure facilitates bypassing repeated content (headings and landmark regions present)<br>
147
+ <strong>Recommendation:</strong> Consider adding an explicit "Skip to main content" link for easier navigation
148
+ </td>
149
+ </tr>
150
+ </details>
151
+
152
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
153
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
154
+ <span style="margin-right:16px;">A</span>
155
+ <span style="margin-right:16px;">no-autoplay-audio</span>
156
+ <span style="color:#155724; font-weight:bold;">Pass</span>
157
+ </summary>
158
+ <tr style="display:none;">
159
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
160
+ <strong>Reason:</strong> No audio or video content autoplays without control<br>
161
+ <strong>Recommendation:</strong> None
162
+ </td>
163
+ </tr>
164
+ </details>
165
+
166
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
167
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
168
+ <span style="margin-right:16px;">A</span>
169
+ <span style="margin-right:16px;">meta-viewport</span>
170
+ <span style="color:#155724; font-weight:bold;">Pass</span>
171
+ </summary>
172
+ <tr style="display:none;">
173
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
174
+ <strong>Reason:</strong> Meta viewport tag does not disable zooming or text scaling<br>
175
+ <strong>Recommendation:</strong> None
176
+ </td>
177
+ </tr>
178
+ </details>
179
+
180
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
181
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
182
+ <span style="margin-right:16px;">AA</span>
183
+ <span style="margin-right:16px;">scrollable-region-focusable</span>
184
+ <span style="color:#155724; font-weight:bold;">Pass</span>
185
+ </summary>
186
+ <tr style="display:none;">
187
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
188
+ <strong>Reason:</strong> Scrollable regions are keyboard accessible<br>
189
+ <strong>Recommendation:</strong> None
190
+ </td>
191
+ </tr>
192
+ </details>
193
+
194
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;" open>
195
+ <summary style="padding:8px; cursor:pointer; background:#d4edda; border-radius:4px; font-weight:bold;">
196
+ <span style="margin-right:16px;">A</span>
197
+ <span style="margin-right:16px;">nested-interactive</span>
198
+ <span style="color:#155724; font-weight:bold;">Pass</span>
199
+ </summary>
200
+ <tr style="display:none;">
201
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#f9fff9;">
202
+ <strong>Reason:</strong> No nested interactive controls that interfere with screen reader focus<br>
203
+ <strong>Recommendation:</strong> None
204
+ </td>
205
+ </tr>
206
+ </details>
207
+
208
+ <!-- Fail rows -->
209
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;">
210
+ <summary style="padding:8px; cursor:pointer; background:#f8d7da; border-radius:4px; font-weight:bold;">
211
+ <span style="margin-right:16px;">A</span>
212
+ <span style="margin-right:16px;">role-img-alt</span>
213
+ <span style="color:#721c24; font-weight:bold;">Fail</span>
214
+ </summary>
215
+ <tr style="display:none;">
216
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#fff5f5;">
217
+ <strong>Reason:</strong> These SVG icons lack alternative text for screen readers<br>
218
+ <strong>Recommendation:</strong> Add descriptive <code>aria-label</code> or <code>&lt;title&gt;</code> within SVG for each icon to provide accessible names
219
+ </td>
220
+ </tr>
221
+ </details>
222
+
223
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;">
224
+ <summary style="padding:8px; cursor:pointer; background:#f8d7da; border-radius:4px; font-weight:bold;">
225
+ <span style="margin-right:16px;">A</span>
226
+ <span style="margin-right:16px;">duplicate-id</span>
227
+ <span style="color:#721c24; font-weight:bold;">Fail</span>
228
+ </summary>
229
+ <tr style="display:none;">
230
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#fff5f5;">
231
+ <strong>Reason:</strong> ID attributes must be unique on the page to avoid reference confusion<br>
232
+ <strong>Recommendation:</strong> Ensure all ID attributes are unique within the HTML document
233
+ </td>
234
+ </tr>
235
+ </details>
236
+
237
+ <details style="border:1px solid #ddd; margin-bottom:4px; border-radius:4px;">
238
+ <summary style="padding:8px; cursor:pointer; background:#f8d7da; border-radius:4px; font-weight:bold;">
239
+ <span style="margin-right:16px;">A</span>
240
+ <span style="margin-right:16px;">skip-link</span>
241
+ <span style="color:#721c24; font-weight:bold;">Fail</span>
242
+ </summary>
243
+ <tr style="display:none;">
244
+ <td colspan="3" style="padding:8px; border: 1px solid #ddd; background:#fff5f5;">
245
+ <strong>Reason:</strong> Absence of skip links impacts keyboard users and screen reader users' ability to quickly access main content<br>
246
+ <strong>Recommendation:</strong> Add a visible or programmatically focusable skip link at the top of the page
247
+ </td>
248
+ </tr>
249
+ </details>
250
+ </tbody>
251
+ </table>
252
+ ```
src/accessibility_v1/output/accessibility_dashboard_20251112_065648.html ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; } /* green */
10
+ .fail { color: #dc2626; } /* red */
11
+ .warning { color: #f59e0b; } /* amber */
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <script>
47
+ const tableRows = [];
48
+ const pageTables = {};
49
+ const buttons = document.querySelectorAll(".filter-btn");
50
+
51
+ // ---------------- Table Row / Per-Page Table ----------------
52
+ function addRow(page, level, rule, status, reason, recommendation) {
53
+ if(!pageTables[page]) {
54
+ const container = document.getElementById("audit-tables");
55
+ const header = document.createElement("div");
56
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
57
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
58
+ const tableDiv = document.createElement("div");
59
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
60
+ const table = document.createElement("table");
61
+ table.className = "min-w-full bg-white shadow rounded";
62
+ table.innerHTML = `
63
+ <thead class="bg-gray-100">
64
+ <tr>
65
+ <th class="px-4 py-2">Level</th>
66
+ <th class="px-4 py-2">Rule</th>
67
+ <th class="px-4 py-2">Pass/Fail</th>
68
+ <th class="px-4 py-2">Reason</th>
69
+ <th class="px-4 py-2">Recommendation</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody></tbody>
73
+ `;
74
+ tableDiv.appendChild(table);
75
+ container.appendChild(header);
76
+ container.appendChild(tableDiv);
77
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
78
+ pageTables[page] = table.querySelector("tbody");
79
+ }
80
+
81
+ const tbody = pageTables[page];
82
+ const tr = document.createElement("tr");
83
+ tr.className = "border-b";
84
+ tr.dataset.status = status.toLowerCase();
85
+ tr.dataset.page = page;
86
+ tr.innerHTML = `
87
+ <td class="px-4 py-2">${level}</td>
88
+ <td class="px-4 py-2">${rule}</td>
89
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
90
+ <td class="px-4 py-2">${reason}</td>
91
+ <td class="px-4 py-2">${recommendation}</td>
92
+ `;
93
+ tableRows.push(tr);
94
+ tbody.appendChild(tr);
95
+ }
96
+
97
+ // ---------------- Page Summary Cards ----------------
98
+ const pageSummaries = {};
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ pageSummaries[page] = {passed, failed, warning};
101
+ const container = document.getElementById("summary-cards");
102
+ const total = passed + failed + warning;
103
+ const passRatio = total>0?passed/total:0;
104
+ let colorClass="bg-green-400";
105
+ if(passRatio<0.3) colorClass="bg-red-500";
106
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
107
+ const passPercent = total>0?(passed/total*100):0;
108
+ const failPercent = total>0?(failed/total*100):0;
109
+ const warnPercent = total>0?(warning/total*100):0;
110
+
111
+ const card = document.createElement("div");
112
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
113
+ card.innerHTML=`
114
+ <div class="flex justify-between items-center mb-2">
115
+ <h3 class="font-bold text-lg">${page}</h3>
116
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
117
+ </div>
118
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
119
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
120
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
121
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
122
+ </div>
123
+ <div class="text-sm">
124
+ <p class="text-green-600">Passed: ${passed}</p>
125
+ <p class="text-red-600">Failed: ${failed}</p>
126
+ <p class="text-yellow-500">Warnings: ${warning}</p>
127
+ </div>
128
+ `;
129
+ card.addEventListener("click", ()=>{
130
+ const row = tableRows.find(r=>r.dataset.page===page);
131
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
132
+ });
133
+ container.appendChild(card);
134
+ }
135
+
136
+ // ---------------- Filters ----------------
137
+ function updateTable(filter){
138
+ Object.values(pageTables).forEach(tbody=>{
139
+ Array.from(tbody.children).forEach(row=>{
140
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
141
+ else row.style.display="none";
142
+ });
143
+ });
144
+ }
145
+ buttons.forEach(btn=>{
146
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
147
+ });
148
+
149
+ // ---------------- Scroll to Top ----------------
150
+ const scrollBtn=document.getElementById("scrollTopBtn");
151
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
152
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
153
+ scrollBtn.style.display="none";
154
+
155
+ // ---------------- Hero Banner Totals ----------------
156
+ function updateTotals(passed, failed, warning){
157
+ document.getElementById("total-pass").innerText=passed;
158
+ document.getElementById("total-fail").innerText=failed;
159
+ document.getElementById("total-warning").innerText=warning;
160
+ const total=passed+failed+warning;
161
+ const ratio=total>0?passed/total:0;
162
+ const summaryEl=document.getElementById("summary");
163
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
164
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
165
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
166
+ else summaryEl.classList.add("text-green-500");
167
+ }
168
+ </script>
169
+
170
+ </body>
171
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251112_070331.html ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; } /* green */
10
+ .fail { color: #dc2626; } /* red */
11
+ .warning { color: #f59e0b; } /* amber */
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <script>
47
+ const tableRows = [];
48
+ const pageTables = {};
49
+ const buttons = document.querySelectorAll(".filter-btn");
50
+
51
+ // ---------------- Table Row / Per-Page Table ----------------
52
+ function addRow(page, level, rule, status, reason, recommendation) {
53
+ if(!pageTables[page]) {
54
+ const container = document.getElementById("audit-tables");
55
+ const header = document.createElement("div");
56
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
57
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
58
+ const tableDiv = document.createElement("div");
59
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
60
+ const table = document.createElement("table");
61
+ table.className = "min-w-full bg-white shadow rounded";
62
+ table.innerHTML = `
63
+ <thead class="bg-gray-100">
64
+ <tr>
65
+ <th class="px-4 py-2">Level</th>
66
+ <th class="px-4 py-2">Rule</th>
67
+ <th class="px-4 py-2">Pass/Fail</th>
68
+ <th class="px-4 py-2">Reason</th>
69
+ <th class="px-4 py-2">Recommendation</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody></tbody>
73
+ `;
74
+ tableDiv.appendChild(table);
75
+ container.appendChild(header);
76
+ container.appendChild(tableDiv);
77
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
78
+ pageTables[page] = table.querySelector("tbody");
79
+ }
80
+
81
+ const tbody = pageTables[page];
82
+ const tr = document.createElement("tr");
83
+ tr.className = "border-b";
84
+ tr.dataset.status = status.toLowerCase();
85
+ tr.dataset.page = page;
86
+ tr.innerHTML = `
87
+ <td class="px-4 py-2">${level}</td>
88
+ <td class="px-4 py-2">${rule}</td>
89
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
90
+ <td class="px-4 py-2">${reason}</td>
91
+ <td class="px-4 py-2">${recommendation}</td>
92
+ `;
93
+ tableRows.push(tr);
94
+ tbody.appendChild(tr);
95
+ }
96
+
97
+ // ---------------- Page Summary Cards ----------------
98
+ const pageSummaries = {};
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ pageSummaries[page] = {passed, failed, warning};
101
+ const container = document.getElementById("summary-cards");
102
+ const total = passed + failed + warning;
103
+ const passRatio = total>0?passed/total:0;
104
+ let colorClass="bg-green-400";
105
+ if(passRatio<0.3) colorClass="bg-red-500";
106
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
107
+ const passPercent = total>0?(passed/total*100):0;
108
+ const failPercent = total>0?(failed/total*100):0;
109
+ const warnPercent = total>0?(warning/total*100):0;
110
+
111
+ const card = document.createElement("div");
112
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
113
+ card.innerHTML=`
114
+ <div class="flex justify-between items-center mb-2">
115
+ <h3 class="font-bold text-lg">${page}</h3>
116
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
117
+ </div>
118
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
119
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
120
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
121
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
122
+ </div>
123
+ <div class="text-sm">
124
+ <p class="text-green-600">Passed: ${passed}</p>
125
+ <p class="text-red-600">Failed: ${failed}</p>
126
+ <p class="text-yellow-500">Warnings: ${warning}</p>
127
+ </div>
128
+ `;
129
+ card.addEventListener("click", ()=>{
130
+ const row = tableRows.find(r=>r.dataset.page===page);
131
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
132
+ });
133
+ container.appendChild(card);
134
+ }
135
+
136
+ // ---------------- Filters ----------------
137
+ function updateTable(filter){
138
+ Object.values(pageTables).forEach(tbody=>{
139
+ Array.from(tbody.children).forEach(row=>{
140
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
141
+ else row.style.display="none";
142
+ });
143
+ });
144
+ }
145
+ buttons.forEach(btn=>{
146
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
147
+ });
148
+
149
+ // ---------------- Scroll to Top ----------------
150
+ const scrollBtn=document.getElementById("scrollTopBtn");
151
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
152
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
153
+ scrollBtn.style.display="none";
154
+
155
+ // ---------------- Hero Banner Totals ----------------
156
+ function updateTotals(passed, failed, warning){
157
+ document.getElementById("total-pass").innerText=passed;
158
+ document.getElementById("total-fail").innerText=failed;
159
+ document.getElementById("total-warning").innerText=warning;
160
+ const total=passed+failed+warning;
161
+ const ratio=total>0?passed/total:0;
162
+ const summaryEl=document.getElementById("summary");
163
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
164
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
165
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
166
+ else summaryEl.classList.add("text-green-500");
167
+ }
168
+ </script>
169
+
170
+ </body>
171
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251112_070840.html ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; } /* green */
10
+ .fail { color: #dc2626; } /* red */
11
+ .warning { color: #f59e0b; } /* amber */
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <script>
47
+ const tableRows = [];
48
+ const pageTables = {};
49
+ const buttons = document.querySelectorAll(".filter-btn");
50
+
51
+ // ---------------- Table Row / Per-Page Table ----------------
52
+ function addRow(page, level, rule, status, reason, recommendation) {
53
+ if(!pageTables[page]) {
54
+ const container = document.getElementById("audit-tables");
55
+ const header = document.createElement("div");
56
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
57
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
58
+ const tableDiv = document.createElement("div");
59
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
60
+ const table = document.createElement("table");
61
+ table.className = "min-w-full bg-white shadow rounded";
62
+ table.innerHTML = `
63
+ <thead class="bg-gray-100">
64
+ <tr>
65
+ <th class="px-4 py-2">Level</th>
66
+ <th class="px-4 py-2">Rule</th>
67
+ <th class="px-4 py-2">Pass/Fail</th>
68
+ <th class="px-4 py-2">Reason</th>
69
+ <th class="px-4 py-2">Recommendation</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody></tbody>
73
+ `;
74
+ tableDiv.appendChild(table);
75
+ container.appendChild(header);
76
+ container.appendChild(tableDiv);
77
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
78
+ pageTables[page] = table.querySelector("tbody");
79
+ }
80
+
81
+ const tbody = pageTables[page];
82
+ const tr = document.createElement("tr");
83
+ tr.className = "border-b";
84
+ tr.dataset.status = status.toLowerCase();
85
+ tr.dataset.page = page;
86
+ tr.innerHTML = `
87
+ <td class="px-4 py-2">${level}</td>
88
+ <td class="px-4 py-2">${rule}</td>
89
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
90
+ <td class="px-4 py-2">${reason}</td>
91
+ <td class="px-4 py-2">${recommendation}</td>
92
+ `;
93
+ tableRows.push(tr);
94
+ tbody.appendChild(tr);
95
+ }
96
+
97
+ // ---------------- Page Summary Cards ----------------
98
+ const pageSummaries = {};
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ pageSummaries[page] = {passed, failed, warning};
101
+ const container = document.getElementById("summary-cards");
102
+ const total = passed + failed + warning;
103
+ const passRatio = total>0?passed/total:0;
104
+ let colorClass="bg-green-400";
105
+ if(passRatio<0.3) colorClass="bg-red-500";
106
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
107
+ const passPercent = total>0?(passed/total*100):0;
108
+ const failPercent = total>0?(failed/total*100):0;
109
+ const warnPercent = total>0?(warning/total*100):0;
110
+
111
+ const card = document.createElement("div");
112
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
113
+ card.innerHTML=`
114
+ <div class="flex justify-between items-center mb-2">
115
+ <h3 class="font-bold text-lg">${page}</h3>
116
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
117
+ </div>
118
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
119
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
120
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
121
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
122
+ </div>
123
+ <div class="text-sm">
124
+ <p class="text-green-600">Passed: ${passed}</p>
125
+ <p class="text-red-600">Failed: ${failed}</p>
126
+ <p class="text-yellow-500">Warnings: ${warning}</p>
127
+ </div>
128
+ `;
129
+ card.addEventListener("click", ()=>{
130
+ const row = tableRows.find(r=>r.dataset.page===page);
131
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
132
+ });
133
+ container.appendChild(card);
134
+ }
135
+
136
+ // ---------------- Filters ----------------
137
+ function updateTable(filter){
138
+ Object.values(pageTables).forEach(tbody=>{
139
+ Array.from(tbody.children).forEach(row=>{
140
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
141
+ else row.style.display="none";
142
+ });
143
+ });
144
+ }
145
+ buttons.forEach(btn=>{
146
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
147
+ });
148
+
149
+ // ---------------- Scroll to Top ----------------
150
+ const scrollBtn=document.getElementById("scrollTopBtn");
151
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
152
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
153
+ scrollBtn.style.display="none";
154
+
155
+ // ---------------- Hero Banner Totals ----------------
156
+ function updateTotals(passed, failed, warning){
157
+ document.getElementById("total-pass").innerText=passed;
158
+ document.getElementById("total-fail").innerText=failed;
159
+ document.getElementById("total-warning").innerText=warning;
160
+ const total=passed+failed+warning;
161
+ const ratio=total>0?passed/total:0;
162
+ const summaryEl=document.getElementById("summary");
163
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
164
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
165
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
166
+ else summaryEl.classList.add("text-green-500");
167
+ }
168
+ </script>
169
+
170
+ </body>
171
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251112_071804.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; }
10
+ .fail { color: #dc2626; }
11
+ .warning { color: #f59e0b; }
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <!-- Inject audit data as JSON -->
47
+ <script>
48
+ const auditData = [{"page": "https://oauthapp.azurewebsites.net/", "rows": [{"level": "", "rule": "Level", "status": "Rule", "reason": "Pass/Fail", "recommendation": "Reason"}, {"level": "", "rule": ":----", "status": ":-----------------------", "reason": ":--------", "recommendation": ":-------------------------------------------------------------------------------------------------------------------------"}, {"level": "", "rule": "A", "status": "Document Title", "reason": "Pass", "recommendation": "The document has a non-empty `<title>` element."}, {"level": "", "rule": "A", "status": "HTML has lang", "reason": "Pass", "recommendation": "The `<html>` element has a lang attribute."}, {"level": "", "rule": "A", "status": "HTML lang valid", "reason": "Pass", "recommendation": "The lang attribute of the `<html>` element has a valid value."}, {"level": "", "rule": "A", "status": "Image alt", "reason": "Pass", "recommendation": "All `<img>` elements have alternate text or a role of none or presentation."}, {"level": "", "rule": "A", "status": "Link name", "reason": "Pass", "recommendation": "All links have discernible text."}, {"level": "", "rule": "A", "status": "List", "reason": "Pass", "recommendation": "Lists are structured correctly."}, {"level": "", "rule": "A", "status": "Listitem", "reason": "Pass", "recommendation": "`<li>` elements are used semantically."}, {"level": "", "rule": "AA", "status": "Meta viewport", "reason": "Pass", "recommendation": "`<meta name=\"viewport\">` does not disable text scaling and zooming."}, {"level": "", "rule": "A", "status": "Bypass", "reason": "Pass", "recommendation": "Each page has at least one mechanism for a user to bypass navigation and jump straight to the content."}, {"level": "", "rule": "AA", "status": "Color contrast", "reason": "Pass", "recommendation": "The contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds."}, {"level": "", "rule": "A", "status": "ARIA hidden body", "reason": "Pass", "recommendation": "`aria-hidden=\"true\"` is not present on the document body."}, {"level": "", "rule": "A", "status": "ARIA hidden focus", "reason": "Pass", "recommendation": "ARIA-hidden elements are not focusable and do not contain focusable elements"}, {"level": "", "rule": "A", "status": "Button name", "reason": "Pass", "recommendation": "Ensures buttons have discernible text"}, {"level": "", "rule": "A", "status": "Nested interactive", "reason": "Pass", "recommendation": "Ensures interactive controls are not nested"}, {"level": "", "rule": "A", "status": "Scrollable region focusable", "reason": "Pass", "recommendation": "Ensures elements that have scrollable content are accessible by keyboard"}, {"level": "", "rule": "N/A", "status": "role-img-missing-name", "reason": "Fail", "recommendation": "The `<img>` elements with a role of \"img\" are missing accessible names."}, {"level": "", "rule": "N/A", "status": "duplicate-id", "reason": "Fail", "recommendation": "There are two elements with the id \"docIcon\""}], "summary": {"pass": 1, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/code", "rows": [{"level": "", "rule": "Level", "status": "Rule", "reason": "Pass/Fail", "recommendation": "Reason"}, {"level": "", "rule": ":----", "status": ":-----------------------------------------------------------", "reason": ":--------", "recommendation": ":------------------------------------------------------------------"}, {"level": "", "rule": "A", "status": "`<ul>` and `<ol>` must only directly contain `<li>`, `<script>` or `<template>` elements", "reason": "Fail", "recommendation": "List element has direct children that are not allowed: `<pre>`"}, {"level": "", "rule": "A", "status": "ARIA commands must have an accessible name", "reason": "Fail", "recommendation": "role img missing name"}, {"level": "", "rule": "N/A", "status": "duplicate-id", "reason": "Fail", "recommendation": "Duplicate ID \"docIcon\""}, {"level": "", "rule": "Area", "status": "Status", "reason": "Issues", "recommendation": ""}, {"level": "", "rule": ":------------", "status": ":-----", "reason": ":-----", "recommendation": ""}, {"level": "", "rule": "Headings", "status": "Pass", "reason": "No H1, but headings are present. Consider adding an H1 for better document structure.", "recommendation": ""}, {"level": "", "rule": "Language", "status": "Pass", "reason": "Language attribute is present.", "recommendation": ""}, {"level": "", "rule": "Images", "status": "Pass", "reason": "No missing alt text.", "recommendation": ""}, {"level": "", "rule": "Forms", "status": "Pass", "reason": "No missing labels.", "recommendation": ""}, {"level": "", "rule": "ARIA", "status": "Fail", "reason": "Several role-img-missing-name issues and duplicate IDs. See above table for fixes.", "recommendation": ""}, {"level": "", "rule": "Keyboard", "status": "Pass", "reason": "Focusable elements are in logical tab order.", "recommendation": ""}, {"level": "", "rule": "Videos", "status": "Pass", "reason": "No videos found.", "recommendation": ""}, {"level": "", "rule": "Contrast", "status": "Pass", "reason": "No contrast issues found.", "recommendation": ""}, {"level": "", "rule": "Semantics", "status": "Pass", "reason": "Landmarks are properly used.", "recommendation": ""}], "summary": {"pass": 8, "fail": 1, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/credential", "rows": [{"level": "", "rule": "Level", "status": "Rule", "reason": "Pass/Fail", "recommendation": "Reason"}, {"level": "", "rule": "---", "status": "---", "reason": "---", "recommendation": "---"}, {"level": "", "rule": "A", "status": "List structure", "reason": "Fail", "recommendation": "`<ol>` element has `pre` element as a direct child, which is not allowed."}, {"level": "", "rule": "A/AA", "status": "ARIA", "reason": "Fail", "recommendation": "Some `role=\"img\"` elements are missing accessible names."}, {"level": "", "rule": "A", "status": "Headings", "reason": "N/A", "recommendation": "The page does not have an H1"}, {"level": "", "rule": "A/AA", "status": "Duplicate IDs", "reason": "N/A", "recommendation": "The page has duplicate IDs."}, {"level": "", "rule": "A/AA", "status": "Image Alt Text", "reason": "Pass", "recommendation": "All images have alt text."}, {"level": "", "rule": "A/AA", "status": "Color Contrast", "reason": "Pass", "recommendation": "All elements have sufficient color contrast."}, {"level": "", "rule": "A", "status": "Link Name", "reason": "Pass", "recommendation": "All links have discernible text."}, {"level": "", "rule": "A", "status": "HTML Lang", "reason": "Pass", "recommendation": "The `<html>` element has a valid `lang` attribute."}, {"level": "", "rule": "A", "status": "Document Title", "reason": "Pass", "recommendation": "The document has a non-empty `<title>` element."}, {"level": "", "rule": "A", "status": "Bypass Blocks", "reason": "Pass", "recommendation": "The page has a mechanism to bypass repeated blocks."}, {"level": "", "rule": "AA", "status": "Meta Viewport", "reason": "Pass", "recommendation": "The `<meta name=\"viewport\">` tag does not disable text scaling and zooming."}, {"level": "", "rule": "A/AA", "status": "Keyboard Access", "reason": "N/A", "recommendation": "The page has a scrollable region that contains focusable elements."}, {"level": "", "rule": "A/AA", "status": "ARIA", "reason": "N/A", "recommendation": "Some elements with `aria-hidden=\"true\"` are focusable or contain focusable elements"}, {"level": "", "rule": "A/AA", "status": "Button Name", "reason": "Pass", "recommendation": "All buttons have discernible text"}], "summary": {"pass": 1, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/implicit", "rows": [{"level": "", "rule": "Level", "status": "Rule", "reason": "Pass/Fail", "recommendation": "Reason"}, {"level": "", "rule": "---", "status": "---", "reason": "---", "recommendation": "---"}, {"level": "", "rule": "A", "status": "List Structure", "reason": "Fail", "recommendation": "`<ol>` element contains a `<pre>` element directly, which is not allowed."}, {"level": "", "rule": "A", "status": "List Structure", "reason": "Pass", "recommendation": "`<ul>` and `<ol>` must only directly contain `<li>`, `<script>` or `<template>` elements"}, {"level": "", "rule": "A", "status": "List Items", "reason": "Pass", "recommendation": "`<li>` elements must be contained in a `<ul>` or `<ol>`"}, {"level": "", "rule": "AA", "status": "Color Contrast", "reason": "Pass", "recommendation": "The contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds"}, {"level": "", "rule": "A", "status": "Document Title", "reason": "Pass", "recommendation": "Each HTML document contains a non-empty `<title>` element"}, {"level": "", "rule": "A", "status": "HTML Lang Attribute", "reason": "Pass", "recommendation": "Every HTML document has a `lang` attribute"}, {"level": "", "rule": "A", "status": "HTML Lang Attribute Value", "reason": "Pass", "recommendation": "The `lang` attribute of the `<html>` element has a valid value"}, {"level": "", "rule": "A", "status": "Image Alt Text", "reason": "Pass", "recommendation": "`<img>` elements have alternate text or a role of none or presentation"}, {"level": "", "rule": "A", "status": "Link Name", "reason": "Pass", "recommendation": "Links have discernible text"}, {"level": "", "rule": "AA", "status": "Meta Viewport", "reason": "Pass", "recommendation": "`<meta name=\"viewport\">` does not disable text scaling and zooming"}, {"level": "", "rule": "A", "status": "Bypass Blocks", "reason": "Pass", "recommendation": "Each page has at least one mechanism for a user to bypass navigation and jump straight to the content"}, {"level": "", "rule": "N/A", "status": "Nested Interactive Elements", "reason": "Pass", "recommendation": "Interactive controls are not nested"}, {"level": "", "rule": "N/A", "status": "Scrollable Region Focusable", "reason": "Pass", "recommendation": "Elements that have scrollable content are accessible by keyboard"}, {"level": "", "rule": "N/A", "status": "ARIA", "reason": "Fail", "recommendation": "`role=img` elements are missing accessible names"}, {"level": "", "rule": "N/A", "status": "ARIA", "reason": "Fail", "recommendation": "Duplicate IDs"}, {"level": "", "rule": "N/A", "status": "Headings", "reason": "Pass", "recommendation": "Heading checks passed"}, {"level": "", "rule": "N/A", "status": "Language", "reason": "Pass", "recommendation": "Language checks passed"}, {"level": "", "rule": "N/A", "status": "Images", "reason": "Pass", "recommendation": "Image checks passed"}, {"level": "", "rule": "N/A", "status": "Forms", "reason": "Pass", "recommendation": "Forms checks passed"}, {"level": "", "rule": "N/A", "status": "Keyboard", "reason": "Pass", "recommendation": "Keyboard checks passed"}, {"level": "", "rule": "N/A", "status": "Videos", "reason": "Pass", "recommendation": "Video checks passed"}, {"level": "", "rule": "N/A", "status": "Contrast", "reason": "Pass", "recommendation": "Contrast checks passed"}, {"level": "", "rule": "N/A", "status": "Semantics", "reason": "Pass", "recommendation": "Semantic checks passed"}], "summary": {"pass": 1, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/password", "rows": [{"level": "", "rule": "Level", "status": "Rule", "reason": "Pass/Fail", "recommendation": "Reason"}, {"level": "", "rule": "---", "status": "---", "reason": "---", "recommendation": "---"}, {"level": "", "rule": "A", "status": "1.3.1: Info and Relationships", "reason": "Fail", "recommendation": "The `<ol>` element contains a `<pre>` element directly, which is not allowed."}, {"level": "", "rule": "A", "status": "4.1.2: Name, Role, Value", "reason": "Fail", "recommendation": "ARIA attributes are used to define a role of `img` but there is no accessible name provided."}, {"level": "", "rule": "A", "status": "4.1.2: Name, Role, Value", "reason": "Fail", "recommendation": "Duplicate ID."}, {"level": "", "rule": "A", "status": "1.1.1: Non-text Content", "reason": "N/A", "recommendation": "Some `<img>` elements with `role=\"img\"` are missing alternative text."}], "summary": {"pass": 0, "fail": 0, "warning": 0}}];
49
+ const tableRows = [];
50
+ const pageTables = {};
51
+ const buttons = document.querySelectorAll(".filter-btn");
52
+
53
+ // Functions
54
+ function addRow(page, level, rule, status, reason, recommendation) {
55
+ if(!pageTables[page]) {
56
+ const container = document.getElementById("audit-tables");
57
+ const header = document.createElement("div");
58
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
59
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
60
+ const tableDiv = document.createElement("div");
61
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
62
+ const table = document.createElement("table");
63
+ table.className = "min-w-full bg-white shadow rounded";
64
+ table.innerHTML = `
65
+ <thead class="bg-gray-100">
66
+ <tr>
67
+ <th class="px-4 py-2">Level</th>
68
+ <th class="px-4 py-2">Rule</th>
69
+ <th class="px-4 py-2">Pass/Fail</th>
70
+ <th class="px-4 py-2">Reason</th>
71
+ <th class="px-4 py-2">Recommendation</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody></tbody>
75
+ `;
76
+ tableDiv.appendChild(table);
77
+ container.appendChild(header);
78
+ container.appendChild(tableDiv);
79
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
80
+ pageTables[page] = table.querySelector("tbody");
81
+ }
82
+
83
+ const tbody = pageTables[page];
84
+ const tr = document.createElement("tr");
85
+ tr.className = "border-b";
86
+ tr.dataset.status = status.toLowerCase();
87
+ tr.dataset.page = page;
88
+ tr.innerHTML = `
89
+ <td class="px-4 py-2">${level}</td>
90
+ <td class="px-4 py-2">${rule}</td>
91
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
92
+ <td class="px-4 py-2">${reason}</td>
93
+ <td class="px-4 py-2">${recommendation}</td>
94
+ `;
95
+ tableRows.push(tr);
96
+ tbody.appendChild(tr);
97
+ }
98
+
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ const container = document.getElementById("summary-cards");
101
+ const total = passed + failed + warning;
102
+ const passRatio = total>0?passed/total:0;
103
+ let colorClass="bg-green-400";
104
+ if(passRatio<0.3) colorClass="bg-red-500";
105
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
106
+ const passPercent = total>0?(passed/total*100):0;
107
+ const failPercent = total>0?(failed/total*100):0;
108
+ const warnPercent = total>0?(warning/total*100):0;
109
+
110
+ const card = document.createElement("div");
111
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
112
+ card.innerHTML=`
113
+ <div class="flex justify-between items-center mb-2">
114
+ <h3 class="font-bold text-lg">${page}</h3>
115
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
116
+ </div>
117
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
118
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
119
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
120
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
121
+ </div>
122
+ <div class="text-sm">
123
+ <p class="text-green-600">Passed: ${passed}</p>
124
+ <p class="text-red-600">Failed: ${failed}</p>
125
+ <p class="text-yellow-500">Warnings: ${warning}</p>
126
+ </div>
127
+ `;
128
+ card.addEventListener("click", ()=>{
129
+ const row = tableRows.find(r=>r.dataset.page===page);
130
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
131
+ });
132
+ container.appendChild(card);
133
+ }
134
+
135
+ function updateTable(filter){
136
+ Object.values(pageTables).forEach(tbody=>{
137
+ Array.from(tbody.children).forEach(row=>{
138
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
139
+ else row.style.display="none";
140
+ });
141
+ });
142
+ }
143
+
144
+ buttons.forEach(btn=>{
145
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
146
+ });
147
+
148
+ // Scroll to top
149
+ const scrollBtn=document.getElementById("scrollTopBtn");
150
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
151
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
152
+ scrollBtn.style.display="none";
153
+
154
+ // Update totals in hero banner
155
+ function updateTotals(passed, failed, warning){
156
+ document.getElementById("total-pass").innerText=passed;
157
+ document.getElementById("total-fail").innerText=failed;
158
+ document.getElementById("total-warning").innerText=warning;
159
+ const total=passed+failed+warning;
160
+ const ratio=total>0?passed/total:0;
161
+ const summaryEl=document.getElementById("summary");
162
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
163
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
164
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
165
+ else summaryEl.classList.add("text-green-500");
166
+ }
167
+
168
+ // Render dashboard from auditData
169
+ auditData.forEach(pageData=>{
170
+ const page = pageData.page;
171
+ const rows = pageData.rows;
172
+ const summary = pageData.summary;
173
+ addSummaryCard(page, summary.pass, summary.fail, summary.warning);
174
+ rows.forEach(r=>addRow(page, r.level, r.rule, r.status, r.reason, r.recommendation));
175
+ });
176
+
177
+ // Update totals
178
+ let totalPass=0,totalFail=0,totalWarning=0;
179
+ auditData.forEach(p=>{
180
+ totalPass+=p.summary.pass;
181
+ totalFail+=p.summary.fail;
182
+ totalWarning+=p.summary.warning;
183
+ });
184
+ updateTotals(totalPass,totalFail,totalWarning);
185
+ </script>
186
+
187
+ </body>
188
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251112_072100.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; }
10
+ .fail { color: #dc2626; }
11
+ .warning { color: #f59e0b; }
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <!-- Inject audit data as JSON -->
47
+ <script>
48
+ const auditData = [{"page": "https://www.microsoft.com/en-us/dynamics-365/blog/?msockid=303a71da1b9a63221fb467ea1a1462a0", "rows": [{"level": "", "rule": "Level", "status": "Rule", "reason": "Pass/Fail", "recommendation": "Reason"}, {"level": "", "rule": "-----", "status": "--------------------------", "reason": "---------", "recommendation": "----------------------------------------------------------"}, {"level": "", "rule": "A", "status": "Document Title", "reason": "Pass", "recommendation": "Document has a non-empty `<title>` element"}, {"level": "", "rule": "A", "status": "HTML has lang", "reason": "Pass", "recommendation": "The `<html>` element has a lang attribute"}, {"level": "", "rule": "A", "status": "HTML lang valid", "reason": "Pass", "recommendation": "The lang attribute of the `<html>` element has a valid value"}, {"level": "", "rule": "A", "status": "Bypass Blocks", "reason": "Pass", "recommendation": "Page has a heading and a landmark region"}, {"level": "", "rule": "AA", "status": "Color Contrast", "reason": "Pass", "recommendation": "Elements have sufficient color contrast"}, {"level": "", "rule": "AA", "status": "Meta Viewport", "reason": "Pass", "recommendation": "`<meta>` tag does not disable zooming on mobile devices"}, {"level": "", "rule": "A", "status": "Image Alt", "reason": "Fail", "recommendation": "Missing alternative text for some images"}, {"level": "", "rule": "A", "status": "Form Label", "reason": "Fail", "recommendation": "Form field missing a label"}, {"level": "", "rule": "A", "status": "ARIA Attributes", "reason": "Pass", "recommendation": "ARIA attributes are valid."}, {"level": "", "rule": "A", "status": "ARIA Roles", "reason": "Pass", "recommendation": "ARIA roles used are valid."}, {"level": "", "rule": "A", "status": "ARIA Required Attributes", "reason": "Pass", "recommendation": "Required ARIA attributes are present."}, {"level": "", "rule": "A", "status": "Unique ARIA ID", "reason": "Pass", "recommendation": "No duplicate ARIA IDs"}, {"level": "", "rule": "A", "status": "List Structure", "reason": "Pass", "recommendation": "Lists are structured correctly"}, {"level": "", "rule": "A", "status": "List Item Structure", "reason": "Pass", "recommendation": "List items are contained in a `<ul>` or `<ol>`"}, {"level": "", "rule": "A", "status": "Link Text", "reason": "Pass", "recommendation": "Links have discernible text"}, {"level": "", "rule": "A", "status": "Button Text", "reason": "Pass", "recommendation": "Buttons have discernible text"}, {"level": "", "rule": "A", "status": "Nested Interactive", "reason": "Pass", "recommendation": "No nested interactive elements"}, {"level": "", "rule": "A", "status": "SVG Alternative Text", "reason": "Pass", "recommendation": "SVG images have an alternative text"}, {"level": "", "rule": "A", "status": "Heading Order", "reason": "Fail", "recommendation": "Heading level should be one greater than previous heading"}], "summary": {"pass": 1, "fail": 0, "warning": 0}}];
49
+ const tableRows = [];
50
+ const pageTables = {};
51
+ const buttons = document.querySelectorAll(".filter-btn");
52
+
53
+ // Functions
54
+ function addRow(page, level, rule, status, reason, recommendation) {
55
+ if(!pageTables[page]) {
56
+ const container = document.getElementById("audit-tables");
57
+ const header = document.createElement("div");
58
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
59
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
60
+ const tableDiv = document.createElement("div");
61
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
62
+ const table = document.createElement("table");
63
+ table.className = "min-w-full bg-white shadow rounded";
64
+ table.innerHTML = `
65
+ <thead class="bg-gray-100">
66
+ <tr>
67
+ <th class="px-4 py-2">Level</th>
68
+ <th class="px-4 py-2">Rule</th>
69
+ <th class="px-4 py-2">Pass/Fail</th>
70
+ <th class="px-4 py-2">Reason</th>
71
+ <th class="px-4 py-2">Recommendation</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody></tbody>
75
+ `;
76
+ tableDiv.appendChild(table);
77
+ container.appendChild(header);
78
+ container.appendChild(tableDiv);
79
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
80
+ pageTables[page] = table.querySelector("tbody");
81
+ }
82
+
83
+ const tbody = pageTables[page];
84
+ const tr = document.createElement("tr");
85
+ tr.className = "border-b";
86
+ tr.dataset.status = status.toLowerCase();
87
+ tr.dataset.page = page;
88
+ tr.innerHTML = `
89
+ <td class="px-4 py-2">${level}</td>
90
+ <td class="px-4 py-2">${rule}</td>
91
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
92
+ <td class="px-4 py-2">${reason}</td>
93
+ <td class="px-4 py-2">${recommendation}</td>
94
+ `;
95
+ tableRows.push(tr);
96
+ tbody.appendChild(tr);
97
+ }
98
+
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ const container = document.getElementById("summary-cards");
101
+ const total = passed + failed + warning;
102
+ const passRatio = total>0?passed/total:0;
103
+ let colorClass="bg-green-400";
104
+ if(passRatio<0.3) colorClass="bg-red-500";
105
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
106
+ const passPercent = total>0?(passed/total*100):0;
107
+ const failPercent = total>0?(failed/total*100):0;
108
+ const warnPercent = total>0?(warning/total*100):0;
109
+
110
+ const card = document.createElement("div");
111
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
112
+ card.innerHTML=`
113
+ <div class="flex justify-between items-center mb-2">
114
+ <h3 class="font-bold text-lg">${page}</h3>
115
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
116
+ </div>
117
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
118
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
119
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
120
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
121
+ </div>
122
+ <div class="text-sm">
123
+ <p class="text-green-600">Passed: ${passed}</p>
124
+ <p class="text-red-600">Failed: ${failed}</p>
125
+ <p class="text-yellow-500">Warnings: ${warning}</p>
126
+ </div>
127
+ `;
128
+ card.addEventListener("click", ()=>{
129
+ const row = tableRows.find(r=>r.dataset.page===page);
130
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
131
+ });
132
+ container.appendChild(card);
133
+ }
134
+
135
+ function updateTable(filter){
136
+ Object.values(pageTables).forEach(tbody=>{
137
+ Array.from(tbody.children).forEach(row=>{
138
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
139
+ else row.style.display="none";
140
+ });
141
+ });
142
+ }
143
+
144
+ buttons.forEach(btn=>{
145
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
146
+ });
147
+
148
+ // Scroll to top
149
+ const scrollBtn=document.getElementById("scrollTopBtn");
150
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
151
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
152
+ scrollBtn.style.display="none";
153
+
154
+ // Update totals in hero banner
155
+ function updateTotals(passed, failed, warning){
156
+ document.getElementById("total-pass").innerText=passed;
157
+ document.getElementById("total-fail").innerText=failed;
158
+ document.getElementById("total-warning").innerText=warning;
159
+ const total=passed+failed+warning;
160
+ const ratio=total>0?passed/total:0;
161
+ const summaryEl=document.getElementById("summary");
162
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
163
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
164
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
165
+ else summaryEl.classList.add("text-green-500");
166
+ }
167
+
168
+ // Render dashboard from auditData
169
+ auditData.forEach(pageData=>{
170
+ const page = pageData.page;
171
+ const rows = pageData.rows;
172
+ const summary = pageData.summary;
173
+ addSummaryCard(page, summary.pass, summary.fail, summary.warning);
174
+ rows.forEach(r=>addRow(page, r.level, r.rule, r.status, r.reason, r.recommendation));
175
+ });
176
+
177
+ // Update totals
178
+ let totalPass=0,totalFail=0,totalWarning=0;
179
+ auditData.forEach(p=>{
180
+ totalPass+=p.summary.pass;
181
+ totalFail+=p.summary.fail;
182
+ totalWarning+=p.summary.warning;
183
+ });
184
+ updateTotals(totalPass,totalFail,totalWarning);
185
+ </script>
186
+
187
+ </body>
188
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251211_051059.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; }
10
+ .fail { color: #dc2626; }
11
+ .warning { color: #f59e0b; }
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <!-- Inject audit data as JSON -->
47
+ <script>
48
+ const auditData = [{"page": "https://oauthapp.azurewebsites.net/", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/code", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/credential", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/implicit", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/password", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}];
49
+ const tableRows = [];
50
+ const pageTables = {};
51
+ const buttons = document.querySelectorAll(".filter-btn");
52
+
53
+ // Functions
54
+ function addRow(page, level, rule, status, reason, recommendation) {
55
+ if(!pageTables[page]) {
56
+ const container = document.getElementById("audit-tables");
57
+ const header = document.createElement("div");
58
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
59
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
60
+ const tableDiv = document.createElement("div");
61
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
62
+ const table = document.createElement("table");
63
+ table.className = "min-w-full bg-white shadow rounded";
64
+ table.innerHTML = `
65
+ <thead class="bg-gray-100">
66
+ <tr>
67
+ <th class="px-4 py-2">Level</th>
68
+ <th class="px-4 py-2">Rule</th>
69
+ <th class="px-4 py-2">Pass/Fail</th>
70
+ <th class="px-4 py-2">Reason</th>
71
+ <th class="px-4 py-2">Recommendation</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody></tbody>
75
+ `;
76
+ tableDiv.appendChild(table);
77
+ container.appendChild(header);
78
+ container.appendChild(tableDiv);
79
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
80
+ pageTables[page] = table.querySelector("tbody");
81
+ }
82
+
83
+ const tbody = pageTables[page];
84
+ const tr = document.createElement("tr");
85
+ tr.className = "border-b";
86
+ tr.dataset.status = status.toLowerCase();
87
+ tr.dataset.page = page;
88
+ tr.innerHTML = `
89
+ <td class="px-4 py-2">${level}</td>
90
+ <td class="px-4 py-2">${rule}</td>
91
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
92
+ <td class="px-4 py-2">${reason}</td>
93
+ <td class="px-4 py-2">${recommendation}</td>
94
+ `;
95
+ tableRows.push(tr);
96
+ tbody.appendChild(tr);
97
+ }
98
+
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ const container = document.getElementById("summary-cards");
101
+ const total = passed + failed + warning;
102
+ const passRatio = total>0?passed/total:0;
103
+ let colorClass="bg-green-400";
104
+ if(passRatio<0.3) colorClass="bg-red-500";
105
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
106
+ const passPercent = total>0?(passed/total*100):0;
107
+ const failPercent = total>0?(failed/total*100):0;
108
+ const warnPercent = total>0?(warning/total*100):0;
109
+
110
+ const card = document.createElement("div");
111
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
112
+ card.innerHTML=`
113
+ <div class="flex justify-between items-center mb-2">
114
+ <h3 class="font-bold text-lg">${page}</h3>
115
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
116
+ </div>
117
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
118
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
119
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
120
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
121
+ </div>
122
+ <div class="text-sm">
123
+ <p class="text-green-600">Passed: ${passed}</p>
124
+ <p class="text-red-600">Failed: ${failed}</p>
125
+ <p class="text-yellow-500">Warnings: ${warning}</p>
126
+ </div>
127
+ `;
128
+ card.addEventListener("click", ()=>{
129
+ const row = tableRows.find(r=>r.dataset.page===page);
130
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
131
+ });
132
+ container.appendChild(card);
133
+ }
134
+
135
+ function updateTable(filter){
136
+ Object.values(pageTables).forEach(tbody=>{
137
+ Array.from(tbody.children).forEach(row=>{
138
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
139
+ else row.style.display="none";
140
+ });
141
+ });
142
+ }
143
+
144
+ buttons.forEach(btn=>{
145
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
146
+ });
147
+
148
+ // Scroll to top
149
+ const scrollBtn=document.getElementById("scrollTopBtn");
150
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
151
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
152
+ scrollBtn.style.display="none";
153
+
154
+ // Update totals in hero banner
155
+ function updateTotals(passed, failed, warning){
156
+ document.getElementById("total-pass").innerText=passed;
157
+ document.getElementById("total-fail").innerText=failed;
158
+ document.getElementById("total-warning").innerText=warning;
159
+ const total=passed+failed+warning;
160
+ const ratio=total>0?passed/total:0;
161
+ const summaryEl=document.getElementById("summary");
162
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
163
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
164
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
165
+ else summaryEl.classList.add("text-green-500");
166
+ }
167
+
168
+ // Render dashboard from auditData
169
+ auditData.forEach(pageData=>{
170
+ const page = pageData.page;
171
+ const rows = pageData.rows;
172
+ const summary = pageData.summary;
173
+ addSummaryCard(page, summary.pass, summary.fail, summary.warning);
174
+ rows.forEach(r=>addRow(page, r.level, r.rule, r.status, r.reason, r.recommendation));
175
+ });
176
+
177
+ // Update totals
178
+ let totalPass=0,totalFail=0,totalWarning=0;
179
+ auditData.forEach(p=>{
180
+ totalPass+=p.summary.pass;
181
+ totalFail+=p.summary.fail;
182
+ totalWarning+=p.summary.warning;
183
+ });
184
+ updateTotals(totalPass,totalFail,totalWarning);
185
+ </script>
186
+
187
+ </body>
188
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251211_051323.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; }
10
+ .fail { color: #dc2626; }
11
+ .warning { color: #f59e0b; }
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <!-- Inject audit data as JSON -->
47
+ <script>
48
+ const auditData = [{"page": "https://oauthapp.azurewebsites.net/", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/code", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/credential", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/implicit", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/password", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}];
49
+ const tableRows = [];
50
+ const pageTables = {};
51
+ const buttons = document.querySelectorAll(".filter-btn");
52
+
53
+ // Functions
54
+ function addRow(page, level, rule, status, reason, recommendation) {
55
+ if(!pageTables[page]) {
56
+ const container = document.getElementById("audit-tables");
57
+ const header = document.createElement("div");
58
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
59
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
60
+ const tableDiv = document.createElement("div");
61
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
62
+ const table = document.createElement("table");
63
+ table.className = "min-w-full bg-white shadow rounded";
64
+ table.innerHTML = `
65
+ <thead class="bg-gray-100">
66
+ <tr>
67
+ <th class="px-4 py-2">Level</th>
68
+ <th class="px-4 py-2">Rule</th>
69
+ <th class="px-4 py-2">Pass/Fail</th>
70
+ <th class="px-4 py-2">Reason</th>
71
+ <th class="px-4 py-2">Recommendation</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody></tbody>
75
+ `;
76
+ tableDiv.appendChild(table);
77
+ container.appendChild(header);
78
+ container.appendChild(tableDiv);
79
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
80
+ pageTables[page] = table.querySelector("tbody");
81
+ }
82
+
83
+ const tbody = pageTables[page];
84
+ const tr = document.createElement("tr");
85
+ tr.className = "border-b";
86
+ tr.dataset.status = status.toLowerCase();
87
+ tr.dataset.page = page;
88
+ tr.innerHTML = `
89
+ <td class="px-4 py-2">${level}</td>
90
+ <td class="px-4 py-2">${rule}</td>
91
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
92
+ <td class="px-4 py-2">${reason}</td>
93
+ <td class="px-4 py-2">${recommendation}</td>
94
+ `;
95
+ tableRows.push(tr);
96
+ tbody.appendChild(tr);
97
+ }
98
+
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ const container = document.getElementById("summary-cards");
101
+ const total = passed + failed + warning;
102
+ const passRatio = total>0?passed/total:0;
103
+ let colorClass="bg-green-400";
104
+ if(passRatio<0.3) colorClass="bg-red-500";
105
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
106
+ const passPercent = total>0?(passed/total*100):0;
107
+ const failPercent = total>0?(failed/total*100):0;
108
+ const warnPercent = total>0?(warning/total*100):0;
109
+
110
+ const card = document.createElement("div");
111
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
112
+ card.innerHTML=`
113
+ <div class="flex justify-between items-center mb-2">
114
+ <h3 class="font-bold text-lg">${page}</h3>
115
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
116
+ </div>
117
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
118
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
119
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
120
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
121
+ </div>
122
+ <div class="text-sm">
123
+ <p class="text-green-600">Passed: ${passed}</p>
124
+ <p class="text-red-600">Failed: ${failed}</p>
125
+ <p class="text-yellow-500">Warnings: ${warning}</p>
126
+ </div>
127
+ `;
128
+ card.addEventListener("click", ()=>{
129
+ const row = tableRows.find(r=>r.dataset.page===page);
130
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
131
+ });
132
+ container.appendChild(card);
133
+ }
134
+
135
+ function updateTable(filter){
136
+ Object.values(pageTables).forEach(tbody=>{
137
+ Array.from(tbody.children).forEach(row=>{
138
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
139
+ else row.style.display="none";
140
+ });
141
+ });
142
+ }
143
+
144
+ buttons.forEach(btn=>{
145
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
146
+ });
147
+
148
+ // Scroll to top
149
+ const scrollBtn=document.getElementById("scrollTopBtn");
150
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
151
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
152
+ scrollBtn.style.display="none";
153
+
154
+ // Update totals in hero banner
155
+ function updateTotals(passed, failed, warning){
156
+ document.getElementById("total-pass").innerText=passed;
157
+ document.getElementById("total-fail").innerText=failed;
158
+ document.getElementById("total-warning").innerText=warning;
159
+ const total=passed+failed+warning;
160
+ const ratio=total>0?passed/total:0;
161
+ const summaryEl=document.getElementById("summary");
162
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
163
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
164
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
165
+ else summaryEl.classList.add("text-green-500");
166
+ }
167
+
168
+ // Render dashboard from auditData
169
+ auditData.forEach(pageData=>{
170
+ const page = pageData.page;
171
+ const rows = pageData.rows;
172
+ const summary = pageData.summary;
173
+ addSummaryCard(page, summary.pass, summary.fail, summary.warning);
174
+ rows.forEach(r=>addRow(page, r.level, r.rule, r.status, r.reason, r.recommendation));
175
+ });
176
+
177
+ // Update totals
178
+ let totalPass=0,totalFail=0,totalWarning=0;
179
+ auditData.forEach(p=>{
180
+ totalPass+=p.summary.pass;
181
+ totalFail+=p.summary.fail;
182
+ totalWarning+=p.summary.warning;
183
+ });
184
+ updateTotals(totalPass,totalFail,totalWarning);
185
+ </script>
186
+
187
+ </body>
188
+ </html>
src/accessibility_v1/output/accessibility_dashboard_20251211_052404.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; }
10
+ .fail { color: #dc2626; }
11
+ .warning { color: #f59e0b; }
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <!-- Inject audit data as JSON -->
47
+ <script>
48
+ const auditData = [{"page": "https://oauthapp.azurewebsites.net/", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/code", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/credential", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/implicit", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}, {"page": "https://oauthapp.azurewebsites.net/password", "rows": [], "summary": {"pass": 0, "fail": 0, "warning": 0}}];
49
+ const tableRows = [];
50
+ const pageTables = {};
51
+ const buttons = document.querySelectorAll(".filter-btn");
52
+
53
+ // Functions
54
+ function addRow(page, level, rule, status, reason, recommendation) {
55
+ if(!pageTables[page]) {
56
+ const container = document.getElementById("audit-tables");
57
+ const header = document.createElement("div");
58
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
59
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
60
+ const tableDiv = document.createElement("div");
61
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
62
+ const table = document.createElement("table");
63
+ table.className = "min-w-full bg-white shadow rounded";
64
+ table.innerHTML = `
65
+ <thead class="bg-gray-100">
66
+ <tr>
67
+ <th class="px-4 py-2">Level</th>
68
+ <th class="px-4 py-2">Rule</th>
69
+ <th class="px-4 py-2">Pass/Fail</th>
70
+ <th class="px-4 py-2">Reason</th>
71
+ <th class="px-4 py-2">Recommendation</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody></tbody>
75
+ `;
76
+ tableDiv.appendChild(table);
77
+ container.appendChild(header);
78
+ container.appendChild(tableDiv);
79
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
80
+ pageTables[page] = table.querySelector("tbody");
81
+ }
82
+
83
+ const tbody = pageTables[page];
84
+ const tr = document.createElement("tr");
85
+ tr.className = "border-b";
86
+ tr.dataset.status = status.toLowerCase();
87
+ tr.dataset.page = page;
88
+ tr.innerHTML = `
89
+ <td class="px-4 py-2">${level}</td>
90
+ <td class="px-4 py-2">${rule}</td>
91
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
92
+ <td class="px-4 py-2">${reason}</td>
93
+ <td class="px-4 py-2">${recommendation}</td>
94
+ `;
95
+ tableRows.push(tr);
96
+ tbody.appendChild(tr);
97
+ }
98
+
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ const container = document.getElementById("summary-cards");
101
+ const total = passed + failed + warning;
102
+ const passRatio = total>0?passed/total:0;
103
+ let colorClass="bg-green-400";
104
+ if(passRatio<0.3) colorClass="bg-red-500";
105
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
106
+ const passPercent = total>0?(passed/total*100):0;
107
+ const failPercent = total>0?(failed/total*100):0;
108
+ const warnPercent = total>0?(warning/total*100):0;
109
+
110
+ const card = document.createElement("div");
111
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
112
+ card.innerHTML=`
113
+ <div class="flex justify-between items-center mb-2">
114
+ <h3 class="font-bold text-lg">${page}</h3>
115
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
116
+ </div>
117
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
118
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
119
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
120
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
121
+ </div>
122
+ <div class="text-sm">
123
+ <p class="text-green-600">Passed: ${passed}</p>
124
+ <p class="text-red-600">Failed: ${failed}</p>
125
+ <p class="text-yellow-500">Warnings: ${warning}</p>
126
+ </div>
127
+ `;
128
+ card.addEventListener("click", ()=>{
129
+ const row = tableRows.find(r=>r.dataset.page===page);
130
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
131
+ });
132
+ container.appendChild(card);
133
+ }
134
+
135
+ function updateTable(filter){
136
+ Object.values(pageTables).forEach(tbody=>{
137
+ Array.from(tbody.children).forEach(row=>{
138
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
139
+ else row.style.display="none";
140
+ });
141
+ });
142
+ }
143
+
144
+ buttons.forEach(btn=>{
145
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
146
+ });
147
+
148
+ // Scroll to top
149
+ const scrollBtn=document.getElementById("scrollTopBtn");
150
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
151
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
152
+ scrollBtn.style.display="none";
153
+
154
+ // Update totals in hero banner
155
+ function updateTotals(passed, failed, warning){
156
+ document.getElementById("total-pass").innerText=passed;
157
+ document.getElementById("total-fail").innerText=failed;
158
+ document.getElementById("total-warning").innerText=warning;
159
+ const total=passed+failed+warning;
160
+ const ratio=total>0?passed/total:0;
161
+ const summaryEl=document.getElementById("summary");
162
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
163
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
164
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
165
+ else summaryEl.classList.add("text-green-500");
166
+ }
167
+
168
+ // Render dashboard from auditData
169
+ auditData.forEach(pageData=>{
170
+ const page = pageData.page;
171
+ const rows = pageData.rows;
172
+ const summary = pageData.summary;
173
+ addSummaryCard(page, summary.pass, summary.fail, summary.warning);
174
+ rows.forEach(r=>addRow(page, r.level, r.rule, r.status, r.reason, r.recommendation));
175
+ });
176
+
177
+ // Update totals
178
+ let totalPass=0,totalFail=0,totalWarning=0;
179
+ auditData.forEach(p=>{
180
+ totalPass+=p.summary.pass;
181
+ totalFail+=p.summary.fail;
182
+ totalWarning+=p.summary.warning;
183
+ });
184
+ updateTotals(totalPass,totalFail,totalWarning);
185
+ </script>
186
+
187
+ </body>
188
+ </html>
src/accessibility_v1/templates/dashboard_template.html ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accessibility Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ .pass { color: #16a34a; }
10
+ .fail { color: #dc2626; }
11
+ .warning { color: #f59e0b; }
12
+ .summary-card { cursor: pointer; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-100 font-sans">
16
+ <header class="bg-blue-600 text-white p-6 shadow-md sticky top-0 z-50">
17
+ <h1 class="text-3xl font-bold" id="site-name">Site Accessibility Dashboard</h1>
18
+ <p class="mt-2" id="summary">
19
+ Summary: All Pages
20
+ | Passed: <span id="total-pass">0</span>
21
+ | Failed: <span id="total-fail">0</span>
22
+ | Warnings: <span id="total-warning">0</span>
23
+ </p>
24
+ </header>
25
+
26
+ <!-- Page Summary Cards -->
27
+ <div class="flex flex-wrap gap-4 px-6 mb-4" id="summary-cards"></div>
28
+
29
+ <!-- Filters -->
30
+ <div class="flex space-x-4 mb-4 px-6">
31
+ <button class="filter-btn px-4 py-2 bg-gray-300 rounded" data-filter="all">All</button>
32
+ <button class="filter-btn px-4 py-2 bg-green-300 rounded" data-filter="pass">Passed</button>
33
+ <button class="filter-btn px-4 py-2 bg-red-300 rounded" data-filter="fail">Failed</button>
34
+ <button class="filter-btn px-4 py-2 bg-yellow-300 rounded" data-filter="warning">Warnings</button>
35
+ </div>
36
+
37
+ <!-- Audit Tables -->
38
+ <div id="audit-tables" class="px-6 space-y-4"></div>
39
+
40
+ <!-- Scroll-to-top button -->
41
+ <button id="scrollTopBtn"
42
+ class="fixed bottom-4 right-4 bg-blue-600 text-white px-4 py-2 rounded shadow-lg hover:bg-blue-700 transition-opacity opacity-75 hover:opacity-100">
43
+ ↑ Top
44
+ </button>
45
+
46
+ <!-- Inject audit data as JSON -->
47
+ <script>
48
+ const auditData = <!--AUDIT_JSON_PLACEHOLDER-->;
49
+ const tableRows = [];
50
+ const pageTables = {};
51
+ const buttons = document.querySelectorAll(".filter-btn");
52
+
53
+ // Functions
54
+ function addRow(page, level, rule, status, reason, recommendation) {
55
+ if(!pageTables[page]) {
56
+ const container = document.getElementById("audit-tables");
57
+ const header = document.createElement("div");
58
+ header.className = "bg-gray-200 px-4 py-2 rounded cursor-pointer shadow";
59
+ header.innerHTML = `<h2 class="font-bold">${page}</h2>`;
60
+ const tableDiv = document.createElement("div");
61
+ tableDiv.className = "overflow-x-auto mt-2 hidden";
62
+ const table = document.createElement("table");
63
+ table.className = "min-w-full bg-white shadow rounded";
64
+ table.innerHTML = `
65
+ <thead class="bg-gray-100">
66
+ <tr>
67
+ <th class="px-4 py-2">Level</th>
68
+ <th class="px-4 py-2">Rule</th>
69
+ <th class="px-4 py-2">Pass/Fail</th>
70
+ <th class="px-4 py-2">Reason</th>
71
+ <th class="px-4 py-2">Recommendation</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody></tbody>
75
+ `;
76
+ tableDiv.appendChild(table);
77
+ container.appendChild(header);
78
+ container.appendChild(tableDiv);
79
+ header.addEventListener("click", () => tableDiv.classList.toggle("hidden"));
80
+ pageTables[page] = table.querySelector("tbody");
81
+ }
82
+
83
+ const tbody = pageTables[page];
84
+ const tr = document.createElement("tr");
85
+ tr.className = "border-b";
86
+ tr.dataset.status = status.toLowerCase();
87
+ tr.dataset.page = page;
88
+ tr.innerHTML = `
89
+ <td class="px-4 py-2">${level}</td>
90
+ <td class="px-4 py-2">${rule}</td>
91
+ <td class="px-4 py-2 ${status.toLowerCase()}">${status}</td>
92
+ <td class="px-4 py-2">${reason}</td>
93
+ <td class="px-4 py-2">${recommendation}</td>
94
+ `;
95
+ tableRows.push(tr);
96
+ tbody.appendChild(tr);
97
+ }
98
+
99
+ function addSummaryCard(page, passed, failed, warning) {
100
+ const container = document.getElementById("summary-cards");
101
+ const total = passed + failed + warning;
102
+ const passRatio = total>0?passed/total:0;
103
+ let colorClass="bg-green-400";
104
+ if(passRatio<0.3) colorClass="bg-red-500";
105
+ else if(passRatio<0.7) colorClass="bg-yellow-400";
106
+ const passPercent = total>0?(passed/total*100):0;
107
+ const failPercent = total>0?(failed/total*100):0;
108
+ const warnPercent = total>0?(warning/total*100):0;
109
+
110
+ const card = document.createElement("div");
111
+ card.className="summary-card bg-white shadow p-4 rounded w-64 hover:shadow-lg";
112
+ card.innerHTML=`
113
+ <div class="flex justify-between items-center mb-2">
114
+ <h3 class="font-bold text-lg">${page}</h3>
115
+ <span class="px-2 py-1 text-white rounded ${colorClass}">Pass: ${passPercent.toFixed(0)}%</span>
116
+ </div>
117
+ <div class="flex h-3 mb-2 w-full rounded overflow-hidden shadow-inner">
118
+ <div class="bg-green-500 h-full" style="width:${passPercent}%;"></div>
119
+ <div class="bg-red-600 h-full" style="width:${failPercent}%;"></div>
120
+ <div class="bg-yellow-400 h-full" style="width:${warnPercent}%;"></div>
121
+ </div>
122
+ <div class="text-sm">
123
+ <p class="text-green-600">Passed: ${passed}</p>
124
+ <p class="text-red-600">Failed: ${failed}</p>
125
+ <p class="text-yellow-500">Warnings: ${warning}</p>
126
+ </div>
127
+ `;
128
+ card.addEventListener("click", ()=>{
129
+ const row = tableRows.find(r=>r.dataset.page===page);
130
+ if(row) row.scrollIntoView({behavior:"smooth", block:"start"});
131
+ });
132
+ container.appendChild(card);
133
+ }
134
+
135
+ function updateTable(filter){
136
+ Object.values(pageTables).forEach(tbody=>{
137
+ Array.from(tbody.children).forEach(row=>{
138
+ if(filter==="all"||row.dataset.status===filter) row.style.display="";
139
+ else row.style.display="none";
140
+ });
141
+ });
142
+ }
143
+
144
+ buttons.forEach(btn=>{
145
+ btn.addEventListener("click",()=>updateTable(btn.dataset.filter));
146
+ });
147
+
148
+ // Scroll to top
149
+ const scrollBtn=document.getElementById("scrollTopBtn");
150
+ scrollBtn.addEventListener("click",()=>window.scrollTo({top:0,behavior:"smooth"}));
151
+ window.addEventListener("scroll",()=>{scrollBtn.style.display=window.scrollY>200?"block":"none";});
152
+ scrollBtn.style.display="none";
153
+
154
+ // Update totals in hero banner
155
+ function updateTotals(passed, failed, warning){
156
+ document.getElementById("total-pass").innerText=passed;
157
+ document.getElementById("total-fail").innerText=failed;
158
+ document.getElementById("total-warning").innerText=warning;
159
+ const total=passed+failed+warning;
160
+ const ratio=total>0?passed/total:0;
161
+ const summaryEl=document.getElementById("summary");
162
+ summaryEl.classList.remove("text-green-500","text-yellow-400","text-red-500");
163
+ if(ratio<0.3) summaryEl.classList.add("text-red-500");
164
+ else if(ratio<0.7) summaryEl.classList.add("text-yellow-400");
165
+ else summaryEl.classList.add("text-green-500");
166
+ }
167
+
168
+ // Render dashboard from auditData
169
+ auditData.forEach(pageData=>{
170
+ const page = pageData.page;
171
+ const rows = pageData.rows;
172
+ const summary = pageData.summary;
173
+ addSummaryCard(page, summary.pass, summary.fail, summary.warning);
174
+ rows.forEach(r=>addRow(page, r.level, r.rule, r.status, r.reason, r.recommendation));
175
+ });
176
+
177
+ // Update totals
178
+ let totalPass=0,totalFail=0,totalWarning=0;
179
+ auditData.forEach(p=>{
180
+ totalPass+=p.summary.pass;
181
+ totalFail+=p.summary.fail;
182
+ totalWarning+=p.summary.warning;
183
+ });
184
+ updateTotals(totalPass,totalFail,totalWarning);
185
+ </script>
186
+
187
+ </body>
188
+ </html>
src/accessibility_v2/app.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import asyncio
3
+ from src.accessibility_v2.patterns.orchestrator import AccessibilityOrchestrator
4
+
5
+ st.set_page_config(page_title="Accessibility V2", layout="wide", page_icon="♿")
6
+
7
+ st.title("♿ AI Accessibility Auditor")
8
+ st.markdown("Enter a URL to perform a comprehensive WCAG 2.1 AA audit powered by Playwright and GPT-4o.")
9
+
10
+ url = st.text_input("Website URL", "https://example.com")
11
+
12
+ if st.button("Run Audit", type="primary"):
13
+ orchestrator = AccessibilityOrchestrator()
14
+ placeholder = st.empty()
15
+
16
+ async def run():
17
+ full_response = ""
18
+ async for update in orchestrator.audit_site(url):
19
+ if update.startswith("🔍") or update.startswith("🧠"):
20
+ placeholder.info(update)
21
+ else:
22
+ full_response = update # The final markdown
23
+
24
+ placeholder.empty()
25
+ st.markdown(full_response)
26
+
27
+ asyncio.run(run())
src/accessibility_v2/layers/action.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.accessibility_v2.tools.web_auditor import WebAuditor
2
+ from typing import Dict, Any
3
+
4
+ class ActionLayer:
5
+ """
6
+ Executes the accessibility audit.
7
+ """
8
+ def __init__(self):
9
+ self.auditor = WebAuditor()
10
+
11
+ async def execute(self, action: str, params: Dict[str, Any]) -> Dict[str, Any]:
12
+ if action == "audit_url":
13
+ url = params.get("url")
14
+ if not url:
15
+ return {"error": "No URL provided"}
16
+
17
+ print(f"[Action] Auditing {url}...")
18
+ return await self.auditor.full_audit(url)
19
+
20
+ return {"error": f"Unknown action: {action}"}
src/accessibility_v2/layers/cognition.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from openai import AsyncOpenAI
4
+
5
+ class CognitionLayer:
6
+ """
7
+ Analyzes audit data and generates readable reports.
8
+ """
9
+ def __init__(self):
10
+ api_key = os.environ.get("OPENAI_API_KEY")
11
+ self.client = AsyncOpenAI(api_key=api_key)
12
+ self.model_name = "gpt-4o"
13
+
14
+ async def analyze(self, audit_data: dict) -> str:
15
+ """
16
+ Takes raw audit JSON and produces a markdown summary with recommendations.
17
+ """
18
+ if not audit_data.get("success"):
19
+ return f"❌ Audit Failed: {audit_data.get('error')}"
20
+
21
+ summary_json = json.dumps(audit_data.get("data", {}).get("summary", {}), indent=2)
22
+
23
+ system_prompt = """
24
+ You are an **Accessibility Expert** (WCAG 2.1 AA Specialist).
25
+ You will receive a JSON summary of accessibility violations found on a webpage.
26
+
27
+ Your Goal:
28
+ 1. Summarize the key issues.
29
+ 2. Explain WHY they are barriers to users (e.g. screen readers, low vision).
30
+ 3. Provide actionable code snippets or recommendations to fix them.
31
+
32
+ Ouput Format:
33
+ Markdown. Use tables for lists of violations.
34
+ """
35
+
36
+ try:
37
+ completion = await self.client.chat.completions.create(
38
+ model=self.model_name,
39
+ messages=[
40
+ {"role": "system", "content": system_prompt},
41
+ {"role": "user", "content": f"Audit Data:\n{summary_json}"}
42
+ ]
43
+ )
44
+ return completion.choices[0].message.content
45
+ except Exception as e:
46
+ return f"Error analyzing results: {e}"
src/accessibility_v2/layers/perception.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import datetime
3
+ from urllib.parse import urlparse
4
+
5
+ @dataclass
6
+ class EnvironmentState:
7
+ url: str
8
+ timestamp: str
9
+ is_valid_url: bool
10
+ error_message: str = None
11
+
12
+ class PerceptionLayer:
13
+ """
14
+ Validates and accepts the target URL.
15
+ """
16
+
17
+ def perceive(self, raw_input: str) -> EnvironmentState:
18
+ url = raw_input.strip()
19
+ is_valid = self._validate_url(url)
20
+
21
+ return EnvironmentState(
22
+ url=url,
23
+ timestamp=datetime.datetime.now().isoformat(),
24
+ is_valid_url=is_valid,
25
+ error_message=None if is_valid else "Invalid URL format."
26
+ )
27
+
28
+ def _validate_url(self, url: str) -> bool:
29
+ try:
30
+ result = urlparse(url)
31
+ return all([result.scheme, result.netloc])
32
+ except:
33
+ return False
src/accessibility_v2/patterns/orchestrator.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.accessibility_v2.layers.perception import PerceptionLayer
2
+ from src.accessibility_v2.layers.action import ActionLayer
3
+ from src.accessibility_v2.layers.cognition import CognitionLayer
4
+
5
+ class AccessibilityOrchestrator:
6
+ def __init__(self):
7
+ self.perception = PerceptionLayer()
8
+ self.action = ActionLayer()
9
+ self.cognition = CognitionLayer()
10
+
11
+ async def audit_site(self, url: str):
12
+ # 1. Perception
13
+ state = self.perception.perceive(url)
14
+ if not state.is_valid_url:
15
+ yield f"❌ Invalid URL: {state.url}"
16
+ return
17
+
18
+ # 2. Action (Run Audit)
19
+ yield f"🔍 Auditing {state.url}..."
20
+ audit_result = await self.action.execute("audit_url", {"url": state.url})
21
+
22
+ if not audit_result.get("success"):
23
+ yield f"❌ Audit failed: {audit_result.get('error')}"
24
+ return
25
+
26
+ # 3. Cognition (Analyze)
27
+ yield "🧠 Analyzing results with AI..."
28
+ analysis = await self.cognition.analyze(audit_result)
29
+
30
+ yield analysis
src/accessibility_v2/tools/axe.min.js ADDED
The diff for this file is too large to render. See raw diff
 
src/accessibility_v2/tools/web_auditor.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import asyncio
3
+ from typing import Any, Dict, List, Optional
4
+ from playwright.async_api import async_playwright, Page
5
+
6
+ # Path to axe script
7
+ DEFAULT_AXE_PATH = os.path.join(os.path.dirname(__file__), "axe.min.js")
8
+ AXE_JS_PATH = os.environ.get("AXE_JS_PATH", DEFAULT_AXE_PATH)
9
+
10
+ class WebAuditor:
11
+ """
12
+ Encapsulates Playwright based accessibility auditing logic.
13
+ """
14
+
15
+ async def _open_page(self, url: str, timeout: int = 30000, wait_until: str = "load"):
16
+ playwright = await async_playwright().start()
17
+ browser = await playwright.chromium.launch(args=["--no-sandbox"], headless=True)
18
+ context = await browser.new_context()
19
+ page = await context.new_page()
20
+ try:
21
+ await page.goto(url, timeout=timeout, wait_until=wait_until)
22
+ await asyncio.sleep(0.5) # Settle
23
+ except Exception as e:
24
+ await context.close()
25
+ await browser.close()
26
+ await playwright.stop()
27
+ raise e
28
+
29
+ return playwright, browser, context, page
30
+
31
+ async def _ensure_axe(self, page: Page):
32
+ if not os.path.exists(AXE_JS_PATH):
33
+ raise FileNotFoundError(f"axe.min.js not found at {AXE_JS_PATH}")
34
+ with open(AXE_JS_PATH, "r", encoding="utf-8") as f:
35
+ axe_source = f.read()
36
+ await page.add_init_script(axe_source)
37
+ # Ensure it loaded
38
+ await page.evaluate("() => { window.__axe_injected = typeof axe !== 'undefined'; }")
39
+
40
+ async def _run_axe(self, page: Page, tags: Optional[List[str]] = None):
41
+ tags = tags or ["wcag2a", "wcag2aa"]
42
+ return await page.evaluate("""(tags) => {
43
+ return axe.run(document, {runOnly: {type: 'tag', values: tags}});
44
+ }""", tags)
45
+
46
+ async def full_audit(self, url: str) -> Dict[str, Any]:
47
+ playwright = browser = context = page = None
48
+ results = {}
49
+
50
+ try:
51
+ playwright, browser, context, page = await self._open_page(url)
52
+
53
+ # 1. Axe Audit
54
+ try:
55
+ await self._ensure_axe(page)
56
+ axe_res = await self._run_axe(page)
57
+ results["axe"] = axe_res
58
+ except Exception as e:
59
+ results["axe_error"] = str(e)
60
+
61
+ # 2. Simple Custom Checks (simplified for brevity here, can expand later)
62
+ results["title"] = await page.title()
63
+
64
+ # Map Axe violations to a simpler summary
65
+ violations = results.get("axe", {}).get("violations", [])
66
+ simple_violations = []
67
+ for v in violations:
68
+ simple_violations.append({
69
+ "id": v.get("id"),
70
+ "impact": v.get("impact"),
71
+ "description": v.get("description"),
72
+ "help": v.get("help"),
73
+ "nodes_count": len(v.get("nodes", []))
74
+ })
75
+
76
+ results["summary"] = {
77
+ "violation_count": len(violations),
78
+ "violations": simple_violations
79
+ }
80
+
81
+ return {"url": url, "success": True, "data": results}
82
+
83
+ except Exception as e:
84
+ return {"url": url, "success": False, "error": str(e)}
85
+ finally:
86
+ if context: await context.close()
87
+ if browser: await browser.close()
88
+ if playwright: await playwright.stop()
src/chatbot_v1/Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1 \
4
+ DEBIAN_FRONTEND=noninteractive \
5
+ PYTHONPATH=/app:/app/common:$PYTHONPATH
6
+
7
+ WORKDIR /app
8
+
9
+ # System deps
10
+ RUN apt-get update && apt-get install -y \
11
+ git build-essential curl \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Install uv
15
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
16
+ ENV PATH="/root/.local/bin:$PATH"
17
+
18
+ # Copy project metadata
19
+ COPY pyproject.toml .
20
+ COPY uv.lock .
21
+
22
+ # Copy required folders
23
+ COPY common/ ./common/
24
+ COPY src/chatbot/ ./src/chatbot/
25
+
26
+ # Install dependencies using uv, then export and install with pip to system
27
+ RUN uv sync --frozen --no-dev && \
28
+ uv pip install -e . --system
29
+
30
+ # Copy entry point
31
+ COPY run.py .
32
+
33
+ EXPOSE 7860
34
+
35
+ CMD ["python", "run.py", "chatbot", "--port", "7860"]
src/chatbot_v1/README.md ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Chatbot
3
+ emoji: 🤖
4
+ colorFrom: pink
5
+ colorTo: yellow
6
+ sdk: docker
7
+ sdk_version: "0.0.1"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ tags:
12
+ - text-generation
13
+ - agentic-ai
14
+ - openai-sdk
15
+ short_description: An Experimental Agentic Chatbot (uses OpenAI Agent SDK)
16
+ ---
17
+
18
+ # AI Chatbot
19
+
20
+ This is an experimental chatbot for chatting with AI. It is equipped with agents & tools to give you realtime data from the web. It uses **OpenAI - SDK** and **OpenAI - Agents**...
21
+
22
+ ## Features
23
+ - Predefined prompts for quick analysis
24
+ - Chat interface with AI responses
25
+ - Enter key support and responsive design
26
+ - Latest messages appear on top
27
+
28
+ ## Usage
29
+ 1. Type a message or select a predefined prompt
30
+ 2. Press **Enter** or click **Send**
31
+ 3. AI responses appear instantly in the chat interface
32
+
33
+ ## Supported APIs
34
+ - OpenAI
35
+ - Google
36
+ - GROQ
37
+ - SERPER
38
+ - News API
39
+
40
+ ## Notes
41
+ - Make sure your API keys are configured in the Space secrets
42
+ - Built using Streamlit and deployed as a Docker Space
43
+
44
+ ## References
45
+
46
+ https://openai.github.io/openai-agents-python/
47
+
48
+ https://github.com/openai/openai-agents-python/tree/main/examples/mcp
49
+
50
+ ## Project Folder Structure
51
+
52
+ ```
53
+ chatbot/
54
+ ├── app.py # Main Streamlit chatbot interface
55
+ ├── appagents/
56
+ │ ├── __init__.py # Package initialization
57
+ │ ├── OrchestratorAgent.py # Main orchestrator - coordinates all agents
58
+ │ ├── FinancialAgent.py # Financial data and analysis agent
59
+ │ ├── NewsAgent.py # News retrieval and summarization agent
60
+ │ ├── SearchAgent.py # General web search agent
61
+ │ └── InputValidationAgent.py # Input validation and sanitization agent
62
+ ├── core/
63
+ │ ├── __init__.py # Package initialization
64
+ │ └── logger.py # Centralized logging configuration
65
+ ├── tools/
66
+ │ ├── __init__.py # Package initialization
67
+ │ ├── google_tools.py # Google search API wrapper
68
+ │ ├── yahoo_tools.py # Yahoo Finance API wrapper
69
+ │ ├── news_tools.py # News API wrapper
70
+ │ └── time_tools.py # Time-related utility functions
71
+ ├── prompts/
72
+ │ ├── economic_news.txt # Prompt for economic news analysis
73
+ │ ├── market_sentiment.txt # Prompt for market sentiment analysis
74
+ │ ├── news_headlines.txt # Prompt for news headline summarization
75
+ │ ├── trade_recommendation.txt # Prompt for trade recommendations
76
+ │ └── upcoming_earnings.txt # Prompt for upcoming earnings alerts
77
+ ├── Dockerfile # Docker configuration for container deployment
78
+ ├── pyproject.toml # Project metadata and dependencies (copied from root)
79
+ ├── uv.lock # Locked dependency versions (copied from root)
80
+ ├── README.md # Project documentation
81
+ └── run.py # Script to run the application locally
82
+ ```
83
+
84
+ ## File Descriptions
85
+
86
+ ### UI Layer
87
+ - **app.py** - Main Streamlit chatbot interface that provides:
88
+ - Chat message display with user and AI messages
89
+ - Text input for user queries
90
+ - Predefined prompt buttons for quick analysis
91
+ - Real-time AI responses
92
+ - Support for Enter key submission
93
+ - Responsive design with latest messages appearing first
94
+
95
+ ### Agents (`appagents/`)
96
+ - **OrchestratorAgent.py** - Main orchestrator that:
97
+ - Coordinates communication between all specialized agents
98
+ - Routes user queries to appropriate agents
99
+ - Manages conversation flow and context
100
+ - Integrates tool responses
101
+
102
+ - **FinancialAgent.py** - Financial data and analysis:
103
+ - Retrieves stock prices and financial metrics
104
+ - Performs financial analysis using Yahoo Finance API
105
+ - Provides market insights and recommendations
106
+ - Integrates with yahoo_tools for data fetching
107
+
108
+ - **NewsAgent.py** - News retrieval and summarization:
109
+ - Fetches latest news articles
110
+ - Summarizes news content
111
+ - Integrates with News API for real-time updates
112
+ - Provides news-based market insights
113
+
114
+ - **SearchAgent.py** - General web search:
115
+ - Performs web searches for general queries
116
+ - Integrates with Google Search / Serper API
117
+ - Returns relevant search results
118
+ - Supports multi-source data gathering
119
+
120
+ - **InputValidationAgent.py** - Input validation:
121
+ - Sanitizes user input
122
+ - Validates query format and content
123
+ - Prevents malicious inputs
124
+ - Ensures appropriate content
125
+
126
+ ### Core Utilities (`core/`)
127
+ - **logger.py** - Centralized logging configuration:
128
+ - Provides consistent logging across agents
129
+ - Handles different log levels
130
+ - Formats log messages for clarity
131
+
132
+ ### Tools (`tools/`)
133
+ - **google_tools.py** - Google Search API wrapper:
134
+ - Executes web searches via Google Search / Serper API
135
+ - Parses and returns search results
136
+ - Handles API authentication
137
+
138
+ - **yahoo_tools.py** - Yahoo Finance API integration:
139
+ - Retrieves stock price data
140
+ - Fetches financial metrics and indicators
141
+ - Provides historical price data
142
+ - Returns earnings information
143
+
144
+ - **news_tools.py** - News API integration:
145
+ - Fetches latest news articles
146
+ - Filters news by category and keywords
147
+ - Returns news headlines and summaries
148
+ - Provides market-related news feeds
149
+
150
+ - **time_tools.py** - Time utility functions:
151
+ - Provides current time information
152
+ - Formats timestamps
153
+ - Handles timezone conversions
154
+
155
+ ### Prompts (`prompts/`)
156
+ Predefined prompts for specialized analysis:
157
+ - **economic_news.txt** - Analyzes economic news and implications
158
+ - **market_sentiment.txt** - Analyzes market sentiment trends
159
+ - **news_headlines.txt** - Summarizes and explains news headlines
160
+ - **trade_recommendation.txt** - Provides trading recommendations
161
+ - **upcoming_earnings.txt** - Alerts about upcoming earnings reports
162
+
163
+ ### Configuration Files
164
+ - **Dockerfile** - Container deployment:
165
+ - Builds Docker image with Python 3.12
166
+ - Installs dependencies using `uv`
167
+ - Sets up Streamlit server on port 8501
168
+ - Configures PYTHONPATH for module imports
169
+
170
+ - **pyproject.toml** - Project metadata:
171
+ - Package name: "agents"
172
+ - Python version requirement: 3.12
173
+ - Lists all dependencies (OpenAI, LangChain, Streamlit, etc.)
174
+
175
+ - **uv.lock** - Dependency lock file:
176
+ - Ensures reproducible builds
177
+ - Pins exact versions of all dependencies
178
+
179
+ ## Key Technologies
180
+
181
+ | Component | Technology | Purpose |
182
+ |-----------|-----------|---------|
183
+ | LLM Framework | OpenAI Agents | Multi-agent orchestration |
184
+ | Chat Interface | Streamlit | User interaction and display |
185
+ | Web Search | Google Search / Serper API | Web search results |
186
+ | Financial Data | Yahoo Finance API | Stock prices and metrics |
187
+ | News Data | News API | Latest news articles |
188
+ | Async Operations | AsyncIO | Parallel agent execution |
189
+ | Dependencies | UV | Fast Python package management |
190
+ | Containerization | Docker | Cloud deployment |
191
+
192
+ ## Predefined Prompts
193
+
194
+ The chatbot includes quick-access buttons for common analysis:
195
+
196
+ 1. **Economic News** - Analyzes current economic trends and news
197
+ 2. **Market Sentiment** - Provides market sentiment analysis
198
+ 3. **News Headlines** - Summarizes latest news headlines
199
+ 4. **Trade Recommendation** - Suggests trading strategies
200
+ 5. **Upcoming Earnings** - Lists upcoming company earnings
201
+
202
+ ## Running Locally
203
+
204
+ ```bash
205
+ # Install dependencies
206
+ uv sync
207
+
208
+ # Set environment variables defined in .env.name file
209
+ export OPENAI_API_KEY="your-key"
210
+ export SERPER_API_KEY="your-key"
211
+ export NEWS_API_KEY="your-key"
212
+
213
+ # Run the Streamlit app (from the root)
214
+ python run.py chatbot
215
+ ```
216
+
217
+ ## Deployment
218
+
219
+ The project is deployed on Hugging Face Spaces as a Docker container:
220
+ - **Space**: https://huggingface.co/spaces/mishrabp/chatbot-app
221
+ - **URL**: https://mishrabp-chatbot-app.hf.space
222
+ - **Trigger**: Automatic deployment on push to `main` branch
223
+ - **Configuration**: `.github/workflows/chatbot-app-hf.yml`
src/chatbot_v1/aagents/__init__.py ADDED
File without changes
src/chatbot_v1/aagents/input_validation_agent.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import json
4
+ from agents import Agent, OpenAIChatCompletionsModel, Runner, GuardrailFunctionOutput
5
+ from pydantic import BaseModel
6
+ from openai import AsyncOpenAI
7
+ from core.model import get_model_client
8
+
9
+ class ValidatedOutput(BaseModel):
10
+ is_valid: bool
11
+ reasoning: str
12
+
13
+ input_validation_agent = Agent(
14
+ name="Guardrail Input Validation Agent",
15
+ instructions="""
16
+ You are a highly efficient and specialized **Agent** 🌐. Your sole function is to validate the user inputs.
17
+
18
+ ## Core Directives & Priorities
19
+ 1. You should flag if the user uses unparaliamentary language ONLY.
20
+ 2. You MUST give reasoning for the same.
21
+
22
+ ## Rules
23
+ - If it contains any of these, mark `"is_valid": false` and explain **why** in `"reasoning"`.
24
+ - Otherwise, mark `"is_valid": true` with reasoning like "The input follows respectful communication guidelines."
25
+
26
+
27
+ ## Output Format (MANDATORY)
28
+ * Return a JSON object with the following structure:
29
+ {
30
+ "is_valid": <boolean>,
31
+ "reasoning": <string>
32
+ }
33
+ """,
34
+ model=get_model_client(),
35
+ output_type=ValidatedOutput,
36
+ )
37
+ input_validation_agent.description = "A guardrail agent that validates user input for unparliamentary language."
38
+
39
+ async def input_validation_guardrail(ctx, agent, input_data):
40
+ result = await Runner.run(input_validation_agent, input_data, context=ctx.context)
41
+ raw_output = result.final_output
42
+
43
+ # Handle different return shapes gracefully
44
+ if isinstance(raw_output, ValidatedOutput):
45
+ final_output = raw_output
46
+ print("Parsed ValidatedOutput:", final_output)
47
+ else:
48
+ final_output = ValidatedOutput(
49
+ is_valid=False,
50
+ reasoning=f"Unexpected output type: {type(raw_output)}"
51
+ )
52
+
53
+ return GuardrailFunctionOutput(
54
+ output_info=final_output,
55
+ tripwire_triggered=not final_output.is_valid,
56
+ )
57
+
58
+ __all__ = ["input_validation_agent", "input_validation_guardrail", "ValidatedOutput"]
src/chatbot_v1/aagents/orchestrator_agent.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import asyncio
4
+ from common.aagents.search_agent import search_agent
5
+ from common.aagents.news_agent import news_agent
6
+ from common.aagents.yf_agent import yf_agent
7
+ from aagents.input_validation_agent import input_validation_guardrail
8
+ from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool
9
+ from openai import AsyncOpenAI
10
+ from core.model import get_model_client
11
+
12
+ # ----------------------------------------------------------
13
+ # PARALLEL EXECUTION TOOL
14
+ # ----------------------------------------------------------
15
+ @function_tool
16
+ async def prompt_broadcaster(query: str, include_finance: bool = True, include_news: bool = True, include_search: bool = True) -> str:
17
+ """
18
+ Broadcasts the search query to selected specialized agents in parallel and aggregates their responses.
19
+
20
+ Args:
21
+ query: The user's question or topic to research.
22
+ include_finance: Whether to include the Yahoo Finance agent (default: True).
23
+ include_news: Whether to include the News agent (default: True).
24
+ include_search: Whether to include the Web Search agent (default: True).
25
+
26
+ Returns:
27
+ Combined reports from the selected agents.
28
+ """
29
+
30
+
31
+ # A better approach for variable tasks is to map them.
32
+ active_agents = []
33
+ if include_finance: active_agents.append(("YahooFinanceAgent", Runner.run(yf_agent, query)))
34
+ if include_news: active_agents.append(("NewsAgent", Runner.run(news_agent, query)))
35
+ if include_search: active_agents.append(("WebSearchAgent", Runner.run(search_agent, query)))
36
+
37
+ if not active_agents:
38
+ return "No agents were selected for this query."
39
+
40
+ # Run in parallel
41
+ agent_names = [name for name, _ in active_agents]
42
+ coroutines = [coro for _, coro in active_agents]
43
+
44
+ results = await asyncio.gather(*coroutines, return_exceptions=True)
45
+
46
+ outputs = []
47
+ for name, res in zip(agent_names, results):
48
+ if isinstance(res, Exception):
49
+ outputs.append(f"❌ {name} Error: {str(res)}")
50
+ else:
51
+ outputs.append(f"✅ {name} Report:\n{res.final_output}")
52
+
53
+ combined_response = "\n--- START OF AGENT REPORTS ---\n\n" + "\n\n-----------------------------------\n\n".join(outputs) + "\n\n--- END OF AGENT REPORTS ---"
54
+
55
+ return combined_response
56
+
57
+ orchestrator_agent = Agent(
58
+ name="AI Chat Orchestrator",
59
+ tools=[prompt_broadcaster],
60
+ instructions="""
61
+ You are the **AI Chat Orchestrator**.
62
+ Your goal is to provide a comprehensive, multi-perspective answer by synthesizing data from specialized sub-agents.
63
+
64
+ **Workflow**:
65
+ 1. **Analyze Request**: Understand the user's question.
66
+ 2. **Determine Needs**: Decide which specialized agents are required.
67
+ * **Finance**: For stock prices, market trends, company financials, or analyst ratings.
68
+ * **News**: For recent events, headlines, or breaking news.
69
+ * **Web Search**: For general knowledge, history, facts, or broad research.
70
+ 3. **Broadcast Query**: Call the `prompt_broadcaster` tool with the `query` and set the `include_*` flags to True/False accordingly.
71
+ * *Optimization Tip*: efficiently select ONLY the necessary agents to reduce latency.
72
+ 4. **Synthesize Results**: Read the returned "Agent Reports".
73
+ * Combine the financial data (prices, sentiment), news headlines, and general search context.
74
+ * Compare and contrast findings if necessary.
75
+ * Resolve conflicts by prioritizing specific data (e.g., Yahoo Finance for prices) over general text.
76
+ 5. **Final Response**: Generate a clear, professional, and well-structured summary for the user. Do not simply paste the individual reports.
77
+
78
+ **Final Response Structure**:
79
+ You should adapt the response structure based on the user's query type:
80
+
81
+ * **For Market/Finance Queries**: Use the "Market Analysis" style with a Financial Snapshot (Price, Sentinel, Ratings), Key Developments, and Synthesis.
82
+ * **For News/Research**: Use a clear "Executive Summary" followed by "Key Findings" and "Details".
83
+ * **For General Chat**: Maintain a conversational but professional tone. Use markdown for clarity (bullet points, bold text).
84
+ * **For Coding Requests**: Provide clear code blocks and explanations.
85
+
86
+ **Constraint**:
87
+ * Do NOT try to answer based on your own knowledge if live data is needed/requested.
88
+ * Use `prompt_broadcaster` when the query implies a need for external information.
89
+ * If agents return "No data", explicitly state that in the relevant section.
90
+ """,
91
+ model=get_model_client(),
92
+ )
93
+ orchestrator_agent.description = "An intelligent orchestrator that queries Finance, News, and Search agents in parallel and synthesizes a comprehensive response."
94
+
95
+
96
+ __all__ = ["orchestrator_agent"]
src/chatbot_v1/app.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import uuid
4
+ import asyncio
5
+ # import trace_config
6
+ import logging
7
+ import streamlit as st
8
+ from aagents.orchestrator_agent import orchestrator_agent
9
+ from agents import Runner, trace, SQLiteSession
10
+ from agents.exceptions import InputGuardrailTripwireTriggered
11
+ # from langsmith import traceable
12
+
13
+ from traceloop.sdk import Traceloop
14
+ from opentelemetry.sdk.trace import Span
15
+
16
+ # --- Monkeypatch to fix "Invalid type Omit" errors ---
17
+ # This filters out 'NotGiven'/'Omit' values from OpenAI that crash the OTel exporter
18
+ _original_set_attribute = Span.set_attribute
19
+
20
+ def _safe_set_attribute(self, key, value):
21
+ # Check string representation of type to avoid importing specific internal types
22
+ type_str = str(type(value))
23
+ if "Omit" in type_str or "NotGiven" in type_str:
24
+ return self
25
+ return _original_set_attribute(self, key, value)
26
+
27
+ Span.set_attribute = _safe_set_attribute
28
+ # -----------------------------------------------------
29
+
30
+ Traceloop.init(
31
+ disable_batch=True,
32
+ api_key="tl_1c19b8e8fcfd411fb9fcdb02d381faef"
33
+ )
34
+
35
+ # -----------------------------
36
+ # Configuration & Utils
37
+ # -----------------------------
38
+ st.set_page_config(
39
+ page_title="AI Assistant",
40
+ layout="wide",
41
+ page_icon="🤖"
42
+ )
43
+
44
+ def load_prompts(folder="prompts"):
45
+ prompts = []
46
+ prompt_labels = []
47
+ if os.path.exists(folder):
48
+ for file_path in glob.glob(os.path.join(folder, "*.txt")):
49
+ with open(file_path, "r", encoding="utf-8") as f:
50
+ content = f.read().strip()
51
+ if content:
52
+ prompts.append(content)
53
+
54
+ prompt_labels.append(os.path.basename(file_path).replace("_", " ").replace(".txt", "").title())
55
+ return prompts, prompt_labels
56
+
57
+ prompts, prompt_labels = load_prompts()
58
+
59
+ # -----------------------------
60
+ # Session State
61
+ # -----------------------------
62
+ if "messages" not in st.session_state:
63
+ st.session_state.messages = []
64
+
65
+ if "ai_session_id" not in st.session_state:
66
+ st.session_state.ai_session_id = str(uuid.uuid4())
67
+
68
+ # Persistent SQLite session
69
+ if "ai_session" not in st.session_state:
70
+ st.session_state.ai_session = SQLiteSession(f"conversation_{st.session_state.ai_session_id}.db")
71
+
72
+ session = st.session_state.ai_session
73
+
74
+ # -----------------------------
75
+ # Premium Styling
76
+ # -----------------------------
77
+ st.markdown("""
78
+ <style>
79
+ /* ---------------------------------------------------------------------
80
+ 1. GLOBAL & RESET
81
+ --------------------------------------------------------------------- */
82
+ * {
83
+ box-sizing: border-box;
84
+ }
85
+
86
+ .stApp, [data-testid="stAppViewContainer"] {
87
+ /* Standard Streamlit background */
88
+ background-color: #f8f9fa;
89
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
90
+ }
91
+
92
+ html {
93
+ -webkit-text-size-adjust: 100%; /* Prevent iOS font boosting */
94
+ }
95
+
96
+ /* ---------------------------------------------------------------------
97
+ 2. LAYOUT & HERO BANNER
98
+ --------------------------------------------------------------------- */
99
+
100
+ /* Mobile font optimization */
101
+ @media (max-width: 768px) {
102
+ /* Target all markdown text specifically */
103
+ .stMarkdown p, .stMarkdown li, .stChatMessage p, .message-content, .stDataFrame, .stTable {
104
+ font-size: 16px !important;
105
+ line-height: 1.6 !important;
106
+ color: #1a1a1a !important;
107
+ }
108
+
109
+ h1, h2, h3, h4, h5, h6 {
110
+ color: #1a1a1a !important;
111
+ }
112
+ }
113
+
114
+ /* Desktop Layout */
115
+ @media (min-width: 769px) {
116
+ .block-container {
117
+ padding-top: 0 !important;
118
+ padding-bottom: 2rem !important;
119
+ padding-left: 5rem !important;
120
+ padding-right: 5rem !important;
121
+ max-width: 100% !important;
122
+ }
123
+
124
+ .hero-container {
125
+ margin-top: -3rem;
126
+ margin-left: -5rem;
127
+ margin-right: -5rem;
128
+ /* Simple negative margins to pull edge-to-edge */
129
+ padding: 2.5rem 1rem 2rem 1rem; /* Compact desktop padding */
130
+ }
131
+ }
132
+
133
+ /* Mobile Layout */
134
+ @media (max-width: 768px) {
135
+ .block-container {
136
+ padding-left: 1rem !important;
137
+ padding-right: 1rem !important;
138
+ padding-top: 0 !important;
139
+ }
140
+
141
+ .hero-container {
142
+ margin-top: -2rem;
143
+ margin-left: -1rem;
144
+ margin-right: -1rem;
145
+ /* Break out of the 1rem padding */
146
+ padding: 2rem 1rem 1.5rem 1rem; /* Compact mobile padding */
147
+ border-radius: 0 0 12px 12px;
148
+ }
149
+
150
+ /* Ensure font sizes are standard (Streamlit defaults is ~16px) */
151
+ /* We DO NOT override them to 17px/fixed, allowing system zoom to work. */
152
+ }
153
+
154
+ /* Hero Styling */
155
+ .hero-container {
156
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
157
+ color: white;
158
+ text-align: center;
159
+ border-radius: 0 0 16px 16px;
160
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
161
+ margin-bottom: 2rem;
162
+ }
163
+
164
+ .hero-title {
165
+ font-size: 2rem; /* Slightly smaller */
166
+ font-weight: 700;
167
+ margin-bottom: 0.25rem;
168
+ color: white !important;
169
+ }
170
+ .hero-subtitle {
171
+ font-size: 1rem;
172
+ opacity: 0.95;
173
+ font-weight: 400;
174
+ color: rgba(255,255,255,0.95) !important;
175
+ }
176
+
177
+ /* Remove Header Decoration */
178
+ header[data-testid="stHeader"] {
179
+ background-color: transparent !important;
180
+ height: 0 !important;
181
+ z-index: 100;
182
+ }
183
+ div[data-testid="stDecoration"] { display: none; }
184
+
185
+ /* ---------------------------------------------------------------------
186
+ 3. COMPONENT STYLING (Healthcare-like)
187
+ --------------------------------------------------------------------- */
188
+
189
+ /* Chat Bubbles - Clean & Readable */
190
+ .stChatMessage {
191
+ background-color: white;
192
+ border-radius: 12px;
193
+ border: 1px solid #e5e5e5;
194
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
195
+ padding: 1rem;
196
+ }
197
+
198
+ .stChatMessage[data-testid="stChatMessage"]:nth-of-type(odd) {
199
+ background-color: #f8f9fa;
200
+ }
201
+
202
+ /* Input Fields */
203
+ .stTextInput input {
204
+ border-radius: 20px; /* Matching healthcare-assistant roundness */
205
+ border: 1px solid #ddd;
206
+ padding: 0.75rem 1rem;
207
+ }
208
+
209
+ /* Buttons */
210
+ .stButton button {
211
+ border-radius: 20px; /* Matching healthcare-assistant */
212
+ min-height: 48px;
213
+ font-weight: 500;
214
+ }
215
+
216
+ /* Sidebar */
217
+ section[data-testid="stSidebar"] {
218
+ background-color: #ffffff;
219
+ border-right: 1px solid #eaeaea;
220
+ }
221
+
222
+ /* Minimize Sidebar Top Padding */
223
+ section[data-testid="stSidebar"] .block-container {
224
+ padding-top: 0rem !important;
225
+ padding-bottom: 0rem !important;
226
+ }
227
+ </style>
228
+ """, unsafe_allow_html=True)
229
+
230
+ # -----------------------------
231
+ # Logic
232
+ # -----------------------------
233
+ # @traceable(name="chatbot")
234
+ async def get_ai_response(prompt: str) -> str:
235
+ try:
236
+ agent = orchestrator_agent
237
+ # Ensure session is valid
238
+ current_session = st.session_state.ai_session
239
+ current_session = st.session_state.ai_session
240
+ with trace("Chatbot Agent Run"): # Keep existing custom trace wrapper
241
+ # Run agent
242
+ result = await Runner.run(agent, prompt, session=current_session)
243
+ return result.final_output
244
+ except InputGuardrailTripwireTriggered as e:
245
+ reasoning = getattr(e, "reasoning", None) \
246
+ or getattr(getattr(e, "output", None), "reasoning", None) \
247
+ or getattr(getattr(e, "guardrail_output", None), "reasoning", None) \
248
+ or "Guardrail triggered, but no reasoning provided."
249
+ return f"⚠️ **Guardrail Blocked Input**\n\n{reasoning}"
250
+ except Exception as e:
251
+ return f"❌ **Error**: {str(e)}"
252
+
253
+ # -----------------------------
254
+ # Sidebar - Quick Actions
255
+ # -----------------------------
256
+ with st.sidebar:
257
+ st.markdown("### ⚡ Quick Starters")
258
+ st.markdown("Select a prompt to start:")
259
+
260
+ # We use a trick with st.button to act as input triggers
261
+ # If a button is clicked, we'll handle it in the main loop logic
262
+ selected_prompt = None
263
+ for idx, prompt_text in enumerate(prompts):
264
+ label = prompt_labels[idx] if idx < len(prompt_labels) else f"Prompt {idx+1}"
265
+ if st.button(label, key=f"sidebar_btn_{idx}", use_container_width=True):
266
+ # Reset conversation
267
+ st.session_state.messages = []
268
+ st.session_state.ai_session_id = str(uuid.uuid4())
269
+ # Recreate session object with new ID
270
+ st.session_state.ai_session = SQLiteSession(f"conversation_{st.session_state.ai_session_id}.db")
271
+ selected_prompt = prompt_text
272
+
273
+ st.markdown("---")
274
+ if st.button("🗑️ Clear Conversation", use_container_width=True):
275
+ st.session_state.messages = []
276
+ st.rerun()
277
+
278
+ # -----------------------------
279
+ # Main Content
280
+ # -----------------------------
281
+
282
+ # Hero Banner (Always visible & Sticky)
283
+ st.markdown("""
284
+ <div class="hero-container" role="banner">
285
+ <div class="hero-title">🤖 AI Companion</div>
286
+ <div class="hero-subtitle">Your intelligent partner for research, analysis, and more.</div>
287
+ </div>
288
+ """, unsafe_allow_html=True)
289
+
290
+ # Display Chat History
291
+ for message in st.session_state.messages:
292
+ with st.chat_message(message["role"]):
293
+ st.markdown(message["content"], unsafe_allow_html=True)
294
+
295
+ # Chat Input Handling
296
+ # We handle both the chat input widget and the sidebar selection here
297
+ if prompt := (st.chat_input("Type your message...") or selected_prompt):
298
+ # User Message
299
+ st.session_state.messages.append({"role": "user", "content": prompt})
300
+ with st.chat_message("user"):
301
+ st.markdown(prompt)
302
+
303
+ # Assistant Response
304
+ with st.chat_message("assistant"):
305
+ with st.spinner("Thinking..."):
306
+ response_text = asyncio.run(get_ai_response(prompt))
307
+ st.markdown(response_text, unsafe_allow_html=True)
308
+
309
+
310
+
311
+ st.session_state.messages.append({"role": "assistant", "content": response_text})
312
+
313
+ # If it was a sidebar click, we need to rerun to clear the selection state potentially,
314
+ # but st.chat_input usually handles focus. With buttons, a rerun happens automatically
315
+ # but we want to make sure the input box is cleared (which 'selected_prompt' doesn't use).
316
+ if selected_prompt:
317
+ st.rerun()
src/chatbot_v1/core/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+
2
+ from .model import get_model_client
3
+
4
+ __all__ = ["get_model_client"]
src/chatbot_v1/core/model.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from common.utility.openai_model_factory import OpenAIModelFactory
2
+
3
+ def get_model_client(provider:str = "openai"):
4
+ if provider.lower() == "google":
5
+ return OpenAIModelFactory.get_model(
6
+ provider="google",
7
+ model_name="gemini-2.5-flash",
8
+ temperature=0
9
+ )
10
+ elif provider.lower() == "openai":
11
+ return OpenAIModelFactory.get_model(
12
+ provider="openai",
13
+ model_name="gpt-4o-mini",
14
+ temperature=0
15
+ )
16
+ elif provider.lower() == "azure":
17
+ return OpenAIModelFactory.get_model(
18
+ provider="azure",
19
+ model_name="gpt-4o-mini",
20
+ temperature=0
21
+ )
22
+ elif provider.lower() == "groq":
23
+ return OpenAIModelFactory.get_model(
24
+ provider="groq",
25
+ model_name="gpt-4o-mini",
26
+ temperature=0
27
+ )
28
+ elif provider.lower() == "ollama":
29
+ return OpenAIModelFactory.get_model(
30
+ provider="ollama",
31
+ model_name="gpt-4o-mini",
32
+ temperature=0
33
+ )
34
+ else:
35
+ raise ValueError(f"Unsupported provider: {provider}")
36
+
src/chatbot_v1/prompts/economic_news.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Provide a concise update on **major economic indicators released recently** in USA.
3
+
4
+ ###### Include
5
+ - **Interest Rates**: Latest central bank decisions, current policy rates, and forward guidance.
6
+ - **Labor Market**: Unemployment rate, job creation figures, and key labor metrics (if available).
7
+ - **Inflation**: CPI, PCE, or other inflation data with MoM and YoY changes.
8
+ - **Growth Indicators**: GDP, PMIs, or industrial production released recently.
9
+ - **Market Reaction**: Brief impact on equities, bonds, FX, and commodities.
10
+
11
+ ###### Guidelines
12
+ - Compare results against forecasts and prior releases
13
+ - Highlight notable surprises and their implications
14
+ - Keep the summary brief, factual, and structured
15
+ - **Always retrieve numerical data from primary or authoritative sources**
16
+
17
+ ###### Fallback
18
+ - If no relevant data was released recently, explicitly state **“No major economic indicators were released during this period.”**
19
+ - If data is partially unavailable, summarize what is available and clearly note missing indicators.
20
+ - Do not infer or fabricate numbers under any circumstance.
21
+
22
+ ###### Output Style
23
+ - Concise, factual, and well-structured
24
+ - Use clear bullet points or short paragraphs
25
+ - Avoid speculation unless explicitly labeled as interpretation
26
+ - **Cite data sources clearly**
27
+ - Use color and emoji to make it more engaging.
src/chatbot_v1/prompts/entertainment_updates.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Provide the **top 5 recent movie or series updates** that are trending or newly released in USA.
3
+
4
+ ###### For each title, include:
5
+ - **Title in bold** and optionally use **color or emojis** to make it fun (e.g., 🎬, 🍿, 🌟)
6
+ - A **short, 2–3 line snippet** that generates excitement or humor about the plot, cast, or vibe
7
+ - **Platform or source** where it can be watched (Netflix, Prime, Disney+, etc.)
8
+ - **Release date or premiere date**
9
+
10
+ ###### Requirements / Guidelines
11
+ - Focus on **recent releases** (last 2–4 weeks) or currently trending content
12
+ - Keep the tone **fun, witty, and engaging**, like a friend recommending a show
13
+ - Use **emojis liberally** to emphasize excitement, genre, or humor
14
+ - Call out the **main actors and actresses** to build the interest
15
+ - Where possible, add a **light humorous quip or pun** about the movie/series
16
+ - If color is supported, use HTML span tags, e.g., `<span style="color:orange">Title</span>` for emphasis
17
+
18
+ ###### Fallback
19
+ - If fewer than 5 titles are available, provide what is available and indicate:
20
+ **“Only X recent releases found.”**
21
+ - Do not fabricate platforms or release dates — only use verified sources
22
+
23
+ ###### Output Style
24
+ - List format (1–5) sorted by **popularity or release date**
25
+ - **Title + snippet + watch source + release date** per entry
26
+ - Use **color, emojis, and humor** to make the output visually appealing and fun to read
src/chatbot_v1/prompts/india_news.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Tell me the **top 3 headlines from India**.
3
+
4
+ ###### For each headline, provide:
5
+ - **Title in bold**
6
+ - A **3‑line summary**
7
+ - **Publish date and time**
8
+ - A **link to the exact source URL**
9
+
10
+ ###### Requirements
11
+ - Use authoritative news sources (e.g., major national/regional news outlets)
12
+ - Headlines should be **recent (last 24 hours)**
13
+ - Provide timestamps in **UTC**
14
+ - If publish date/time is not available, indicate “Date/Time not provided”
15
+
16
+ ###### Fallback
17
+ - If fewer than 3 headlines are found, provide what is available and state:
18
+ **“Only X recent headlines found for India.”**
19
+ - Do not fabricate headlines, dates, or URLs
20
+
21
+ ###### Output Style
22
+ - Structured list sorted by **most recent first**
23
+ - Clear and concise formatting as requested
24
+ - Use color and emoji to make it more engaging.
25
+ - Use `<span style="color:...">` for coloring the title if the renderer supports it
26
+ - Keep the output **concise, factual, and visually engaging**
src/chatbot_v1/prompts/market_sentiment.txt ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Act as a **Senior Market Analyst** and provide a concise market sentiment update for the **US Stock Market / S&P 500**.
3
+
4
+ ###### Steps
5
+ 1. **Data Gathering**: Search for the **top 5 financial news headlines** from the last 24 hours related to the [US Stock Market / S&P 500].
6
+ 2. **Market Check**: Retrieve the **current value** and **today’s percentage change** for:
7
+ - **S&P 500 (SPX)**
8
+ - **VIX (Volatility Index)**
9
+ 3. **Synthesis**: Based on the **tone of the news headlines** and the **index performance**, determine whether the **current market sentiment** is:
10
+ - **Bullish**
11
+ - **Bearish**
12
+ - **Neutral**
13
+ 4. **Output**: Provide a:
14
+ - **Sentiment Score (1–10)**
15
+ - **Top 3 key drivers** influencing this sentiment
16
+
17
+ ###### Guidelines
18
+ - Prioritize **reliable financial news sources** (e.g., Bloomberg, Reuters, WSJ, CNBC)
19
+ - Use **accurate, real-time market data** for indices
20
+ - Base sentiment on both **news tone** and **market movement**
21
+ - Avoid subjective or unsupported judgments
22
+
23
+ ###### Fallback
24
+ - If no relevant financial headlines are found in the last 24 hours, clearly state:
25
+ **“No significant market news available in the last 24 hours.”**
26
+ - If either index value or change is unavailable, report available data and note missing values explicitly
27
+ - Do not invent or estimate values — only use verified data
28
+
29
+ ###### Output Style
30
+ - Concise, factual, and structured
31
+ - Use clear bullet points or short paragraphs
32
+ - Include numerical values and data timestamps
33
+ - Provide **sources for headlines and index data**
34
+ - Use color and emoji to make it more engaging.
src/chatbot_v1/prompts/news_headlines.txt ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Tell me the **top 3 USA headlines**. Use **emojis** and, where supported, **HTML color tags** to make the output engaging.
3
+
4
+ ###### For each headline, provide:
5
+ - **Title in bold** and optionally in color, e.g., `<span style="color:blue">Title</span>`
6
+ - A **3-line summary** with an emoji indicating the type of news:
7
+ - 📰 Politics
8
+ - 💼 Business
9
+ - 🌎 World
10
+ - ⚡ Breaking news
11
+ - **Publish date and time** (UTC)
12
+ - A **link to the exact source URL**
13
+
14
+ ###### Requirements
15
+ - Use **credible news sources** (Reuters, AP, BBC, Guardian, etc.)
16
+ - Headlines should be **recent (last 24 hours)**
17
+ - If publish time is unavailable, indicate **“Time not provided”**
18
+
19
+ ###### Fallback
20
+ - If fewer than 3 headlines are found, state:
21
+ **“Only X recent headlines found for the USA.”**
22
+ - Do not fabricate headlines, dates, or URLs
23
+
24
+ ###### Output Style
25
+ - Structured list sorted by **most recent first**
26
+ - Use emojis consistently to indicate news type
27
+ - Use `<span style="color:...">` for coloring the title if the renderer supports it
28
+ - Keep the output **concise, factual, and visually engaging**
src/chatbot_v1/prompts/odia_news.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Tell me the **top 3 headlines from Odisha**.
3
+
4
+ ###### For each headline, provide:
5
+ - **Title in bold**
6
+ - A **3‑line summary**
7
+ - **Publish date and time**
8
+ - A **link to the exact source URL**
9
+
10
+ ###### Requirements
11
+ - Use authoritative news sources (e.g., major national/regional news outlets)
12
+ - Headlines should be **recent (last 24 hours)**
13
+ - Provide timestamps in **UTC**
14
+ - If publish date/time is not available, indicate “Date/Time not provided”
15
+
16
+ ###### Fallback
17
+ - If fewer than 3 headlines are found, provide what is available and state:
18
+ **“Only X recent headlines found for Odisha.”**
19
+ - Do not fabricate headlines, dates, or URLs
20
+
21
+ ###### Output Style
22
+ - Structured list sorted by **most recent first**
23
+ - Clear and concise formatting as requested
24
+ - Use color and emoji to make it more engaging.
25
+ - Use `<span style="color:...">` for coloring the title if the renderer supports it
26
+ - Keep the output **concise, factual, and visually engaging**
src/chatbot_v1/prompts/trade_recommendation.txt ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Recommend **three option spreads** with **>80% probability of profit**. Perform a thorough analysis of each underlying’s **3-month price trend** and current **market sentiment** before selecting spreads.
3
+
4
+ ###### Steps
5
+ 1. **Stock selection & analysis**
6
+ - Analyze the **last 3 months** of price action (trend, volatility, support/resistance).
7
+ - Assess market sentiment from the **last 7 days** of headlines and social/analyst tone.
8
+ 2. **Spread construction**
9
+ - For each of the **3 recommended spreads**, specify:
10
+ - **Underlying ticker**
11
+ - **Spread type** (e.g., bull put, bear call, iron condor)
12
+ - **Exact expiry date** (YYYY-MM-DD)
13
+ - **Each leg**: side (sell/buy), option type (put/call), **strike price**
14
+ - **Premium entry**: exact net credit/debit per share (use live bid/ask midpoint)
15
+ - **Position size guidance** (risk per trade as % of portfolio) — optional
16
+ 3. **Probability & rationale**
17
+ - Provide a **quantitative probability of profit (%)** (clearly state model/method used).
18
+ - Give a concise **rationale** linking 3-month trend, implied volatility, and sentiment to the spread choice.
19
+ - Show key supporting numbers: current spot, IV30, recent volatility, and relevant news headlines (with timestamps).
20
+
21
+ ###### Requirements / Guidelines
22
+ - Target **>80% probability of profit** for each spread. Explain how the probability was computed (IV-based log-normal, normal approximation, or risk-neutral model).
23
+ - **Always** use live option-chain quotes (bid/ask midpoint) and authoritative sources for prices/IV (e.g., exchange data, major market data providers).
24
+ - Compare outcomes **vs. forecasts / recent range** and note any idiosyncratic risk (earnings, events).
25
+ - Include **exact timestamps** (UTC) for all quoted prices.
26
+ - Provide **sources** for price, IV, and headlines.
27
+
28
+ ###### Fallback
29
+ - If live option-chain or price data is unavailable, state: **“Live market data unavailable — cannot generate exact strike/premium. Provide analysis based on most recent available snapshot.”**
30
+ - If sentiment or 3-month history is incomplete, present what is available and **explicitly list missing items**.
31
+ - **Do not fabricate** strikes, premiums, probabilities, or news — only use verified data.
32
+
33
+ ###### Output Style
34
+ - For each spread, use a compact block with:
35
+ - Ticker — Spread type — Expiry (YYYY-MM-DD) — Net premium — PO P (%)
36
+ - Legs: bullet list of exact leg details (sell/buy, put/call, strike, premium)
37
+ - Rationale: 2–3 short sentences linking trend & sentiment to the trade
38
+ - Sources & timestamps
39
+ - Keep language concise, factual, and machine/agent friendly for downstream parsing.
40
+ - Use color and emoji to make it more engaging.
src/chatbot_v1/prompts/upcoming_earnings.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ##### Task
2
+ Search for **upcoming critical earnings announcements** in the stock market.
3
+
4
+ ###### Include
5
+ - **Ticker**
6
+ - **Company name**
7
+ - **Earnings date & time**
8
+ - **Expected EPS & revenue consensus**
9
+ - **Last quarter’s actual EPS & revenue**
10
+ - **Implied volatility trend ahead of earnings**
11
+
12
+ ###### Requirements / Guidelines
13
+ - Focus on **high‑impact names** (large cap, high volume, sector leaders)
14
+ - Include **earnings expected within the next 7 calendar days**
15
+ - Use **primary/authoritative sources** for earnings dates and estimates (e.g., exchange calendars, Bloomberg/Refinitiv/Estimize)
16
+ - Show **timestamped data** (UTC)
17
+
18
+ ###### Fallback
19
+ - If no critical earnings are found in the next 7 days, state:
20
+ **“No upcoming critical earnings announcements found within the specified period.”**
21
+ - If consensus estimates are unavailable, list the earnings date/time and note missing metrics.
22
+
23
+ ###### Output Style
24
+ - Structured list sorted by **earnings date**
25
+ - Use clear bullet points or short paragraphs
26
+ - Provide **sources** for each item
27
+ - Use color and emoji to make it more engaging.
src/chatbot_v1/trace_config.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # trace_config.py
2
+ import openai
3
+ from langsmith.wrappers import wrap_openai
4
+ import os
5
+
6
+ print("🔌 APPLYING LANGSMITH TRACE PATCH...")
7
+
8
+ # 1. Save original classes
9
+ _OriginalOpenAI = openai.OpenAI
10
+ _OriginalAsyncOpenAI = openai.AsyncOpenAI
11
+
12
+ # 2. Define the shim
13
+ def PatchedOpenAI(*args, **kwargs):
14
+ print("✨ Creating Wrapped OpenAI Client (Sync)") # Debug print
15
+ client = _OriginalOpenAI(*args, **kwargs)
16
+ return wrap_openai(client)
17
+
18
+ def PatchedAsyncOpenAI(*args, **kwargs):
19
+ print("✨ Creating Wrapped OpenAI Client (Async)") # Debug print
20
+ client = _OriginalAsyncOpenAI(*args, **kwargs)
21
+ return wrap_openai(client)
22
+
23
+ # 3. Apply patch
24
+ openai.OpenAI = PatchedOpenAI
25
+ openai.AsyncOpenAI = PatchedAsyncOpenAI
26
+
27
+ from langsmith import traceable
28
+
29
+ # You can't decorate the class directly with @traceable,
30
+ # but you can use this helper to wrap all methods:
31
+
32
+ def instrument_class(cls):
33
+ for attr_name, attr_value in cls.__dict__.items():
34
+ if callable(attr_value) and not attr_name.startswith("__"):
35
+ setattr(cls, attr_name, traceable(attr_value, run_type="tool"))
36
+ return cls
37
+
src/chatbot_v2/Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1 \
4
+ DEBIAN_FRONTEND=noninteractive \
5
+ PYTHONPATH=/app:/app/common:$PYTHONPATH
6
+
7
+ WORKDIR /app
8
+
9
+ # System deps
10
+ RUN apt-get update && apt-get install -y \
11
+ git build-essential curl \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ # Install uv
15
+ RUN curl -LsSf https://astral.sh/uv/install.sh | sh
16
+ ENV PATH="/root/.local/bin:$PATH"
17
+
18
+ # Copy project metadata
19
+ COPY pyproject.toml .
20
+ COPY uv.lock .
21
+
22
+ # Copy required folders
23
+ COPY common/ ./common/
24
+ COPY src/chatbot_v2/ ./src/chatbot_v2/
25
+
26
+ # Install dependencies using uv, then export and install with pip to system
27
+ RUN uv sync --frozen --no-dev && \
28
+ uv pip install -e . --system
29
+
30
+ # Copy entry point
31
+ COPY run.py .
32
+
33
+ EXPOSE 7860
34
+
35
+ CMD ["python", "run.py", "chatbot_v2", "--port", "7860"]
src/chatbot_v2/README.md ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Chatbot
3
+ emoji: 🤖
4
+ colorFrom: pink
5
+ colorTo: yellow
6
+ sdk: docker
7
+ sdk_version: "0.0.1"
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ tags:
12
+ - text-generation
13
+ - agentic-ai
14
+ - openai-sdk
15
+ short_description: An Experimental Agentic Chatbot (uses OpenAI Agent SDK)
16
+ ---
17
+
18
+ # AI Chatbot
19
+
20
+ This is an experimental chatbot for chatting with AI. It is equipped with agents & tools to give you realtime data from the web. It uses **OpenAI - SDK** and **OpenAI - Agents**...
21
+
22
+ ## Features
23
+ - Predefined prompts for quick analysis
24
+ - Chat interface with AI responses
25
+ - Enter key support and responsive design
26
+ - Latest messages appear on top
27
+
28
+ ## Usage
29
+ 1. Type a message or select a predefined prompt
30
+ 2. Press **Enter** or click **Send**
31
+ 3. AI responses appear instantly in the chat interface
32
+
33
+ ## Supported APIs
34
+ - OpenAI
35
+ - Google
36
+ - GROQ
37
+ - SERPER
38
+ - News API
39
+
40
+ ## Notes
41
+ - Make sure your API keys are configured in the Space secrets
42
+ - Built using Streamlit and deployed as a Docker Space
43
+
44
+ ## References
45
+
46
+ https://openai.github.io/openai-agents-python/
47
+
48
+ https://github.com/openai/openai-agents-python/tree/main/examples/mcp
49
+
50
+ ## Project Folder Structure
51
+
52
+ ```
53
+ chatbot/
54
+ ├── app.py # Main Streamlit chatbot interface
55
+ ├── appagents/
56
+ │ ├── __init__.py # Package initialization
57
+ │ ├── OrchestratorAgent.py # Main orchestrator - coordinates all agents
58
+ │ ├── FinancialAgent.py # Financial data and analysis agent
59
+ │ ├── NewsAgent.py # News retrieval and summarization agent
60
+ │ ├── SearchAgent.py # General web search agent
61
+ │ └── InputValidationAgent.py # Input validation and sanitization agent
62
+ ├── core/
63
+ │ ├── __init__.py # Package initialization
64
+ │ └── logger.py # Centralized logging configuration
65
+ ├── tools/
66
+ │ ├── __init__.py # Package initialization
67
+ │ ├── google_tools.py # Google search API wrapper
68
+ │ ├── yahoo_tools.py # Yahoo Finance API wrapper
69
+ │ ├── news_tools.py # News API wrapper
70
+ │ └── time_tools.py # Time-related utility functions
71
+ ├── prompts/
72
+ │ ├── economic_news.txt # Prompt for economic news analysis
73
+ │ ├── market_sentiment.txt # Prompt for market sentiment analysis
74
+ │ ├── news_headlines.txt # Prompt for news headline summarization
75
+ │ ├── trade_recommendation.txt # Prompt for trade recommendations
76
+ │ └── upcoming_earnings.txt # Prompt for upcoming earnings alerts
77
+ ├── Dockerfile # Docker configuration for container deployment
78
+ ├── pyproject.toml # Project metadata and dependencies (copied from root)
79
+ ├── uv.lock # Locked dependency versions (copied from root)
80
+ ├── README.md # Project documentation
81
+ └── run.py # Script to run the application locally
82
+ ```
83
+
84
+ ## File Descriptions
85
+
86
+ ### UI Layer
87
+ - **app.py** - Main Streamlit chatbot interface that provides:
88
+ - Chat message display with user and AI messages
89
+ - Text input for user queries
90
+ - Predefined prompt buttons for quick analysis
91
+ - Real-time AI responses
92
+ - Support for Enter key submission
93
+ - Responsive design with latest messages appearing first
94
+
95
+ ### Agents (`appagents/`)
96
+ - **OrchestratorAgent.py** - Main orchestrator that:
97
+ - Coordinates communication between all specialized agents
98
+ - Routes user queries to appropriate agents
99
+ - Manages conversation flow and context
100
+ - Integrates tool responses
101
+
102
+ - **FinancialAgent.py** - Financial data and analysis:
103
+ - Retrieves stock prices and financial metrics
104
+ - Performs financial analysis using Yahoo Finance API
105
+ - Provides market insights and recommendations
106
+ - Integrates with yahoo_tools for data fetching
107
+
108
+ - **NewsAgent.py** - News retrieval and summarization:
109
+ - Fetches latest news articles
110
+ - Summarizes news content
111
+ - Integrates with News API for real-time updates
112
+ - Provides news-based market insights
113
+
114
+ - **SearchAgent.py** - General web search:
115
+ - Performs web searches for general queries
116
+ - Integrates with Google Search / Serper API
117
+ - Returns relevant search results
118
+ - Supports multi-source data gathering
119
+
120
+ - **InputValidationAgent.py** - Input validation:
121
+ - Sanitizes user input
122
+ - Validates query format and content
123
+ - Prevents malicious inputs
124
+ - Ensures appropriate content
125
+
126
+ ### Core Utilities (`core/`)
127
+ - **logger.py** - Centralized logging configuration:
128
+ - Provides consistent logging across agents
129
+ - Handles different log levels
130
+ - Formats log messages for clarity
131
+
132
+ ### Tools (`tools/`)
133
+ - **google_tools.py** - Google Search API wrapper:
134
+ - Executes web searches via Google Search / Serper API
135
+ - Parses and returns search results
136
+ - Handles API authentication
137
+
138
+ - **yahoo_tools.py** - Yahoo Finance API integration:
139
+ - Retrieves stock price data
140
+ - Fetches financial metrics and indicators
141
+ - Provides historical price data
142
+ - Returns earnings information
143
+
144
+ - **news_tools.py** - News API integration:
145
+ - Fetches latest news articles
146
+ - Filters news by category and keywords
147
+ - Returns news headlines and summaries
148
+ - Provides market-related news feeds
149
+
150
+ - **time_tools.py** - Time utility functions:
151
+ - Provides current time information
152
+ - Formats timestamps
153
+ - Handles timezone conversions
154
+
155
+ ### Prompts (`prompts/`)
156
+ Predefined prompts for specialized analysis:
157
+ - **economic_news.txt** - Analyzes economic news and implications
158
+ - **market_sentiment.txt** - Analyzes market sentiment trends
159
+ - **news_headlines.txt** - Summarizes and explains news headlines
160
+ - **trade_recommendation.txt** - Provides trading recommendations
161
+ - **upcoming_earnings.txt** - Alerts about upcoming earnings reports
162
+
163
+ ### Configuration Files
164
+ - **Dockerfile** - Container deployment:
165
+ - Builds Docker image with Python 3.12
166
+ - Installs dependencies using `uv`
167
+ - Sets up Streamlit server on port 8501
168
+ - Configures PYTHONPATH for module imports
169
+
170
+ - **pyproject.toml** - Project metadata:
171
+ - Package name: "agents"
172
+ - Python version requirement: 3.12
173
+ - Lists all dependencies (OpenAI, LangChain, Streamlit, etc.)
174
+
175
+ - **uv.lock** - Dependency lock file:
176
+ - Ensures reproducible builds
177
+ - Pins exact versions of all dependencies
178
+
179
+ ## Key Technologies
180
+
181
+ | Component | Technology | Purpose |
182
+ |-----------|-----------|---------|
183
+ | LLM Framework | OpenAI Agents | Multi-agent orchestration |
184
+ | Chat Interface | Streamlit | User interaction and display |
185
+ | Web Search | Google Search / Serper API | Web search results |
186
+ | Financial Data | Yahoo Finance API | Stock prices and metrics |
187
+ | News Data | News API | Latest news articles |
188
+ | Async Operations | AsyncIO | Parallel agent execution |
189
+ | Dependencies | UV | Fast Python package management |
190
+ | Containerization | Docker | Cloud deployment |
191
+
192
+ ## Predefined Prompts
193
+
194
+ The chatbot includes quick-access buttons for common analysis:
195
+
196
+ 1. **Economic News** - Analyzes current economic trends and news
197
+ 2. **Market Sentiment** - Provides market sentiment analysis
198
+ 3. **News Headlines** - Summarizes latest news headlines
199
+ 4. **Trade Recommendation** - Suggests trading strategies
200
+ 5. **Upcoming Earnings** - Lists upcoming company earnings
201
+
202
+ ## Running Locally
203
+
204
+ ```bash
205
+ # Install dependencies
206
+ uv sync
207
+
208
+ # Set environment variables defined in .env.name file
209
+ export OPENAI_API_KEY="your-key"
210
+ export SERPER_API_KEY="your-key"
211
+ export NEWS_API_KEY="your-key"
212
+
213
+ # Run the Streamlit app (from the root)
214
+ python run.py chatbot
215
+ ```
216
+
217
+ ## Deployment
218
+
219
+ The project is deployed on Hugging Face Spaces as a Docker container:
220
+ - **Space**: https://huggingface.co/spaces/mishrabp/chatbot-app
221
+ - **URL**: https://mishrabp-chatbot-app.hf.space
222
+ - **Trigger**: Automatic deployment on push to `main` branch
223
+ - **Configuration**: `.github/workflows/chatbot-app-hf.yml`
src/chatbot_v2/app.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import uuid
4
+ import asyncio
5
+ import logging
6
+ import streamlit as st
7
+
8
+ # Import the new Orchestrator Pattern
9
+ from src.chatbot_v2.patterns.orchestrator import ChatbotOrchestrator
10
+
11
+ # -----------------------------
12
+ # Configuration & Utils
13
+ # -----------------------------
14
+ st.set_page_config(
15
+ page_title="Layered AI Assistant",
16
+ layout="wide",
17
+ page_icon="🧠"
18
+ )
19
+
20
+ def load_prompts(folder="prompts"):
21
+ prompts = []
22
+ prompt_labels = []
23
+ if os.path.exists(folder):
24
+ for file_path in glob.glob(os.path.join(folder, "*.txt")):
25
+ with open(file_path, "r", encoding="utf-8") as f:
26
+ content = f.read().strip()
27
+ if content:
28
+ prompts.append(content)
29
+
30
+ prompt_labels.append(os.path.basename(file_path).replace("_", " ").replace(".txt", "").title())
31
+ return prompts, prompt_labels
32
+
33
+ prompts, prompt_labels = load_prompts()
34
+
35
+ # -----------------------------
36
+ # Session State
37
+ # -----------------------------
38
+ if "messages" not in st.session_state:
39
+ st.session_state.messages = []
40
+
41
+ # Initialize the Agent
42
+ if "agent" not in st.session_state:
43
+ st.session_state.agent = ChatbotOrchestrator()
44
+
45
+ # -----------------------------
46
+ # Premium Styling
47
+ # -----------------------------
48
+ st.markdown("""
49
+ <style>
50
+ /* ---------------------------------------------------------------------
51
+ 1. GLOBAL & RESET
52
+ --------------------------------------------------------------------- */
53
+ * {
54
+ box-sizing: border-box;
55
+ }
56
+
57
+ .stApp, [data-testid="stAppViewContainer"] {
58
+ /* Standard Streamlit background */
59
+ background-color: #f8f9fa;
60
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji';
61
+ }
62
+
63
+ html {
64
+ -webkit-text-size-adjust: 100%; /* Prevent iOS font boosting */
65
+ }
66
+
67
+ /* ---------------------------------------------------------------------
68
+ 2. LAYOUT & HERO BANNER
69
+ --------------------------------------------------------------------- */
70
+
71
+ /* Mobile font optimization */
72
+ @media (max-width: 768px) {
73
+ /* Target all markdown text specifically */
74
+ .stMarkdown p, .stMarkdown li, .stChatMessage p, .message-content, .stDataFrame, .stTable {
75
+ font-size: 16px !important;
76
+ line-height: 1.6 !important;
77
+ color: #1a1a1a !important;
78
+ }
79
+
80
+ h1, h2, h3, h4, h5, h6 {
81
+ color: #1a1a1a !important;
82
+ }
83
+ }
84
+
85
+ /* Desktop Layout */
86
+ @media (min-width: 769px) {
87
+ .block-container {
88
+ padding-top: 0 !important;
89
+ padding-bottom: 2rem !important;
90
+ padding-left: 5rem !important;
91
+ padding-right: 5rem !important;
92
+ max-width: 100% !important;
93
+ }
94
+
95
+ .hero-container {
96
+ margin-top: -3rem;
97
+ margin-left: -5rem;
98
+ margin-right: -5rem;
99
+ /* Simple negative margins to pull edge-to-edge */
100
+ padding: 2.5rem 1rem 2rem 1rem; /* Compact desktop padding */
101
+ }
102
+ }
103
+
104
+ /* Mobile Layout */
105
+ @media (max-width: 768px) {
106
+ .block-container {
107
+ padding-left: 1rem !important;
108
+ padding-right: 1rem !important;
109
+ padding-top: 0 !important;
110
+ padding-bottom: 0rem !important;
111
+ }
112
+
113
+ .hero-container {
114
+ margin-top: -2rem;
115
+ margin-left: -1rem;
116
+ margin-right: -1rem;
117
+ /* Break out of the 1rem padding */
118
+ padding: 2rem 1rem 1.5rem 1rem; /* Compact mobile padding */
119
+ border-radius: 0 0 12px 12px;
120
+ }
121
+ }
122
+
123
+ /* Hero Styling */
124
+ .hero-container {
125
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
126
+ color: white;
127
+ text-align: center;
128
+ border-radius: 0 0 16px 16px;
129
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
130
+ margin-bottom: 2rem;
131
+ }
132
+
133
+ .hero-title {
134
+ font-size: 2rem; /* Slightly smaller */
135
+ font-weight: 700;
136
+ margin-bottom: 0.25rem;
137
+ color: white !important;
138
+ }
139
+ .hero-subtitle {
140
+ font-size: 1rem;
141
+ opacity: 0.95;
142
+ font-weight: 400;
143
+ color: rgba(255,255,255,0.95) !important;
144
+ }
145
+
146
+ /* Remove Header Decoration */
147
+ header[data-testid="stHeader"] {
148
+ background-color: transparent !important;
149
+ height: 0 !important;
150
+ z-index: 100;
151
+ }
152
+ div[data-testid="stDecoration"] { display: none; }
153
+
154
+ /* ---------------------------------------------------------------------
155
+ 3. COMPONENT STYLING (Healthcare-like)
156
+ --------------------------------------------------------------------- */
157
+
158
+ /* Chat Bubbles - Clean & Readable */
159
+ .stChatMessage {
160
+ background-color: white;
161
+ border-radius: 12px;
162
+ border: 1px solid #e5e5e5;
163
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
164
+ padding: 1rem;
165
+ }
166
+
167
+ .stChatMessage[data-testid="stChatMessage"]:nth-of-type(odd) {
168
+ background-color: #f8f9fa;
169
+ }
170
+
171
+ /* Input Fields */
172
+ .stTextInput input {
173
+ border-radius: 20px; /* Matching healthcare-assistant roundness */
174
+ border: 1px solid #ddd;
175
+ padding: 0.75rem 1rem;
176
+ }
177
+
178
+ /* Buttons */
179
+ .stButton button {
180
+ border-radius: 20px; /* Matching healthcare-assistant */
181
+ min-height: 48px;
182
+ font-weight: 500;
183
+ }
184
+
185
+ /* Sidebar */
186
+ section[data-testid="stSidebar"] {
187
+ background-color: #ffffff;
188
+ border-right: 1px solid #eaeaea;
189
+ }
190
+
191
+ /* Minimize Sidebar Top Padding */
192
+ section[data-testid="stSidebar"] .block-container {
193
+ padding-top: 0rem !important;
194
+ padding-bottom: 0rem !important;
195
+ }
196
+ </style>
197
+ """, unsafe_allow_html=True)
198
+
199
+ # -----------------------------
200
+ # Logic
201
+ # -----------------------------
202
+ async def get_ai_response(prompt: str) -> str:
203
+ try:
204
+ agent: ChatbotOrchestrator = st.session_state.agent
205
+
206
+ # We pass the *previous* history (messages excluding the latest one which we just appended)
207
+ # Actually, st.session_state.messages ALREADY has the new user message appended below.
208
+ # So we pass messages[:-1] as "history"
209
+ history = st.session_state.messages[:-1]
210
+
211
+ result = await agent.run(user_input=prompt, external_history=history)
212
+ return result
213
+ except Exception as e:
214
+ return f"❌ **Error**: {str(e)}"
215
+
216
+ # -----------------------------
217
+ # Sidebar - Quick Actions
218
+ # -----------------------------
219
+ with st.sidebar:
220
+ st.markdown("### ⚡ Quick Starters")
221
+ st.markdown("Select a prompt to start:")
222
+
223
+ # We use a trick with st.button to act as input triggers
224
+ # If a button is clicked, we'll handle it in the main loop logic
225
+ selected_prompt = None
226
+ for idx, prompt_text in enumerate(prompts):
227
+ label = prompt_labels[idx] if idx < len(prompt_labels) else f"Prompt {idx+1}"
228
+ if st.button(label, key=f"sidebar_btn_{idx}", use_container_width=True):
229
+ # Reset conversation
230
+ st.session_state.messages = []
231
+ st.session_state.agent = ChatbotOrchestrator() # Reset agent memory too
232
+ selected_prompt = prompt_text
233
+
234
+ st.markdown("---")
235
+ if st.button("🗑️ Clear Conversation", use_container_width=True):
236
+ st.session_state.messages = []
237
+ st.session_state.agent = ChatbotOrchestrator()
238
+ st.rerun()
239
+
240
+ # -----------------------------
241
+ # Main Content
242
+ # -----------------------------
243
+
244
+ # Hero Banner (Always visible & Sticky)
245
+ st.markdown("""
246
+ <div class="hero-container" role="banner">
247
+ <div class="hero-title">🧠 Layered AI Agent</div>
248
+ <div class="hero-subtitle">Architecture: Perception ➜ Cognition ➜ Action</div>
249
+ </div>
250
+ """, unsafe_allow_html=True)
251
+
252
+ # Display Chat History
253
+ for message in st.session_state.messages:
254
+ with st.chat_message(message["role"]):
255
+ st.markdown(message["content"], unsafe_allow_html=True)
256
+
257
+ # Chat Input Handling
258
+ # We handle both the chat input widget and the sidebar selection here
259
+ if prompt := (st.chat_input("Type your message...") or selected_prompt):
260
+ # User Message
261
+ st.session_state.messages.append({"role": "user", "content": prompt})
262
+ with st.chat_message("user"):
263
+ st.markdown(prompt)
264
+
265
+ # Assistant Response
266
+ with st.chat_message("assistant"):
267
+ with st.spinner("🧠 Thinking (Layers Active)..."):
268
+ response_text = asyncio.run(get_ai_response(prompt))
269
+ st.markdown(response_text, unsafe_allow_html=True)
270
+
271
+ st.session_state.messages.append({"role": "assistant", "content": response_text})
272
+
273
+ if selected_prompt:
274
+ st.rerun()
src/chatbot_v2/layers/__init__.py ADDED
File without changes
src/chatbot_v2/layers/action.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from typing import Any, Dict, List
3
+ from agents import Runner
4
+ from common.aagents.search_agent import search_agent
5
+ from common.aagents.news_agent import news_agent
6
+ from common.aagents.yf_agent import yf_agent
7
+
8
+ class ActionLayer:
9
+ """
10
+ The 'Hands' of the agent.
11
+ Responsibility: Execute specific, well-defined tools or side-effects.
12
+ Does NOT reason about 'why'.
13
+ """
14
+
15
+ def __init__(self):
16
+ # Register available tools
17
+ self.tools = {
18
+ "web_search": self._tool_web_search,
19
+ "financial_data": self._tool_financial_data,
20
+ "news_search": self._tool_news_search,
21
+ "broadcast_research": self._tool_broadcast_research
22
+ }
23
+
24
+ async def execute(self, tool_name: str, args: Dict[str, Any]) -> str:
25
+ if tool_name not in self.tools:
26
+ return f"Error: Tool '{tool_name}' not found."
27
+
28
+ print(f"[Action] Executing tool: {tool_name} with args: {args}")
29
+ try:
30
+ return await self.tools[tool_name](**args)
31
+ except Exception as e:
32
+ return f"Error executing {tool_name}: {str(e)}"
33
+
34
+ async def _tool_web_search(self, query: str) -> str:
35
+ result = await Runner.run(search_agent, query)
36
+ return f"Web Search Result:\n{result.final_output}"
37
+
38
+ async def _tool_financial_data(self, query: str) -> str:
39
+ result = await Runner.run(yf_agent, query)
40
+ return f"Financial Data:\n{result.final_output}"
41
+
42
+ async def _tool_news_search(self, query: str) -> str:
43
+ result = await Runner.run(news_agent, query)
44
+ return f"News Result:\n{result.final_output}"
45
+
46
+ async def _tool_broadcast_research(self, query: str, include_finance: bool = True, include_news: bool = True, include_search: bool = True) -> str:
47
+ """
48
+ Broadcasts the search query to selected specialized agents in parallel and aggregates their responses.
49
+ """
50
+ active_agents = []
51
+ if include_finance: active_agents.append(("YahooFinanceAgent", Runner.run(yf_agent, query)))
52
+ if include_news: active_agents.append(("NewsAgent", Runner.run(news_agent, query)))
53
+ if include_search: active_agents.append(("WebSearchAgent", Runner.run(search_agent, query)))
54
+
55
+ if not active_agents:
56
+ return "No agents were selected for this query."
57
+
58
+ # Run in parallel
59
+ agent_names = [name for name, _ in active_agents]
60
+ coroutines = [coro for _, coro in active_agents]
61
+
62
+ results = await asyncio.gather(*coroutines, return_exceptions=True)
63
+
64
+ outputs = []
65
+ for name, res in zip(agent_names, results):
66
+ if isinstance(res, Exception):
67
+ outputs.append(f"❌ {name} Error: {str(res)}")
68
+ else:
69
+ outputs.append(f"✅ {name} Report:\n{res.final_output}")
70
+
71
+ return "\n--- START OF AGENT REPORTS ---\n\n" + "\n\n-----------------------------------\n\n".join(outputs) + "\n\n--- END OF AGENT REPORTS ---"
src/chatbot_v2/layers/cognition.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from typing import List, Dict, Optional
4
+ from common.utility.openai_model_factory import OpenAIModelFactory
5
+ from openai import OpenAI, AsyncOpenAI
6
+
7
+ class CognitiveOutput:
8
+ def __init__(self, thought: str, action: Optional[str] = None, action_input: Optional[Dict] = None, final_answer: Optional[str] = None):
9
+ self.thought = thought
10
+ self.action = action
11
+ self.action_input = action_input
12
+ self.final_answer = final_answer
13
+
14
+ class CognitionLayer:
15
+ """
16
+ The 'Brain' of the agent.
17
+ Uses OpenAI GPT-4o (via OpenAIModelFactory) to reason and decide which tools to use.
18
+ """
19
+
20
+
21
+ def __init__(self):
22
+ # Direct OpenAI client usage for maximum compatibility
23
+ # We circumvent the factory wrapper to access the raw client directly for JSON mode
24
+ api_key = os.environ.get("OPENAI_API_KEY")
25
+ self.client = OpenAI(api_key=api_key) if api_key else None
26
+
27
+ # If we wanted to use the factory strictly, we'd need to know the exact internal attribute
28
+ # self.model_wrapper = OpenAIModelFactory.get_model(...)
29
+ # self.client = self.model_wrapper.client # guessing 'client' vs 'openai_client'
30
+ # But to be safe and fix the user's error immediately:
31
+ from openai import AsyncOpenAI
32
+ self.client = AsyncOpenAI(api_key=api_key)
33
+
34
+ self.model_name = "gpt-4o"
35
+
36
+ self.system_prompt = """
37
+ You are the **AI Chat Orchestrator**.
38
+ Your goal is to provide a comprehensive, multi-perspective answer by synthesizing data from specialized sub-agents.
39
+
40
+ **Available Tools**:
41
+ 1. `broadcast_research(query: str, include_finance: bool, include_news: bool, include_search: bool)`:
42
+ Broadcasts the query to specialized agents (Finance, News, Web Search). Use this for complex queries needing external info.
43
+ 2. `web_search(query: str)`: Single web search.
44
+ 3. `financial_data(query: str)`: Single financial check.
45
+ 4. `news_search(query: str)`: Single news check.
46
+
47
+ **Workflow**:
48
+ 1. **Analyze Request**: Understand the user's question.
49
+ 2. **Determine Needs**: Decide calls are needed.
50
+ * **Finance**: For stock prices, market trends, company financials.
51
+ * **News**: For recent events, headlines.
52
+ * **Web Search**: For general knowledge, history.
53
+ 3. **Action**:
54
+ If you need external info, PREFER `broadcast_research` to query multiple sources in parallel.
55
+ If it's a simple greeting or general chat not requiring data, just answer.
56
+ 4. **Synthesize Results**:
57
+ When you receive tool outputs ("Agent Reports"), combine them into a clear, professional summary.
58
+ Do NOT simply paste the reports. Synthesize them.
59
+
60
+ **Output Format**:
61
+ You must output valid JSON only:
62
+ {
63
+ "thought": "Reasoning...",
64
+ "action": "tool_name_or_null",
65
+ "action_input": { "arg": "value" } or null,
66
+ "final_answer": "Final output to user" or null
67
+ }
68
+ """
69
+
70
+ async def decide(self, history: List[Dict[str, str]]) -> CognitiveOutput:
71
+ # 1. Construct Messages
72
+ messages = [{"role": "system", "content": self.system_prompt}]
73
+
74
+ for entry in history:
75
+ role = entry['role']
76
+ content = entry['content']
77
+
78
+ # Map roles. 'system' in our history layer usually means tool output.
79
+ if role == 'user':
80
+ messages.append({"role": "user", "content": content})
81
+ elif role == 'assistant':
82
+ messages.append({"role": "assistant", "content": content})
83
+ elif role == 'system':
84
+ messages.append({"role": "user", "content": f"Observation/Tool Output: {content}"})
85
+
86
+ # 2. Call LLM (Async)
87
+ try:
88
+ completion = await self.client.chat.completions.create(
89
+ model=self.model_name,
90
+ messages=messages,
91
+ response_format={"type": "json_object"}
92
+ )
93
+
94
+ response_text = completion.choices[0].message.content
95
+ if response_text.startswith("```"):
96
+ response_text = response_text.strip("`").replace("json", "").strip()
97
+
98
+ data = json.loads(response_text)
99
+
100
+ return CognitiveOutput(
101
+ thought=data.get("thought", ""),
102
+ action=data.get("action"),
103
+ action_input=data.get("action_input"),
104
+ final_answer=data.get("final_answer")
105
+ )
106
+
107
+ except Exception as e:
108
+ return CognitiveOutput(
109
+ thought=f"Error: {str(e)}",
110
+ final_answer="I encountered an error processing your request."
111
+ )
src/chatbot_v2/layers/memory.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict
2
+
3
+ class MemoryLayer:
4
+ """
5
+ The 'Memory' of the agent.
6
+ Responsibility: Store and retrieve conversation history.
7
+ """
8
+
9
+ def __init__(self):
10
+ self.history: List[Dict[str, str]] = []
11
+
12
+ def add_entry(self, role: str, content: str):
13
+ self.history.append({"role": role, "content": content})
14
+
15
+ def get_history(self) -> List[Dict[str, str]]:
16
+ return self.history
17
+
18
+ def set_history(self, messages: List[Dict[str, str]]):
19
+ """Allows initializing/overwriting memory from external source (e.g. Streamlit session)"""
20
+ self.history = messages