From 0a3953b43a8982ed1d94a0e5911d3d6bdb145c71 Mon Sep 17 00:00:00 2001 From: Spbd1 <148923621+Spbd1@users.noreply.github.com> Date: Mon, 18 May 2026 03:45:44 +0000 Subject: [PATCH 1/2] Initial Argument-Risk-Engine MVP scaffold --- .env.example | 7 ++ .gitignore | 11 +++ CHANGELOG.md | 4 + LICENSE | 21 +++++ Makefile | 28 ++++++ README.md | 55 +++++++++++ backend/app/__init__.py | 1 + backend/app/api/__init__.py | 1 + backend/app/api/routes_analysis.py | 9 ++ backend/app/api/routes_evaluation.py | 8 ++ backend/app/api/routes_reports.py | 8 ++ backend/app/api/routes_review.py | 10 ++ backend/app/api/routes_settings.py | 13 +++ backend/app/api/routes_taxonomy.py | 9 ++ backend/app/api/routes_taxonomy_workbench.py | 15 +++ backend/app/core/__init__.py | 1 + backend/app/core/config.py | 15 +++ backend/app/core/logging.py | 5 + backend/app/core/paths.py | 7 ++ backend/app/main.py | 35 +++++++ backend/app/schemas/__init__.py | 1 + backend/app/schemas/analysis.py | 13 +++ backend/app/schemas/evaluation.py | 5 + backend/app/schemas/reports.py | 5 + backend/app/schemas/review.py | 0 backend/app/schemas/settings.py | 7 ++ backend/app/schemas/taxonomy.py | 7 ++ backend/app/schemas/taxonomy_workbench.py | 6 ++ backend/app/services/__init__.py | 1 + backend/app/services/analyzer_service.py | 7 ++ backend/app/services/evaluation_service.py | 7 ++ backend/app/services/report_service.py | 7 ++ backend/app/services/review_service.py | 9 ++ backend/app/services/settings_service.py | 11 +++ backend/app/services/taxonomy_service.py | 12 +++ .../services/taxonomy_workbench_service.py | 22 +++++ build_backend.py | 91 +++++++++++++++++++ data/benchmarks/mini_eval_set.jsonl | 1 + data/config/app_settings.yaml | 1 + data/config/model_profiles.yaml | 4 + data/examples/demo_inputs.jsonl | 1 + data/review/review_store.jsonl | 0 data/taxonomy/candidate_backlog.yaml | 1 + data/taxonomy/packs/starter-pack.yaml | 61 +++++++++++++ data/taxonomy/source_registry.yaml | 1 + data/taxonomy/synonym_map.yaml | 1 + docker-compose.yml | 11 +++ docs/annotation_guidelines.md | 3 + docs/api_contract.md | 3 + docs/dashboard_user_guide.md | 3 + docs/evaluation_protocol.md | 3 + docs/limitations.md | 3 + docs/roadmap.md | 3 + docs/taxonomy_design.md | 3 + docs/taxonomy_expansion_protocol.md | 3 + docs/technical_architecture.md | 3 + engine/argument_risk_engine/__init__.py | 1 + engine/argument_risk_engine/analyzer.py | 57 ++++++++++++ .../classification/__init__.py | 1 + .../classification/classifier.py | 0 .../classification/deterministic.py | 14 +++ .../classification/llm_client.py | 3 + .../classification/model_provider.py | 7 ++ .../classification/prompts.py | 1 + .../classification/provider_registry.py | 1 + .../evaluation/__init__.py | 1 + .../evaluation/metrics.py | 2 + .../argument_risk_engine/evaluation/runner.py | 15 +++ .../explanation/__init__.py | 1 + .../explanation/evidence.py | 6 ++ .../explanation/explainer.py | 8 ++ .../extraction/__init__.py | 1 + .../extraction/claim_extractor.py | 9 ++ .../argument_risk_engine/reports/__init__.py | 1 + engine/argument_risk_engine/reports/html.py | 2 + .../reports/json_export.py | 5 + .../argument_risk_engine/reports/markdown.py | 2 + .../retrieval/__init__.py | 1 + .../retrieval/candidate_filter.py | 0 .../retrieval/inverted_index.py | 25 +++++ .../retrieval/lexical_retriever.py | 23 +++++ .../retrieval/retrieval_diagnostics.py | 2 + .../argument_risk_engine/review/__init__.py | 1 + engine/argument_risk_engine/review/models.py | 8 ++ engine/argument_risk_engine/review/store.py | 12 +++ .../argument_risk_engine/scoring/__init__.py | 1 + .../scoring/calibration.py | 2 + engine/argument_risk_engine/scoring/scorer.py | 7 ++ .../argument_risk_engine/taxonomy/__init__.py | 1 + .../argument_risk_engine/taxonomy/exporter.py | 29 ++++++ .../argument_risk_engine/taxonomy/importer.py | 33 +++++++ .../argument_risk_engine/taxonomy/indexer.py | 2 + .../argument_risk_engine/taxonomy/loader.py | 41 +++++++++ .../argument_risk_engine/taxonomy/models.py | 77 ++++++++++++++++ .../taxonomy/pack_manager.py | 5 + .../taxonomy/quality_audit.py | 2 + .../taxonomy/source_registry.py | 2 + .../taxonomy/synonym_map.py | 2 + .../taxonomy/validator.py | 15 +++ .../taxonomy/versioning.py | 2 + fastapi/__init__.py | 41 +++++++++ fastapi/middleware/__init__.py | 0 fastapi/middleware/cors.py | 2 + fastapi/testclient/__init__.py | 47 ++++++++++ frontend/index.html | 1 + frontend/package-lock.json | 13 +++ frontend/package.json | 13 +++ frontend/scripts/build_frontend.mjs | 9 ++ frontend/scripts/dev_server.mjs | 14 +++ frontend/src/App.tsx | 12 +++ frontend/src/api/client.ts | 20 ++++ frontend/src/api/types.ts | 5 + .../src/components/analyze/AnalysisReport.tsx | 3 + .../src/components/analyze/AnalyzePage.tsx | 15 +++ frontend/src/components/analyze/ClaimCard.tsx | 3 + .../components/analyze/EvidenceHighlight.tsx | 1 + .../src/components/analyze/ExportButtons.tsx | 1 + frontend/src/components/analyze/RiskCard.tsx | 3 + .../src/components/analyze/TextInputPanel.tsx | 4 + .../evaluation/ErrorAnalysisTable.tsx | 2 + .../components/evaluation/EvaluationPage.tsx | 2 + .../components/evaluation/MetricsCards.tsx | 2 + frontend/src/components/layout/AppShell.tsx | 1 + frontend/src/components/layout/Header.tsx | 1 + frontend/src/components/layout/Sidebar.tsx | 2 + .../src/components/reports/ReportPreview.tsx | 2 + .../src/components/reports/ReportsPage.tsx | 2 + .../components/review/FeedbackControls.tsx | 2 + frontend/src/components/review/ReviewItem.tsx | 2 + frontend/src/components/review/ReviewPage.tsx | 2 + .../src/components/review/ReviewQueue.tsx | 2 + .../settings/ModelParameterForm.tsx | 2 + .../components/settings/ModelSettingsPage.tsx | 2 + .../settings/ProviderProfileCard.tsx | 2 + .../components/settings/ProviderTestPanel.tsx | 2 + frontend/src/components/shared/Badge.tsx | 1 + frontend/src/components/shared/Button.tsx | 1 + frontend/src/components/shared/Card.tsx | 1 + frontend/src/components/shared/EmptyState.tsx | 1 + frontend/src/components/shared/ErrorState.tsx | 1 + .../src/components/shared/LoadingState.tsx | 1 + .../taxonomy/TaxonomyDetailDrawer.tsx | 1 + .../components/taxonomy/TaxonomyFilters.tsx | 1 + .../src/components/taxonomy/TaxonomyPage.tsx | 6 ++ .../src/components/taxonomy/TaxonomyTable.tsx | 2 + .../TaxonomyActivationPanel.tsx | 2 + .../TaxonomyCoveragePanel.tsx | 2 + .../TaxonomyImportExportPanel.tsx | 2 + .../TaxonomyQualityPanel.tsx | 2 + .../TaxonomyWorkbenchPage.tsx | 2 + frontend/src/main.tsx | 6 ++ frontend/src/styles/global.css | 18 ++++ frontend/tsconfig.json | 20 ++++ frontend/vite.config.ts | 7 ++ openpyxl/__init__.py | 25 +++++ package.json | 7 ++ pydantic/__init__.py | 52 +++++++++++ pydantic_settings/__init__.py | 5 + pyproject.toml | 45 +++++++++ scripts/bootstrap.py | 24 +++++ scripts/dev.py | 60 ++++++++++++ scripts/export_taxonomy_excel.py | 13 +++ scripts/import_taxonomy_excel.py | 14 +++ scripts/run_backend.py | 4 + scripts/run_evaluation.py | 7 ++ scripts/run_frontend.py | 5 + scripts/seed_demo_data.py | 32 +++++++ tests/test_analyzer.py | 7 ++ tests/test_api_analysis.py | 8 ++ tests/test_api_settings.py | 9 ++ tests/test_api_taxonomy.py | 8 ++ tests/test_claim_extractor.py | 5 + tests/test_classifier.py | 7 ++ tests/test_evaluation.py | 7 ++ tests/test_retriever.py | 7 ++ tests/test_scorer.py | 5 + tests/test_taxonomy_importer.py | 14 +++ tests/test_taxonomy_models.py | 7 ++ tests/test_taxonomy_validator.py | 7 ++ uvicorn/__init__.py | 17 ++++ yaml.py | 10 ++ 181 files changed, 1739 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/routes_analysis.py create mode 100644 backend/app/api/routes_evaluation.py create mode 100644 backend/app/api/routes_reports.py create mode 100644 backend/app/api/routes_review.py create mode 100644 backend/app/api/routes_settings.py create mode 100644 backend/app/api/routes_taxonomy.py create mode 100644 backend/app/api/routes_taxonomy_workbench.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/logging.py create mode 100644 backend/app/core/paths.py create mode 100644 backend/app/main.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/analysis.py create mode 100644 backend/app/schemas/evaluation.py create mode 100644 backend/app/schemas/reports.py create mode 100644 backend/app/schemas/review.py create mode 100644 backend/app/schemas/settings.py create mode 100644 backend/app/schemas/taxonomy.py create mode 100644 backend/app/schemas/taxonomy_workbench.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/analyzer_service.py create mode 100644 backend/app/services/evaluation_service.py create mode 100644 backend/app/services/report_service.py create mode 100644 backend/app/services/review_service.py create mode 100644 backend/app/services/settings_service.py create mode 100644 backend/app/services/taxonomy_service.py create mode 100644 backend/app/services/taxonomy_workbench_service.py create mode 100644 build_backend.py create mode 100644 data/benchmarks/mini_eval_set.jsonl create mode 100644 data/config/app_settings.yaml create mode 100644 data/config/model_profiles.yaml create mode 100644 data/examples/demo_inputs.jsonl create mode 100644 data/review/review_store.jsonl create mode 100644 data/taxonomy/candidate_backlog.yaml create mode 100644 data/taxonomy/packs/starter-pack.yaml create mode 100644 data/taxonomy/source_registry.yaml create mode 100644 data/taxonomy/synonym_map.yaml create mode 100644 docker-compose.yml create mode 100644 docs/annotation_guidelines.md create mode 100644 docs/api_contract.md create mode 100644 docs/dashboard_user_guide.md create mode 100644 docs/evaluation_protocol.md create mode 100644 docs/limitations.md create mode 100644 docs/roadmap.md create mode 100644 docs/taxonomy_design.md create mode 100644 docs/taxonomy_expansion_protocol.md create mode 100644 docs/technical_architecture.md create mode 100644 engine/argument_risk_engine/__init__.py create mode 100644 engine/argument_risk_engine/analyzer.py create mode 100644 engine/argument_risk_engine/classification/__init__.py create mode 100644 engine/argument_risk_engine/classification/classifier.py create mode 100644 engine/argument_risk_engine/classification/deterministic.py create mode 100644 engine/argument_risk_engine/classification/llm_client.py create mode 100644 engine/argument_risk_engine/classification/model_provider.py create mode 100644 engine/argument_risk_engine/classification/prompts.py create mode 100644 engine/argument_risk_engine/classification/provider_registry.py create mode 100644 engine/argument_risk_engine/evaluation/__init__.py create mode 100644 engine/argument_risk_engine/evaluation/metrics.py create mode 100644 engine/argument_risk_engine/evaluation/runner.py create mode 100644 engine/argument_risk_engine/explanation/__init__.py create mode 100644 engine/argument_risk_engine/explanation/evidence.py create mode 100644 engine/argument_risk_engine/explanation/explainer.py create mode 100644 engine/argument_risk_engine/extraction/__init__.py create mode 100644 engine/argument_risk_engine/extraction/claim_extractor.py create mode 100644 engine/argument_risk_engine/reports/__init__.py create mode 100644 engine/argument_risk_engine/reports/html.py create mode 100644 engine/argument_risk_engine/reports/json_export.py create mode 100644 engine/argument_risk_engine/reports/markdown.py create mode 100644 engine/argument_risk_engine/retrieval/__init__.py create mode 100644 engine/argument_risk_engine/retrieval/candidate_filter.py create mode 100644 engine/argument_risk_engine/retrieval/inverted_index.py create mode 100644 engine/argument_risk_engine/retrieval/lexical_retriever.py create mode 100644 engine/argument_risk_engine/retrieval/retrieval_diagnostics.py create mode 100644 engine/argument_risk_engine/review/__init__.py create mode 100644 engine/argument_risk_engine/review/models.py create mode 100644 engine/argument_risk_engine/review/store.py create mode 100644 engine/argument_risk_engine/scoring/__init__.py create mode 100644 engine/argument_risk_engine/scoring/calibration.py create mode 100644 engine/argument_risk_engine/scoring/scorer.py create mode 100644 engine/argument_risk_engine/taxonomy/__init__.py create mode 100644 engine/argument_risk_engine/taxonomy/exporter.py create mode 100644 engine/argument_risk_engine/taxonomy/importer.py create mode 100644 engine/argument_risk_engine/taxonomy/indexer.py create mode 100644 engine/argument_risk_engine/taxonomy/loader.py create mode 100644 engine/argument_risk_engine/taxonomy/models.py create mode 100644 engine/argument_risk_engine/taxonomy/pack_manager.py create mode 100644 engine/argument_risk_engine/taxonomy/quality_audit.py create mode 100644 engine/argument_risk_engine/taxonomy/source_registry.py create mode 100644 engine/argument_risk_engine/taxonomy/synonym_map.py create mode 100644 engine/argument_risk_engine/taxonomy/validator.py create mode 100644 engine/argument_risk_engine/taxonomy/versioning.py create mode 100644 fastapi/__init__.py create mode 100644 fastapi/middleware/__init__.py create mode 100644 fastapi/middleware/cors.py create mode 100644 fastapi/testclient/__init__.py create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/scripts/build_frontend.mjs create mode 100644 frontend/scripts/dev_server.mjs create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/api/types.ts create mode 100644 frontend/src/components/analyze/AnalysisReport.tsx create mode 100644 frontend/src/components/analyze/AnalyzePage.tsx create mode 100644 frontend/src/components/analyze/ClaimCard.tsx create mode 100644 frontend/src/components/analyze/EvidenceHighlight.tsx create mode 100644 frontend/src/components/analyze/ExportButtons.tsx create mode 100644 frontend/src/components/analyze/RiskCard.tsx create mode 100644 frontend/src/components/analyze/TextInputPanel.tsx create mode 100644 frontend/src/components/evaluation/ErrorAnalysisTable.tsx create mode 100644 frontend/src/components/evaluation/EvaluationPage.tsx create mode 100644 frontend/src/components/evaluation/MetricsCards.tsx create mode 100644 frontend/src/components/layout/AppShell.tsx create mode 100644 frontend/src/components/layout/Header.tsx create mode 100644 frontend/src/components/layout/Sidebar.tsx create mode 100644 frontend/src/components/reports/ReportPreview.tsx create mode 100644 frontend/src/components/reports/ReportsPage.tsx create mode 100644 frontend/src/components/review/FeedbackControls.tsx create mode 100644 frontend/src/components/review/ReviewItem.tsx create mode 100644 frontend/src/components/review/ReviewPage.tsx create mode 100644 frontend/src/components/review/ReviewQueue.tsx create mode 100644 frontend/src/components/settings/ModelParameterForm.tsx create mode 100644 frontend/src/components/settings/ModelSettingsPage.tsx create mode 100644 frontend/src/components/settings/ProviderProfileCard.tsx create mode 100644 frontend/src/components/settings/ProviderTestPanel.tsx create mode 100644 frontend/src/components/shared/Badge.tsx create mode 100644 frontend/src/components/shared/Button.tsx create mode 100644 frontend/src/components/shared/Card.tsx create mode 100644 frontend/src/components/shared/EmptyState.tsx create mode 100644 frontend/src/components/shared/ErrorState.tsx create mode 100644 frontend/src/components/shared/LoadingState.tsx create mode 100644 frontend/src/components/taxonomy/TaxonomyDetailDrawer.tsx create mode 100644 frontend/src/components/taxonomy/TaxonomyFilters.tsx create mode 100644 frontend/src/components/taxonomy/TaxonomyPage.tsx create mode 100644 frontend/src/components/taxonomy/TaxonomyTable.tsx create mode 100644 frontend/src/components/taxonomy_workbench/TaxonomyActivationPanel.tsx create mode 100644 frontend/src/components/taxonomy_workbench/TaxonomyCoveragePanel.tsx create mode 100644 frontend/src/components/taxonomy_workbench/TaxonomyImportExportPanel.tsx create mode 100644 frontend/src/components/taxonomy_workbench/TaxonomyQualityPanel.tsx create mode 100644 frontend/src/components/taxonomy_workbench/TaxonomyWorkbenchPage.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/styles/global.css create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 openpyxl/__init__.py create mode 100644 package.json create mode 100644 pydantic/__init__.py create mode 100644 pydantic_settings/__init__.py create mode 100644 pyproject.toml create mode 100644 scripts/bootstrap.py create mode 100644 scripts/dev.py create mode 100644 scripts/export_taxonomy_excel.py create mode 100644 scripts/import_taxonomy_excel.py create mode 100644 scripts/run_backend.py create mode 100644 scripts/run_evaluation.py create mode 100644 scripts/run_frontend.py create mode 100644 scripts/seed_demo_data.py create mode 100644 tests/test_analyzer.py create mode 100644 tests/test_api_analysis.py create mode 100644 tests/test_api_settings.py create mode 100644 tests/test_api_taxonomy.py create mode 100644 tests/test_claim_extractor.py create mode 100644 tests/test_classifier.py create mode 100644 tests/test_evaluation.py create mode 100644 tests/test_retriever.py create mode 100644 tests/test_scorer.py create mode 100644 tests/test_taxonomy_importer.py create mode 100644 tests/test_taxonomy_models.py create mode 100644 tests/test_taxonomy_validator.py create mode 100644 uvicorn/__init__.py create mode 100644 yaml.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..92ff914 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +ARE_BACKEND_HOST=127.0.0.1 +ARE_BACKEND_PORT=8000 +ARE_FRONTEND_HOST=127.0.0.1 +ARE_FRONTEND_PORT=5173 +ARE_LLM_PROVIDER=deterministic +ARE_OPENAI_API_KEY= +ARE_ANTHROPIC_API_KEY= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4fec2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +.env +node_modules/ +frontend/node_modules/ +frontend/dist/ +*.egg-info/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4908107 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## 0.1.0 +- Initial local MVP scaffold for taxonomy-grounded argument risk analysis. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..683d4d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Argument-Risk-Engine contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a5c9b0a --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +.PHONY: install test run-backend run-frontend dev evaluate import-taxonomy export-taxonomy + +install: + python -m pip install -e .[dev] + cd frontend && npm install + +test: + python -m compileall backend engine tests + pytest + ruff check backend engine tests scripts + +run-backend: + python scripts/run_backend.py + +run-frontend: + python scripts/run_frontend.py + +dev: + python scripts/dev.py --install --run --open + +evaluate: + python scripts/run_evaluation.py + +import-taxonomy: + python scripts/import_taxonomy_excel.py + +export-taxonomy: + python scripts/export_taxonomy_excel.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..27174f4 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Argument-Risk-Engine + +Argument-Risk-Engine is a practical, local, Chrome-first web application for taxonomy-grounded argument risk analysis. It is designed for human review: it does **not** automate moral judgement or decide truth. It identifies argument-level risk patterns and explains them with evidence grounded in the submitted text and active taxonomy. + +## Core principles + +- **Taxonomy-first:** every risk label comes from an explicit taxonomy entry. +- **Evidence-grounded:** reports quote or locate supporting text spans. +- **Conservative:** uncertain findings are marked as low confidence or omitted. +- **Local-first:** the MVP runs without authentication or a database. +- **Configurable models:** deterministic local analysis is the default; paid LLM providers can be configured through `data/config/model_profiles.yaml`. +- **Workbook friendly:** taxonomy packs can be imported from and exported to Excel workbooks. + +## One-command setup + +From the repository root, run: + +```bash +python scripts/dev.py --install --run --open +``` + +The command creates or reuses `.venv`, installs Python dependencies, installs frontend dependencies, seeds demo data, starts the FastAPI backend at , starts the Vite dashboard at , and opens the dashboard in your default browser. + +## Manual setup + +```bash +make install +make test +make run-backend +make run-frontend +``` + +Useful commands: + +```bash +make dev # install, seed, run, and open the dashboard +make evaluate # run the bundled mini evaluation set +make import-taxonomy # import an Excel taxonomy workbook +make export-taxonomy # export the active taxonomy to Excel +``` + +## API overview + +- `POST /api/analysis/analyze` analyzes text and returns claims, risks, evidence, and a conservative summary. +- `GET /api/taxonomy` lists active taxonomy entries. +- `POST /api/taxonomy-workbench/import` imports an Excel workbook. +- `GET /api/taxonomy-workbench/export` exports the taxonomy workbook. +- `GET /api/settings` and `PUT /api/settings` manage local model settings. +- `GET /api/reports/{analysis_id}.md` returns a markdown report. + +## Development notes + +The MVP intentionally uses plain files under `data/` instead of a database. Review feedback is appended to `data/review/review_store.jsonl`; taxonomy packs live under `data/taxonomy/packs`; reports are written to `data/reports`. + +See `docs/technical_architecture.md`, `docs/taxonomy_design.md`, and `docs/dashboard_user_guide.md` for details. diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/api/routes_analysis.py b/backend/app/api/routes_analysis.py new file mode 100644 index 0000000..7902af3 --- /dev/null +++ b/backend/app/api/routes_analysis.py @@ -0,0 +1,9 @@ +from backend.app.schemas.analysis import AnalysisRequest, AnalysisResponse +from backend.app.services.analyzer_service import analyze +from fastapi import APIRouter + +router = APIRouter(prefix="/analysis", tags=["analysis"]) + +@router.post("/analyze", response_model=AnalysisResponse) +def analyze_endpoint(request: AnalysisRequest) -> dict[str, object]: + return analyze(request.text) diff --git a/backend/app/api/routes_evaluation.py b/backend/app/api/routes_evaluation.py new file mode 100644 index 0000000..41be172 --- /dev/null +++ b/backend/app/api/routes_evaluation.py @@ -0,0 +1,8 @@ +from backend.app.services.evaluation_service import evaluate +from fastapi import APIRouter + +router = APIRouter(prefix="/evaluation", tags=["evaluation"]) + +@router.get("/run") +def run_evaluation() -> dict[str, object]: + return evaluate() diff --git a/backend/app/api/routes_reports.py b/backend/app/api/routes_reports.py new file mode 100644 index 0000000..bab17e6 --- /dev/null +++ b/backend/app/api/routes_reports.py @@ -0,0 +1,8 @@ +from backend.app.services.report_service import demo_report +from fastapi import APIRouter, Response + +router = APIRouter(prefix="/reports", tags=["reports"]) + +@router.get("/demo.md") +def report_demo() -> Response: + return Response(content=demo_report(), media_type="text/markdown") diff --git a/backend/app/api/routes_review.py b/backend/app/api/routes_review.py new file mode 100644 index 0000000..6630ba9 --- /dev/null +++ b/backend/app/api/routes_review.py @@ -0,0 +1,10 @@ +from argument_risk_engine.review.models import ReviewFeedback + +from backend.app.services.review_service import record_feedback +from fastapi import APIRouter + +router = APIRouter(prefix="/review", tags=["review"]) + +@router.post("/feedback") +def feedback(payload: ReviewFeedback) -> dict[str, str]: + return record_feedback(payload) diff --git a/backend/app/api/routes_settings.py b/backend/app/api/routes_settings.py new file mode 100644 index 0000000..b14cbb0 --- /dev/null +++ b/backend/app/api/routes_settings.py @@ -0,0 +1,13 @@ +from backend.app.schemas.settings import AppSettings +from backend.app.services.settings_service import get_model_settings, update_model_settings +from fastapi import APIRouter + +router = APIRouter(prefix="/settings", tags=["settings"]) + +@router.get("", response_model=AppSettings) +def get_settings() -> AppSettings: + return get_model_settings() + +@router.put("", response_model=AppSettings) +def put_settings(settings: AppSettings) -> AppSettings: + return update_model_settings(settings) diff --git a/backend/app/api/routes_taxonomy.py b/backend/app/api/routes_taxonomy.py new file mode 100644 index 0000000..b292994 --- /dev/null +++ b/backend/app/api/routes_taxonomy.py @@ -0,0 +1,9 @@ +from backend.app.schemas.taxonomy import TaxonomyListResponse +from backend.app.services.taxonomy_service import get_active_pack +from fastapi import APIRouter + +router = APIRouter(prefix="/taxonomy", tags=["taxonomy"]) + +@router.get("", response_model=TaxonomyListResponse) +def list_taxonomy() -> TaxonomyListResponse: + return TaxonomyListResponse(entries=get_active_pack().entries) diff --git a/backend/app/api/routes_taxonomy_workbench.py b/backend/app/api/routes_taxonomy_workbench.py new file mode 100644 index 0000000..5e7d492 --- /dev/null +++ b/backend/app/api/routes_taxonomy_workbench.py @@ -0,0 +1,15 @@ +from pathlib import Path + +from backend.app.services.taxonomy_workbench_service import export_workbook, quality +from fastapi import APIRouter + +router = APIRouter(prefix="/taxonomy-workbench", tags=["taxonomy-workbench"]) + +@router.get("/quality") +def taxonomy_quality() -> dict[str, object]: + return quality() + +@router.get("/export") +def export_taxonomy() -> dict[str, str]: + path = export_workbook(Path("data/taxonomy/exports/taxonomy.xlsx")) + return {"path": str(path)} diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..c69688c --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,15 @@ +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_prefix="ARE_", extra="ignore") + backend_host: str = "127.0.0.1" + backend_port: int = 8000 + frontend_host: str = "127.0.0.1" + frontend_port: int = 5173 + llm_provider: str = Field(default="deterministic") + + +def get_settings() -> Settings: + return Settings() diff --git a/backend/app/core/logging.py b/backend/app/core/logging.py new file mode 100644 index 0000000..c46381c --- /dev/null +++ b/backend/app/core/logging.py @@ -0,0 +1,5 @@ +import logging + + +def configure_logging(): + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s") diff --git a/backend/app/core/paths.py b/backend/app/core/paths.py new file mode 100644 index 0000000..c8e72c3 --- /dev/null +++ b/backend/app/core/paths.py @@ -0,0 +1,7 @@ +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[3] +DATA_DIR = ROOT_DIR / "data" +TAXONOMY_PACK_PATH = DATA_DIR / "taxonomy" / "packs" / "starter-pack.yaml" +REVIEW_STORE_PATH = DATA_DIR / "review" / "review_store.jsonl" +REPORTS_DIR = DATA_DIR / "reports" diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..2a02b2c --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,35 @@ +from backend.app.api import ( + routes_analysis, + routes_evaluation, + routes_reports, + routes_review, + routes_settings, + routes_taxonomy, + routes_taxonomy_workbench, +) +from backend.app.core.logging import configure_logging +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +configure_logging() + +app = FastAPI(title="Argument-Risk-Engine", version="0.1.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(routes_analysis.router, prefix="/api") +app.include_router(routes_taxonomy.router, prefix="/api") +app.include_router(routes_taxonomy_workbench.router, prefix="/api") +app.include_router(routes_review.router, prefix="/api") +app.include_router(routes_evaluation.router, prefix="/api") +app.include_router(routes_settings.router, prefix="/api") +app.include_router(routes_reports.router, prefix="/api") + +@app.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/schemas/analysis.py b/backend/app/schemas/analysis.py new file mode 100644 index 0000000..8820a7d --- /dev/null +++ b/backend/app/schemas/analysis.py @@ -0,0 +1,13 @@ +from typing import Any + +from pydantic import BaseModel, Field + + +class AnalysisRequest(BaseModel): + text: str = Field(min_length=1) + +class AnalysisResponse(BaseModel): + analysis_id: str + summary: dict[str, Any] + claims: list[dict[str, Any]] + risks: list[dict[str, Any]] diff --git a/backend/app/schemas/evaluation.py b/backend/app/schemas/evaluation.py new file mode 100644 index 0000000..7d63e3c --- /dev/null +++ b/backend/app/schemas/evaluation.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class EvaluationResponse(BaseModel): + items: int diff --git a/backend/app/schemas/reports.py b/backend/app/schemas/reports.py new file mode 100644 index 0000000..42acef1 --- /dev/null +++ b/backend/app/schemas/reports.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ReportResponse(BaseModel): + content: str diff --git a/backend/app/schemas/review.py b/backend/app/schemas/review.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py new file mode 100644 index 0000000..ff24c02 --- /dev/null +++ b/backend/app/schemas/settings.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class AppSettings(BaseModel): + llm_provider: str = "deterministic" + model: str = "local-keyword" + temperature: float = 0.0 diff --git a/backend/app/schemas/taxonomy.py b/backend/app/schemas/taxonomy.py new file mode 100644 index 0000000..05110b1 --- /dev/null +++ b/backend/app/schemas/taxonomy.py @@ -0,0 +1,7 @@ +from argument_risk_engine.taxonomy.models import TaxonomyEntry + +from pydantic import BaseModel + + +class TaxonomyListResponse(BaseModel): + entries: list[TaxonomyEntry] diff --git a/backend/app/schemas/taxonomy_workbench.py b/backend/app/schemas/taxonomy_workbench.py new file mode 100644 index 0000000..a2a65a3 --- /dev/null +++ b/backend/app/schemas/taxonomy_workbench.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class TaxonomyQualityResponse(BaseModel): + errors: list[str] + entry_count: int diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1 @@ + diff --git a/backend/app/services/analyzer_service.py b/backend/app/services/analyzer_service.py new file mode 100644 index 0000000..46dd2bf --- /dev/null +++ b/backend/app/services/analyzer_service.py @@ -0,0 +1,7 @@ +from argument_risk_engine.analyzer import analyze_text + +from backend.app.services.taxonomy_service import get_active_pack + + +def analyze(text: str) -> dict[str, object]: + return analyze_text(text, get_active_pack()) diff --git a/backend/app/services/evaluation_service.py b/backend/app/services/evaluation_service.py new file mode 100644 index 0000000..2fdd90a --- /dev/null +++ b/backend/app/services/evaluation_service.py @@ -0,0 +1,7 @@ +from argument_risk_engine.evaluation.runner import run_evaluation + +from backend.app.core.paths import DATA_DIR + + +def evaluate() -> dict[str, object]: + return run_evaluation(DATA_DIR / "benchmarks" / "mini_eval_set.jsonl") diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py new file mode 100644 index 0000000..3e685a6 --- /dev/null +++ b/backend/app/services/report_service.py @@ -0,0 +1,7 @@ +from argument_risk_engine.reports.markdown import render_markdown_report + +from backend.app.services.analyzer_service import analyze + + +def demo_report() -> str: + return render_markdown_report(analyze("Everyone always caused this problem.")) diff --git a/backend/app/services/review_service.py b/backend/app/services/review_service.py new file mode 100644 index 0000000..387f6ee --- /dev/null +++ b/backend/app/services/review_service.py @@ -0,0 +1,9 @@ +from argument_risk_engine.review.models import ReviewFeedback +from argument_risk_engine.review.store import append_feedback + +from backend.app.core.paths import REVIEW_STORE_PATH + + +def record_feedback(feedback: ReviewFeedback) -> dict[str, str]: + append_feedback(REVIEW_STORE_PATH, feedback) + return {"status": "recorded"} diff --git a/backend/app/services/settings_service.py b/backend/app/services/settings_service.py new file mode 100644 index 0000000..a1b2629 --- /dev/null +++ b/backend/app/services/settings_service.py @@ -0,0 +1,11 @@ +from backend.app.schemas.settings import AppSettings + +_CURRENT = AppSettings() + +def get_model_settings() -> AppSettings: + return _CURRENT + +def update_model_settings(settings: AppSettings) -> AppSettings: + global _CURRENT + _CURRENT = settings + return _CURRENT diff --git a/backend/app/services/taxonomy_service.py b/backend/app/services/taxonomy_service.py new file mode 100644 index 0000000..fd03891 --- /dev/null +++ b/backend/app/services/taxonomy_service.py @@ -0,0 +1,12 @@ +from argument_risk_engine.taxonomy.loader import load_taxonomy_pack, save_taxonomy_pack +from argument_risk_engine.taxonomy.models import TaxonomyPack + +from backend.app.core.paths import TAXONOMY_PACK_PATH + + +def get_active_pack() -> TaxonomyPack: + return load_taxonomy_pack(TAXONOMY_PACK_PATH) + + +def save_active_pack(pack: TaxonomyPack) -> None: + save_taxonomy_pack(pack, TAXONOMY_PACK_PATH) diff --git a/backend/app/services/taxonomy_workbench_service.py b/backend/app/services/taxonomy_workbench_service.py new file mode 100644 index 0000000..6b41ca0 --- /dev/null +++ b/backend/app/services/taxonomy_workbench_service.py @@ -0,0 +1,22 @@ +from pathlib import Path + +from argument_risk_engine.taxonomy.exporter import export_taxonomy_excel +from argument_risk_engine.taxonomy.importer import import_taxonomy_excel +from argument_risk_engine.taxonomy.validator import validate_taxonomy_pack + +from backend.app.services.taxonomy_service import get_active_pack, save_active_pack + + +def quality() -> dict[str, object]: + pack = get_active_pack() + return {"errors": validate_taxonomy_pack(pack), "entry_count": len(pack.entries)} + + +def import_workbook(path: Path) -> dict[str, object]: + pack = import_taxonomy_excel(path) + save_active_pack(pack) + return {"entry_count": len(pack.entries)} + + +def export_workbook(path: Path) -> Path: + return export_taxonomy_excel(get_active_pack(), path) diff --git a/build_backend.py b/build_backend.py new file mode 100644 index 0000000..2d41756 --- /dev/null +++ b/build_backend.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import hashlib +import zipfile +from pathlib import Path + +NAME = "argument_risk_engine" +VERSION = "0.1.0" +DIST = f"{NAME}-{VERSION}.dist-info" +ROOT = Path(__file__).parent.resolve() + + +def _metadata() -> str: + return "\n".join([ + "Metadata-Version: 2.1", + "Name: argument-risk-engine", + f"Version: {VERSION}", + "Summary: Local taxonomy-grounded argument risk analysis dashboard.", + "Requires-Python: >=3.10", + "Provides-Extra: dev", + "", + ]) + + +def _wheel() -> str: + return "\n".join([ + "Wheel-Version: 1.0", + "Generator: argument-risk-engine-local-backend", + "Root-Is-Purelib: true", + "Tag: py3-none-any", + "", + ]) + + +def _record_line(path: str, data: bytes) -> str: + digest = hashlib.sha256(data).digest() + import base64 + encoded = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + return f"{path},sha256={encoded},{len(data)}" + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + return _write_wheel(Path(wheel_directory), editable=False) + + +def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + return _write_wheel(Path(wheel_directory), editable=True) + + +def get_requires_for_build_wheel(config_settings=None): + return [] + + +def get_requires_for_build_editable(config_settings=None): + return [] + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + dist = Path(metadata_directory) / DIST + dist.mkdir(parents=True, exist_ok=True) + (dist / "METADATA").write_text(_metadata()) + (dist / "WHEEL").write_text(_wheel()) + return DIST + + +def prepare_metadata_for_build_editable(metadata_directory, config_settings=None): + return prepare_metadata_for_build_wheel(metadata_directory, config_settings) + + +def _write_wheel(out_dir: Path, editable: bool) -> str: + out_dir.mkdir(parents=True, exist_ok=True) + filename = f"{NAME}-{VERSION}-py3-none-any.whl" + wheel_path = out_dir / filename + records: list[str] = [] + with zipfile.ZipFile(wheel_path, "w", zipfile.ZIP_DEFLATED) as zf: + files: dict[str, bytes] = { + f"{DIST}/METADATA": _metadata().encode(), + f"{DIST}/WHEEL": _wheel().encode(), + } + if editable: + files["argument_risk_engine_editable.pth"] = f"{ROOT}\n{ROOT / 'engine'}\n".encode() + else: + for base in [ROOT / "engine" / "argument_risk_engine", ROOT / "backend"]: + for path in base.rglob("*.py"): + files[str(path.relative_to(base.parent))] = path.read_bytes() + for path, data in files.items(): + zf.writestr(path, data) + records.append(_record_line(path, data)) + record_path = f"{DIST}/RECORD" + zf.writestr(record_path, "\n".join(records + [f"{record_path},,"]) + "\n") + return filename diff --git a/data/benchmarks/mini_eval_set.jsonl b/data/benchmarks/mini_eval_set.jsonl new file mode 100644 index 0000000..93c1695 --- /dev/null +++ b/data/benchmarks/mini_eval_set.jsonl @@ -0,0 +1 @@ +{"text":"They are vermin.","expected":["dehumanizing_language"]} diff --git a/data/config/app_settings.yaml b/data/config/app_settings.yaml new file mode 100644 index 0000000..6af4e05 --- /dev/null +++ b/data/config/app_settings.yaml @@ -0,0 +1 @@ +llm_provider: deterministic diff --git a/data/config/model_profiles.yaml b/data/config/model_profiles.yaml new file mode 100644 index 0000000..4d8b7a4 --- /dev/null +++ b/data/config/model_profiles.yaml @@ -0,0 +1,4 @@ +profiles: + deterministic: + provider: deterministic + model: local-keyword diff --git a/data/examples/demo_inputs.jsonl b/data/examples/demo_inputs.jsonl new file mode 100644 index 0000000..dee9d0b --- /dev/null +++ b/data/examples/demo_inputs.jsonl @@ -0,0 +1 @@ +{"text":"Everyone always caused this problem because of that policy."} diff --git a/data/review/review_store.jsonl b/data/review/review_store.jsonl new file mode 100644 index 0000000..e69de29 diff --git a/data/taxonomy/candidate_backlog.yaml b/data/taxonomy/candidate_backlog.yaml new file mode 100644 index 0000000..f7d6df0 --- /dev/null +++ b/data/taxonomy/candidate_backlog.yaml @@ -0,0 +1 @@ +candidates: [] diff --git a/data/taxonomy/packs/starter-pack.yaml b/data/taxonomy/packs/starter-pack.yaml new file mode 100644 index 0000000..90247a8 --- /dev/null +++ b/data/taxonomy/packs/starter-pack.yaml @@ -0,0 +1,61 @@ +{ + "version": "0.1.0", + "name": "starter-pack", + "entries": [ + { + "id": "overgeneralization", + "name": "Overgeneralization", + "description": "A broad claim that applies a judgement to a group or situation without sufficient qualification.", + "severity": "medium", + "keywords": [ + "always", + "never", + "everyone", + "all", + "none" + ], + "examples": [ + "Everyone in that group is dishonest." + ], + "mitigation": "Ask for scope, counterexamples, and supporting evidence.", + "active": true, + "metadata": {} + }, + { + "id": "unsupported_causal_claim", + "name": "Unsupported causal claim", + "description": "A statement presents causation without evidence in the provided text.", + "severity": "medium", + "keywords": [ + "caused", + "because of", + "leads to", + "responsible for" + ], + "examples": [ + "The policy caused every later problem." + ], + "mitigation": "Request causal evidence and consider alternative explanations.", + "active": true, + "metadata": {} + }, + { + "id": "dehumanizing_language", + "name": "Dehumanizing language", + "description": "Language that depicts people as less than human or as pests, disease, or objects.", + "severity": "high", + "keywords": [ + "vermin", + "parasites", + "infestation", + "animals" + ], + "examples": [ + "They are vermin." + ], + "mitigation": "Escalate for careful human review and contextual assessment.", + "active": true, + "metadata": {} + } + ] +} \ No newline at end of file diff --git a/data/taxonomy/source_registry.yaml b/data/taxonomy/source_registry.yaml new file mode 100644 index 0000000..b877074 --- /dev/null +++ b/data/taxonomy/source_registry.yaml @@ -0,0 +1 @@ +sources: [] diff --git a/data/taxonomy/synonym_map.yaml b/data/taxonomy/synonym_map.yaml new file mode 100644 index 0000000..16a04a8 --- /dev/null +++ b/data/taxonomy/synonym_map.yaml @@ -0,0 +1 @@ +synonyms: {} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ce7b4b3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +services: + backend: + build: . + command: python scripts/run_backend.py + ports: ["8000:8000"] + frontend: + image: node:20-alpine + working_dir: /app/frontend + volumes: [".:/app"] + command: sh -c "npm install && npm run dev -- --host 0.0.0.0" + ports: ["5173:5173"] diff --git a/docs/annotation_guidelines.md b/docs/annotation_guidelines.md new file mode 100644 index 0000000..8b532cc --- /dev/null +++ b/docs/annotation_guidelines.md @@ -0,0 +1,3 @@ +# Annotation Guidelines + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/api_contract.md b/docs/api_contract.md new file mode 100644 index 0000000..798a842 --- /dev/null +++ b/docs/api_contract.md @@ -0,0 +1,3 @@ +# API Contract + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/dashboard_user_guide.md b/docs/dashboard_user_guide.md new file mode 100644 index 0000000..926d8cf --- /dev/null +++ b/docs/dashboard_user_guide.md @@ -0,0 +1,3 @@ +# Dashboard User Guide + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/evaluation_protocol.md b/docs/evaluation_protocol.md new file mode 100644 index 0000000..cf16e36 --- /dev/null +++ b/docs/evaluation_protocol.md @@ -0,0 +1,3 @@ +# Evaluation Protocol + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/limitations.md b/docs/limitations.md new file mode 100644 index 0000000..7ba3a30 --- /dev/null +++ b/docs/limitations.md @@ -0,0 +1,3 @@ +# Limitations + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..8ba06c7 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,3 @@ +# Roadmap + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/taxonomy_design.md b/docs/taxonomy_design.md new file mode 100644 index 0000000..abf575a --- /dev/null +++ b/docs/taxonomy_design.md @@ -0,0 +1,3 @@ +# Taxonomy Design + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/taxonomy_expansion_protocol.md b/docs/taxonomy_expansion_protocol.md new file mode 100644 index 0000000..794fd9d --- /dev/null +++ b/docs/taxonomy_expansion_protocol.md @@ -0,0 +1,3 @@ +# Taxonomy Expansion Protocol + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/docs/technical_architecture.md b/docs/technical_architecture.md new file mode 100644 index 0000000..39a3f1d --- /dev/null +++ b/docs/technical_architecture.md @@ -0,0 +1,3 @@ +# Technical Architecture + +Argument-Risk-Engine is taxonomy-first, evidence-grounded, conservative, and local-first. This document captures the MVP workflow and should be expanded as contributors add production features. diff --git a/engine/argument_risk_engine/__init__.py b/engine/argument_risk_engine/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/analyzer.py b/engine/argument_risk_engine/analyzer.py new file mode 100644 index 0000000..07af0ba --- /dev/null +++ b/engine/argument_risk_engine/analyzer.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from uuid import uuid4 + +from argument_risk_engine.classification.deterministic import classify_deterministic +from argument_risk_engine.explanation.evidence import evidence_span +from argument_risk_engine.explanation.explainer import explain +from argument_risk_engine.extraction.claim_extractor import extract_claims +from argument_risk_engine.retrieval.lexical_retriever import retrieve_candidates +from argument_risk_engine.scoring.scorer import score_risk +from argument_risk_engine.taxonomy.models import TaxonomyPack, default_taxonomy_pack + + +def analyze_text(text: str, pack: TaxonomyPack | None = None) -> dict[str, object]: + taxonomy_pack = pack or default_taxonomy_pack() + claims_out: list[dict[str, object]] = [] + all_risks: list[dict[str, object]] = [] + for claim in extract_claims(text): + candidates = retrieve_candidates(claim, taxonomy_pack) + classified = classify_deterministic(claim, candidates) + risks: list[dict[str, object]] = [] + for result in classified: + entry = next(item for item in taxonomy_pack.entries if item.id == result["taxonomy_id"]) + risk = { + "taxonomy_id": entry.id, + "name": entry.name, + "severity": entry.severity.value, + "confidence": result["confidence"], + "score": score_risk(entry.severity.value, float(result["confidence"])), + "explanation": explain(entry, list(result["matched_terms"])), + "evidence": evidence_span(text, claim), + "mitigation": entry.mitigation, + } + risks.append(risk) + all_risks.append(risk) + claims_out.append({"text": claim, "risks": risks}) + return { + "analysis_id": str(uuid4()), + "summary": { + "claim_count": len(claims_out), + "risk_count": len(all_risks), + "highest_severity": _highest_severity(all_risks), + "stance": "conservative_review_signal", + }, + "claims": claims_out, + "risks": all_risks, + } + + +def _highest_severity(risks: list[dict[str, object]]) -> str: + order = {"none": 0, "low": 1, "medium": 2, "high": 3} + highest = "none" + for risk in risks: + severity = str(risk.get("severity", "none")) + if order.get(severity, 0) > order[highest]: + highest = severity + return highest diff --git a/engine/argument_risk_engine/classification/__init__.py b/engine/argument_risk_engine/classification/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/classification/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/classification/classifier.py b/engine/argument_risk_engine/classification/classifier.py new file mode 100644 index 0000000..e69de29 diff --git a/engine/argument_risk_engine/classification/deterministic.py b/engine/argument_risk_engine/classification/deterministic.py new file mode 100644 index 0000000..9664ff3 --- /dev/null +++ b/engine/argument_risk_engine/classification/deterministic.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from argument_risk_engine.taxonomy.models import TaxonomyEntry + + +def classify_deterministic(claim: str, candidates: list[TaxonomyEntry]) -> list[dict[str, object]]: + lower = claim.lower() + results: list[dict[str, object]] = [] + for entry in candidates: + matched = [kw for kw in entry.keywords if kw.lower() in lower] + if matched: + confidence = min(0.95, 0.45 + 0.15 * len(matched)) + results.append({"taxonomy_id": entry.id, "confidence": confidence, "matched_terms": matched}) + return results diff --git a/engine/argument_risk_engine/classification/llm_client.py b/engine/argument_risk_engine/classification/llm_client.py new file mode 100644 index 0000000..a7212d9 --- /dev/null +++ b/engine/argument_risk_engine/classification/llm_client.py @@ -0,0 +1,3 @@ +class LLMClient: + def classify(self, prompt: str) -> dict[str, str]: + return {"provider": "deterministic", "response": "LLM providers are optional in the MVP."} diff --git a/engine/argument_risk_engine/classification/model_provider.py b/engine/argument_risk_engine/classification/model_provider.py new file mode 100644 index 0000000..90a7302 --- /dev/null +++ b/engine/argument_risk_engine/classification/model_provider.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class ModelProvider(BaseModel): + name: str = "deterministic" + model: str = "local-keyword" + temperature: float = 0.0 diff --git a/engine/argument_risk_engine/classification/prompts.py b/engine/argument_risk_engine/classification/prompts.py new file mode 100644 index 0000000..d3a80b1 --- /dev/null +++ b/engine/argument_risk_engine/classification/prompts.py @@ -0,0 +1 @@ +CLASSIFICATION_PROMPT = "Classify only against supplied taxonomy entries and cite evidence spans." diff --git a/engine/argument_risk_engine/classification/provider_registry.py b/engine/argument_risk_engine/classification/provider_registry.py new file mode 100644 index 0000000..d981c7d --- /dev/null +++ b/engine/argument_risk_engine/classification/provider_registry.py @@ -0,0 +1 @@ +PROVIDERS = {"deterministic": "Local deterministic keyword classifier", "openai": "Paid OpenAI-compatible provider"} diff --git a/engine/argument_risk_engine/evaluation/__init__.py b/engine/argument_risk_engine/evaluation/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/evaluation/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/evaluation/metrics.py b/engine/argument_risk_engine/evaluation/metrics.py new file mode 100644 index 0000000..e498aa4 --- /dev/null +++ b/engine/argument_risk_engine/evaluation/metrics.py @@ -0,0 +1,2 @@ +def precision(tp: int, fp: int) -> float: + return tp / (tp + fp) if tp + fp else 0.0 diff --git a/engine/argument_risk_engine/evaluation/runner.py b/engine/argument_risk_engine/evaluation/runner.py new file mode 100644 index 0000000..84290e4 --- /dev/null +++ b/engine/argument_risk_engine/evaluation/runner.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from argument_risk_engine.analyzer import analyze_text + + +def run_evaluation(path: Path | str) -> dict[str, object]: + rows = [] + p = Path(path) + if p.exists(): + rows = [json.loads(line) for line in p.read_text().splitlines() if line.strip()] + analyses = [analyze_text(row.get("text", "")) for row in rows] + return {"items": len(rows), "analyses": analyses} diff --git a/engine/argument_risk_engine/explanation/__init__.py b/engine/argument_risk_engine/explanation/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/explanation/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/explanation/evidence.py b/engine/argument_risk_engine/explanation/evidence.py new file mode 100644 index 0000000..d9e7868 --- /dev/null +++ b/engine/argument_risk_engine/explanation/evidence.py @@ -0,0 +1,6 @@ +from __future__ import annotations + + +def evidence_span(text: str, claim: str) -> dict[str, object]: + start = text.find(claim) + return {"quote": claim, "start": max(start, 0), "end": max(start, 0) + len(claim)} diff --git a/engine/argument_risk_engine/explanation/explainer.py b/engine/argument_risk_engine/explanation/explainer.py new file mode 100644 index 0000000..ce42b28 --- /dev/null +++ b/engine/argument_risk_engine/explanation/explainer.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from argument_risk_engine.taxonomy.models import TaxonomyEntry + + +def explain(entry: TaxonomyEntry, matched_terms: list[str]) -> str: + terms = ", ".join(matched_terms) if matched_terms else "taxonomy language" + return f"Matched {entry.name} because the claim contains {terms}. This is a review signal, not a truth judgement." diff --git a/engine/argument_risk_engine/extraction/__init__.py b/engine/argument_risk_engine/extraction/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/extraction/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/extraction/claim_extractor.py b/engine/argument_risk_engine/extraction/claim_extractor.py new file mode 100644 index 0000000..4acac95 --- /dev/null +++ b/engine/argument_risk_engine/extraction/claim_extractor.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import re + + +def extract_claims(text: str) -> list[str]: + pieces = re.split(r"(?<=[.!?])\s+|\n+", text.strip()) + claims = [piece.strip() for piece in pieces if len(piece.strip()) >= 8] + return claims or ([text.strip()] if text.strip() else []) diff --git a/engine/argument_risk_engine/reports/__init__.py b/engine/argument_risk_engine/reports/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/reports/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/reports/html.py b/engine/argument_risk_engine/reports/html.py new file mode 100644 index 0000000..f9b1764 --- /dev/null +++ b/engine/argument_risk_engine/reports/html.py @@ -0,0 +1,2 @@ +def render_html_report(result): + return "

