Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +22 -0
- Dockerfile +22 -0
- README.md +27 -10
- pyproject.toml +201 -0
- src/accessibility_v1/Dockerfile +34 -0
- src/accessibility_v1/app.py +187 -0
- src/accessibility_v1/mcp/axe.min.js +0 -0
- src/accessibility_v1/mcp/server.py +373 -0
- src/accessibility_v1/notebook/accessibility.ipynb +768 -0
- src/accessibility_v1/notebook/output/accessibility_audit_20251112_061159.html +252 -0
- src/accessibility_v1/output/accessibility_dashboard_20251112_065648.html +171 -0
- src/accessibility_v1/output/accessibility_dashboard_20251112_070331.html +171 -0
- src/accessibility_v1/output/accessibility_dashboard_20251112_070840.html +171 -0
- src/accessibility_v1/output/accessibility_dashboard_20251112_071804.html +188 -0
- src/accessibility_v1/output/accessibility_dashboard_20251112_072100.html +188 -0
- src/accessibility_v1/output/accessibility_dashboard_20251211_051059.html +188 -0
- src/accessibility_v1/output/accessibility_dashboard_20251211_051323.html +188 -0
- src/accessibility_v1/output/accessibility_dashboard_20251211_052404.html +188 -0
- src/accessibility_v1/templates/dashboard_template.html +188 -0
- src/accessibility_v2/app.py +27 -0
- src/accessibility_v2/layers/action.py +20 -0
- src/accessibility_v2/layers/cognition.py +46 -0
- src/accessibility_v2/layers/perception.py +33 -0
- src/accessibility_v2/patterns/orchestrator.py +30 -0
- src/accessibility_v2/tools/axe.min.js +0 -0
- src/accessibility_v2/tools/web_auditor.py +88 -0
- src/chatbot_v1/Dockerfile +35 -0
- src/chatbot_v1/README.md +223 -0
- src/chatbot_v1/aagents/__init__.py +0 -0
- src/chatbot_v1/aagents/input_validation_agent.py +58 -0
- src/chatbot_v1/aagents/orchestrator_agent.py +96 -0
- src/chatbot_v1/app.py +317 -0
- src/chatbot_v1/core/__init__.py +4 -0
- src/chatbot_v1/core/model.py +36 -0
- src/chatbot_v1/prompts/economic_news.txt +27 -0
- src/chatbot_v1/prompts/entertainment_updates.txt +26 -0
- src/chatbot_v1/prompts/india_news.txt +26 -0
- src/chatbot_v1/prompts/market_sentiment.txt +34 -0
- src/chatbot_v1/prompts/news_headlines.txt +28 -0
- src/chatbot_v1/prompts/odia_news.txt +26 -0
- src/chatbot_v1/prompts/trade_recommendation.txt +40 -0
- src/chatbot_v1/prompts/upcoming_earnings.txt +27 -0
- src/chatbot_v1/trace_config.py +37 -0
- src/chatbot_v2/Dockerfile +35 -0
- src/chatbot_v2/README.md +223 -0
- src/chatbot_v2/app.py +274 -0
- src/chatbot_v2/layers/__init__.py +0 -0
- src/chatbot_v2/layers/action.py +71 -0
- src/chatbot_v2/layers/cognition.py +111 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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><title></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><html></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><h1></code> and properly structured <code><h2></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><title></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
|