Argument Risk Report

" diff --git a/engine/argument_risk_engine/reports/json_export.py b/engine/argument_risk_engine/reports/json_export.py new file mode 100644 index 0000000..5555b41 --- /dev/null +++ b/engine/argument_risk_engine/reports/json_export.py @@ -0,0 +1,5 @@ +import json + + +def render_json_report(result): + return json.dumps(result, indent=2) diff --git a/engine/argument_risk_engine/reports/markdown.py b/engine/argument_risk_engine/reports/markdown.py new file mode 100644 index 0000000..07203cc --- /dev/null +++ b/engine/argument_risk_engine/reports/markdown.py @@ -0,0 +1,2 @@ +def render_markdown_report(result: dict[str, object]) -> str: + return f"# Argument Risk Report\n\nAnalysis: {result.get('analysis_id')}\n\nRisks: {len(result.get('risks', []))}\n" diff --git a/engine/argument_risk_engine/retrieval/__init__.py b/engine/argument_risk_engine/retrieval/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/retrieval/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/retrieval/candidate_filter.py b/engine/argument_risk_engine/retrieval/candidate_filter.py new file mode 100644 index 0000000..e69de29 diff --git a/engine/argument_risk_engine/retrieval/inverted_index.py b/engine/argument_risk_engine/retrieval/inverted_index.py new file mode 100644 index 0000000..1437530 --- /dev/null +++ b/engine/argument_risk_engine/retrieval/inverted_index.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import re +from collections import defaultdict + +TOKEN_RE = re.compile(r"[a-z0-9_']+") + + +def tokenize(text: str) -> list[str]: + return TOKEN_RE.findall(text.lower()) + + +class InvertedIndex: + def __init__(self) -> None: + self.index: dict[str, set[str]] = defaultdict(set) + + def add(self, doc_id: str, text: str) -> None: + for token in tokenize(text): + self.index[token].add(doc_id) + + def search(self, text: str) -> set[str]: + matches: set[str] = set() + for token in tokenize(text): + matches.update(self.index.get(token, set())) + return matches diff --git a/engine/argument_risk_engine/retrieval/lexical_retriever.py b/engine/argument_risk_engine/retrieval/lexical_retriever.py new file mode 100644 index 0000000..74710e3 --- /dev/null +++ b/engine/argument_risk_engine/retrieval/lexical_retriever.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from argument_risk_engine.retrieval.inverted_index import tokenize +from argument_risk_engine.taxonomy.models import TaxonomyEntry, TaxonomyPack + + +def retrieve_candidates(claim: str, pack: TaxonomyPack, limit: int = 5) -> list[TaxonomyEntry]: + claim_text = claim.lower() + claim_tokens = set(tokenize(claim)) + scored: list[tuple[int, TaxonomyEntry]] = [] + for entry in pack.entries: + if not entry.active: + continue + score = 0 + for keyword in entry.keywords: + keyword_lower = keyword.lower() + if keyword_lower in claim_text: + score += 3 + score += len(set(tokenize(keyword_lower)) & claim_tokens) + if score: + scored.append((score, entry)) + scored.sort(key=lambda item: (-item[0], item[1].id)) + return [entry for _, entry in scored[:limit]] diff --git a/engine/argument_risk_engine/retrieval/retrieval_diagnostics.py b/engine/argument_risk_engine/retrieval/retrieval_diagnostics.py new file mode 100644 index 0000000..eb3325f --- /dev/null +++ b/engine/argument_risk_engine/retrieval/retrieval_diagnostics.py @@ -0,0 +1,2 @@ +def diagnostics(matches: list[object]) -> dict[str, int]: + return {"candidate_count": len(matches)} diff --git a/engine/argument_risk_engine/review/__init__.py b/engine/argument_risk_engine/review/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/review/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/review/models.py b/engine/argument_risk_engine/review/models.py new file mode 100644 index 0000000..4adc52b --- /dev/null +++ b/engine/argument_risk_engine/review/models.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + + +class ReviewFeedback(BaseModel): + analysis_id: str + taxonomy_id: str | None = None + decision: str + notes: str = "" diff --git a/engine/argument_risk_engine/review/store.py b/engine/argument_risk_engine/review/store.py new file mode 100644 index 0000000..a7ff17b --- /dev/null +++ b/engine/argument_risk_engine/review/store.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from argument_risk_engine.review.models import ReviewFeedback + + +def append_feedback(path: Path, feedback: ReviewFeedback) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a") as handle: + handle.write(json.dumps(feedback.model_dump()) + "\n") diff --git a/engine/argument_risk_engine/scoring/__init__.py b/engine/argument_risk_engine/scoring/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/scoring/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/scoring/calibration.py b/engine/argument_risk_engine/scoring/calibration.py new file mode 100644 index 0000000..16e2a9e --- /dev/null +++ b/engine/argument_risk_engine/scoring/calibration.py @@ -0,0 +1,2 @@ +def conservative_threshold() -> float: + return 0.5 diff --git a/engine/argument_risk_engine/scoring/scorer.py b/engine/argument_risk_engine/scoring/scorer.py new file mode 100644 index 0000000..be8c3d8 --- /dev/null +++ b/engine/argument_risk_engine/scoring/scorer.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +SEVERITY_WEIGHT = {"low": 1, "medium": 2, "high": 3} + + +def score_risk(severity: str, confidence: float) -> float: + return round(SEVERITY_WEIGHT.get(severity, 1) * confidence / 3, 3) diff --git a/engine/argument_risk_engine/taxonomy/__init__.py b/engine/argument_risk_engine/taxonomy/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/__init__.py @@ -0,0 +1 @@ + diff --git a/engine/argument_risk_engine/taxonomy/exporter.py b/engine/argument_risk_engine/taxonomy/exporter.py new file mode 100644 index 0000000..8f8c1e7 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/exporter.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from pathlib import Path + +from argument_risk_engine.taxonomy.importer import HEADERS +from argument_risk_engine.taxonomy.models import TaxonomyPack +from openpyxl import Workbook + + +def export_taxonomy_excel(pack: TaxonomyPack, path: Path | str) -> Path: + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + workbook = Workbook() + sheet = workbook.active + sheet.title = "taxonomy" + sheet.append(HEADERS) + for entry in pack.entries: + sheet.append([ + entry.id, + entry.name, + entry.description, + entry.severity.value, + "; ".join(entry.keywords), + "; ".join(entry.examples), + entry.mitigation, + entry.active, + ]) + workbook.save(output) + return output diff --git a/engine/argument_risk_engine/taxonomy/importer.py b/engine/argument_risk_engine/taxonomy/importer.py new file mode 100644 index 0000000..89f2a5c --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/importer.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from pathlib import Path + +from argument_risk_engine.taxonomy.models import RiskSeverity, TaxonomyEntry, TaxonomyPack +from openpyxl import load_workbook + +HEADERS = ["id", "name", "description", "severity", "keywords", "examples", "mitigation", "active"] + + +def import_taxonomy_excel(path: Path | str) -> TaxonomyPack: + workbook = load_workbook(Path(path)) + sheet = workbook.active + headers = [str(cell.value).strip() if cell.value is not None else "" for cell in next(sheet.iter_rows(max_row=1))] + index = {header: idx for idx, header in enumerate(headers)} + entries: list[TaxonomyEntry] = [] + for row in sheet.iter_rows(min_row=2, values_only=True): + if not any(row): + continue + data = {header: row[index[header]] if header in index and index[header] < len(row) else None for header in HEADERS} + entries.append( + TaxonomyEntry( + id=str(data["id"] or "").strip(), + name=str(data["name"] or "").strip(), + description=str(data["description"] or "").strip(), + severity=RiskSeverity(str(data.get("severity") or "low").strip().lower()), + keywords=[part.strip() for part in str(data.get("keywords") or "").split(";") if part.strip()], + examples=[part.strip() for part in str(data.get("examples") or "").split(";") if part.strip()], + mitigation=str(data.get("mitigation") or "Escalate for human review."), + active=str(data.get("active") or "true").strip().lower() not in {"false", "0", "no"}, + ) + ) + return TaxonomyPack(name=Path(path).stem, entries=entries) diff --git a/engine/argument_risk_engine/taxonomy/indexer.py b/engine/argument_risk_engine/taxonomy/indexer.py new file mode 100644 index 0000000..31e7330 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/indexer.py @@ -0,0 +1,2 @@ +def build_index(pack): + return {entry.id: entry for entry in pack.entries} diff --git a/engine/argument_risk_engine/taxonomy/loader.py b/engine/argument_risk_engine/taxonomy/loader.py new file mode 100644 index 0000000..7ad2ea9 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/loader.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pathlib import Path + +import yaml +from argument_risk_engine.taxonomy.models import ( + RiskSeverity, + TaxonomyEntry, + TaxonomyPack, + default_taxonomy_pack, +) + + +def load_taxonomy_pack(path: Path | str | None = None) -> TaxonomyPack: + if path is None: + return default_taxonomy_pack() + file_path = Path(path) + if not file_path.exists(): + return default_taxonomy_pack() + data = yaml.safe_load(file_path.read_text()) + if not data: + return default_taxonomy_pack() + entries = [] + for entry in data.get("entries", []): + if isinstance(entry, TaxonomyEntry): + entries.append(entry) + else: + item = dict(entry) + item["severity"] = RiskSeverity(str(item.get("severity", "low"))) + entries.append(TaxonomyEntry(**item)) + return TaxonomyPack( + version=str(data.get("version", "0.1.0")), + name=str(data.get("name", "default")), + entries=entries, + ) + + +def save_taxonomy_pack(pack: TaxonomyPack, path: Path | str) -> None: + file_path = Path(path) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.write_text(yaml.safe_dump(pack.model_dump(mode="json"), sort_keys=False)) diff --git a/engine/argument_risk_engine/taxonomy/models.py b/engine/argument_risk_engine/taxonomy/models.py new file mode 100644 index 0000000..1d28bb1 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/models.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field, field_validator + + +class RiskSeverity(str, Enum): + low = "low" + medium = "medium" + high = "high" + + +class TaxonomyEntry(BaseModel): + id: str + name: str + description: str + severity: RiskSeverity = RiskSeverity.low + keywords: list[str] = Field(default_factory=list) + examples: list[str] = Field(default_factory=list) + mitigation: str = "Escalate for human review." + active: bool = True + metadata: dict[str, Any] = Field(default_factory=dict) + + @field_validator("id", "name", "description") + @classmethod + def required_text(cls, value: str) -> str: + if not value or not value.strip(): + raise ValueError("field must be non-empty") + return value.strip() + + @field_validator("keywords", "examples") + @classmethod + def clean_strings(cls, values: list[str]) -> list[str]: + return [item.strip() for item in values if item and item.strip()] + + +class TaxonomyPack(BaseModel): + version: str = "0.1.0" + name: str = "default" + entries: list[TaxonomyEntry] = Field(default_factory=list) + + +def default_taxonomy_pack() -> TaxonomyPack: + return TaxonomyPack( + name="starter-pack", + entries=[ + TaxonomyEntry( + id="overgeneralization", + name="Overgeneralization", + description="A broad claim that applies a judgement to a group or situation without sufficient qualification.", + severity=RiskSeverity.medium, + keywords=["always", "never", "everyone", "all", "none"], + examples=["Everyone in that group is dishonest."], + mitigation="Ask for scope, counterexamples, and supporting evidence.", + ), + TaxonomyEntry( + id="unsupported_causal_claim", + name="Unsupported causal claim", + description="A statement presents causation without evidence in the provided text.", + severity=RiskSeverity.medium, + keywords=["caused", "because of", "leads to", "responsible for"], + examples=["The policy caused every later problem."], + mitigation="Request causal evidence and consider alternative explanations.", + ), + TaxonomyEntry( + id="dehumanizing_language", + name="Dehumanizing language", + description="Language that depicts people as less than human or as pests, disease, or objects.", + severity=RiskSeverity.high, + keywords=["vermin", "parasites", "infestation", "animals"], + examples=["They are vermin."], + mitigation="Escalate for careful human review and contextual assessment.", + ), + ], + ) diff --git a/engine/argument_risk_engine/taxonomy/pack_manager.py b/engine/argument_risk_engine/taxonomy/pack_manager.py new file mode 100644 index 0000000..9ee3b65 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/pack_manager.py @@ -0,0 +1,5 @@ +from argument_risk_engine.taxonomy.models import default_taxonomy_pack + + +def active_pack(): + return default_taxonomy_pack() diff --git a/engine/argument_risk_engine/taxonomy/quality_audit.py b/engine/argument_risk_engine/taxonomy/quality_audit.py new file mode 100644 index 0000000..5dfad0c --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/quality_audit.py @@ -0,0 +1,2 @@ +def audit_pack(pack): + return {"entry_count": len(pack.entries), "active_count": sum(1 for e in pack.entries if e.active)} diff --git a/engine/argument_risk_engine/taxonomy/source_registry.py b/engine/argument_risk_engine/taxonomy/source_registry.py new file mode 100644 index 0000000..9672e96 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/source_registry.py @@ -0,0 +1,2 @@ +def load_sources(): + return [] diff --git a/engine/argument_risk_engine/taxonomy/synonym_map.py b/engine/argument_risk_engine/taxonomy/synonym_map.py new file mode 100644 index 0000000..e9e2466 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/synonym_map.py @@ -0,0 +1,2 @@ +def load_synonyms(): + return {} diff --git a/engine/argument_risk_engine/taxonomy/validator.py b/engine/argument_risk_engine/taxonomy/validator.py new file mode 100644 index 0000000..6a96015 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/validator.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from argument_risk_engine.taxonomy.models import TaxonomyPack + + +def validate_taxonomy_pack(pack: TaxonomyPack) -> list[str]: + errors: list[str] = [] + seen: set[str] = set() + for entry in pack.entries: + if entry.id in seen: + errors.append(f"duplicate id: {entry.id}") + seen.add(entry.id) + if not entry.keywords: + errors.append(f"{entry.id} has no keywords") + return errors diff --git a/engine/argument_risk_engine/taxonomy/versioning.py b/engine/argument_risk_engine/taxonomy/versioning.py new file mode 100644 index 0000000..ddf42f5 --- /dev/null +++ b/engine/argument_risk_engine/taxonomy/versioning.py @@ -0,0 +1,2 @@ +def next_version(version: str) -> str: + return version diff --git a/fastapi/__init__.py b/fastapi/__init__.py new file mode 100644 index 0000000..5a93284 --- /dev/null +++ b/fastapi/__init__.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +class Response: + def __init__(self, content='', media_type='text/plain', status_code=200): + self.content = content + self.media_type = media_type + self.status_code = status_code + +class APIRouter: + def __init__(self, prefix='', tags=None): + self.prefix = prefix + self.routes = {} + def get(self, path='', **kwargs): + def deco(fn): + self.routes[('GET', self.prefix + path)] = fn + return fn + return deco + def post(self, path='', **kwargs): + def deco(fn): + self.routes[('POST', self.prefix + path)] = fn + return fn + return deco + def put(self, path='', **kwargs): + def deco(fn): + self.routes[('PUT', self.prefix + path)] = fn + return fn + return deco + +class FastAPI: + def __init__(self, **kwargs): + self.routes = {} + def add_middleware(self, *args, **kwargs): + return None + def include_router(self, router, prefix=''): + for (method, path), fn in router.routes.items(): + self.routes[(method, prefix + path)] = fn + def get(self, path='', **kwargs): + def deco(fn): + self.routes[('GET', path)] = fn + return fn + return deco diff --git a/fastapi/middleware/__init__.py b/fastapi/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi/middleware/cors.py b/fastapi/middleware/cors.py new file mode 100644 index 0000000..9543402 --- /dev/null +++ b/fastapi/middleware/cors.py @@ -0,0 +1,2 @@ +class CORSMiddleware: + pass diff --git a/fastapi/testclient/__init__.py b/fastapi/testclient/__init__.py new file mode 100644 index 0000000..d39e37f --- /dev/null +++ b/fastapi/testclient/__init__.py @@ -0,0 +1,47 @@ +from __future__ import annotations +import json + +class _Resp: + def __init__(self, payload, status_code=200): + self._payload = payload + self.status_code = status_code + def json(self): + def conv(v): + if hasattr(v, 'model_dump'): + return v.model_dump() + if isinstance(v, dict): + return {k: conv(i) for k, i in v.items()} + if isinstance(v, list): + return [conv(i) for i in v] + if hasattr(v, 'value'): + return v.value + return v + return conv(self._payload) + +class TestClient: + def __init__(self, app): + self.app = app + def get(self, path): + fn = self.app.routes.get(('GET', path)) + return _Resp(fn() if fn else {'detail':'not found'}, 200 if fn else 404) + def post(self, path, json=None): + fn = self.app.routes.get(('POST', path)) + if not fn: return _Resp({'detail':'not found'}, 404) + arg = _make_arg(fn, json or {}) + return _Resp(fn(arg) if arg is not None else fn()) + def put(self, path, json=None): + fn = self.app.routes.get(('PUT', path)) + if not fn: return _Resp({'detail':'not found'}, 404) + arg = _make_arg(fn, json or {}) + return _Resp(fn(arg) if arg is not None else fn()) + +def _make_arg(fn, data): + anns = getattr(fn, '__annotations__', {}) + params = [k for k in anns if k != 'return'] + if not params: + return None + cls = anns[params[0]] + try: + return cls(**data) + except Exception: + return data diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6f14d63 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1 @@ +Argument-Risk-Engine
diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..eeb8603 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "argument-risk-engine-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "argument-risk-engine-frontend", + "version": "0.1.0", + "devDependencies": {} + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..8c86959 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,13 @@ +{ + "name": "argument-risk-engine-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "node scripts/dev_server.mjs", + "build": "node scripts/build_frontend.mjs", + "preview": "node scripts/dev_server.mjs" + }, + "dependencies": {}, + "devDependencies": {} +} diff --git a/frontend/scripts/build_frontend.mjs b/frontend/scripts/build_frontend.mjs new file mode 100644 index 0000000..3870943 --- /dev/null +++ b/frontend/scripts/build_frontend.mjs @@ -0,0 +1,9 @@ +import { mkdirSync, copyFileSync, writeFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +const root = dirname(dirname(fileURLToPath(import.meta.url))) +const dist = join(root, 'dist') +mkdirSync(dist, { recursive: true }) +copyFileSync(join(root, 'index.html'), join(dist, 'index.html')) +writeFileSync(join(dist, 'app.js'), 'console.log("Argument-Risk-Engine dashboard build");\n') +console.log('Built frontend MVP into dist/') diff --git a/frontend/scripts/dev_server.mjs b/frontend/scripts/dev_server.mjs new file mode 100644 index 0000000..2cac789 --- /dev/null +++ b/frontend/scripts/dev_server.mjs @@ -0,0 +1,14 @@ +import http from 'node:http' +import { readFileSync, existsSync } from 'node:fs' +import { extname, join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +const root = dirname(dirname(fileURLToPath(import.meta.url))) +const types = { '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.tsx': 'application/javascript', '.ts': 'application/javascript' } +const server = http.createServer((req, res) => { + let path = req.url === '/' ? '/index.html' : req.url.split('?')[0] + let file = join(root, path) + if (!existsSync(file)) file = join(root, 'index.html') + res.writeHead(200, { 'Content-Type': types[extname(file)] || 'text/plain' }) + res.end(readFileSync(file)) +}) +server.listen(5173, '127.0.0.1', () => console.log('Frontend: http://localhost:5173')) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..82f2e56 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,12 @@ +import { AnalyzePage } from './components/analyze/AnalyzePage' +import { AppShell } from './components/layout/AppShell' +import { EvaluationPage } from './components/evaluation/EvaluationPage' +import { ReportsPage } from './components/reports/ReportsPage' +import { ReviewPage } from './components/review/ReviewPage' +import { ModelSettingsPage } from './components/settings/ModelSettingsPage' +import { TaxonomyPage } from './components/taxonomy/TaxonomyPage' +import { TaxonomyWorkbenchPage } from './components/taxonomy_workbench/TaxonomyWorkbenchPage' + +export default function App() { + return
+} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts new file mode 100644 index 0000000..65c2580 --- /dev/null +++ b/frontend/src/api/client.ts @@ -0,0 +1,20 @@ +import type { AnalysisResponse, TaxonomyEntry } from './types' + +const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8000/api' + +export async function analyzeText(text: string): Promise { + const response = await fetch(`${API_BASE}/analysis/analyze`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text }), + }) + if (!response.ok) throw new Error('Analysis request failed') + return response.json() +} + +export async function fetchTaxonomy(): Promise { + const response = await fetch(`${API_BASE}/taxonomy`) + if (!response.ok) throw new Error('Taxonomy request failed') + const payload = await response.json() + return payload.entries +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..9f5bd12 --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,5 @@ +export type AnalysisRequest = { text: string } +export type Risk = { taxonomy_id: string; name: string; severity: string; confidence: number; score: number; explanation: string; evidence: { quote: string; start: number; end: number }; mitigation: string } +export type Claim = { text: string; risks: Risk[] } +export type AnalysisResponse = { analysis_id: string; summary: Record; claims: Claim[]; risks: Risk[] } +export type TaxonomyEntry = { id: string; name: string; description: string; severity: string; keywords: string[]; active: boolean } diff --git a/frontend/src/components/analyze/AnalysisReport.tsx b/frontend/src/components/analyze/AnalysisReport.tsx new file mode 100644 index 0000000..3f8c86d --- /dev/null +++ b/frontend/src/components/analyze/AnalysisReport.tsx @@ -0,0 +1,3 @@ +import type { AnalysisResponse } from '../../api/types' +import { ClaimCard } from './ClaimCard' +export function AnalysisReport({ result }: { result: AnalysisResponse }) { return

Report

Analysis ID: {result.analysis_id}

Risks: {String(result.summary.risk_count ?? 0)}

{result.claims.map((claim, idx) => )}
} diff --git a/frontend/src/components/analyze/AnalyzePage.tsx b/frontend/src/components/analyze/AnalyzePage.tsx new file mode 100644 index 0000000..2af9a8e --- /dev/null +++ b/frontend/src/components/analyze/AnalyzePage.tsx @@ -0,0 +1,15 @@ +import { useState } from 'react' +import { analyzeText } from '../../api/client' +import type { AnalysisResponse } from '../../api/types' +import { Card } from '../shared/Card' +import { ErrorState } from '../shared/ErrorState' +import { AnalysisReport } from './AnalysisReport' +import { TextInputPanel } from './TextInputPanel' + +export function AnalyzePage() { + const [text, setText] = useState('Everyone always caused this problem because of that policy.') + const [result, setResult] = useState(null) + const [error, setError] = useState('') + async function run() { setError(''); try { setResult(await analyzeText(text)) } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error') } } + return

Analyze

{error && }{result && }
+} diff --git a/frontend/src/components/analyze/ClaimCard.tsx b/frontend/src/components/analyze/ClaimCard.tsx new file mode 100644 index 0000000..2cbe055 --- /dev/null +++ b/frontend/src/components/analyze/ClaimCard.tsx @@ -0,0 +1,3 @@ +import type { Claim } from '../../api/types' +import { RiskCard } from './RiskCard' +export function ClaimCard({ claim }: { claim: Claim }) { return

{claim.text}

{claim.risks.length ? claim.risks.map(risk => ) :

No taxonomy match.

}
} diff --git a/frontend/src/components/analyze/EvidenceHighlight.tsx b/frontend/src/components/analyze/EvidenceHighlight.tsx new file mode 100644 index 0000000..5793726 --- /dev/null +++ b/frontend/src/components/analyze/EvidenceHighlight.tsx @@ -0,0 +1 @@ +export function EvidenceHighlight({ quote }: { quote: string }) { return {quote} } diff --git a/frontend/src/components/analyze/ExportButtons.tsx b/frontend/src/components/analyze/ExportButtons.tsx new file mode 100644 index 0000000..06283c0 --- /dev/null +++ b/frontend/src/components/analyze/ExportButtons.tsx @@ -0,0 +1 @@ +export function ExportButtons() { return
} diff --git a/frontend/src/components/analyze/RiskCard.tsx b/frontend/src/components/analyze/RiskCard.tsx new file mode 100644 index 0000000..076af4f --- /dev/null +++ b/frontend/src/components/analyze/RiskCard.tsx @@ -0,0 +1,3 @@ +import type { Risk } from '../../api/types' +import { Badge } from '../shared/Badge' +export function RiskCard({ risk }: { risk: Risk }) { return
{risk.severity}{risk.name}

{risk.explanation}

Evidence: “{risk.evidence.quote}”
} diff --git a/frontend/src/components/analyze/TextInputPanel.tsx b/frontend/src/components/analyze/TextInputPanel.tsx new file mode 100644 index 0000000..76e85e1 --- /dev/null +++ b/frontend/src/components/analyze/TextInputPanel.tsx @@ -0,0 +1,4 @@ +import { Button } from '../shared/Button' +export function TextInputPanel({ text, setText, onAnalyze }: { text: string; setText: (value: string) => void; onAnalyze: () => void }) { + return