Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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=
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.env
node_modules/
frontend/node_modules/
frontend/dist/
*.egg-info/
.DS_Store
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## 0.1.0
- Initial local MVP scaffold for taxonomy-grounded argument risk analysis.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 28 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <http://localhost:8000>, starts the Vite dashboard at <http://localhost:5173>, 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.
Binary file not shown.
1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

1 change: 1 addition & 0 deletions backend/app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

9 changes: 9 additions & 0 deletions backend/app/api/routes_analysis.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 8 additions & 0 deletions backend/app/api/routes_evaluation.py
Original file line number Diff line number Diff line change
@@ -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()
8 changes: 8 additions & 0 deletions backend/app/api/routes_reports.py
Original file line number Diff line number Diff line change
@@ -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")
10 changes: 10 additions & 0 deletions backend/app/api/routes_review.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 13 additions & 0 deletions backend/app/api/routes_settings.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions backend/app/api/routes_taxonomy.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions backend/app/api/routes_taxonomy_workbench.py
Original file line number Diff line number Diff line change
@@ -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)}
1 change: 1 addition & 0 deletions backend/app/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

15 changes: 15 additions & 0 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 5 additions & 0 deletions backend/app/core/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import logging


def configure_logging():
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
7 changes: 7 additions & 0 deletions backend/app/core/paths.py
Original file line number Diff line number Diff line change
@@ -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"
35 changes: 35 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -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"}
1 change: 1 addition & 0 deletions backend/app/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

13 changes: 13 additions & 0 deletions backend/app/schemas/analysis.py
Original file line number Diff line number Diff line change
@@ -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]]
5 changes: 5 additions & 0 deletions backend/app/schemas/evaluation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class EvaluationResponse(BaseModel):
items: int
5 changes: 5 additions & 0 deletions backend/app/schemas/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class ReportResponse(BaseModel):
content: str
Empty file added backend/app/schemas/review.py
Empty file.
7 changes: 7 additions & 0 deletions backend/app/schemas/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pydantic import BaseModel


class AppSettings(BaseModel):
llm_provider: str = "deterministic"
model: str = "local-keyword"
temperature: float = 0.0
7 changes: 7 additions & 0 deletions backend/app/schemas/taxonomy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from argument_risk_engine.taxonomy.models import TaxonomyEntry

from pydantic import BaseModel


class TaxonomyListResponse(BaseModel):
entries: list[TaxonomyEntry]
6 changes: 6 additions & 0 deletions backend/app/schemas/taxonomy_workbench.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class TaxonomyQualityResponse(BaseModel):
errors: list[str]
entry_count: int
1 change: 1 addition & 0 deletions backend/app/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

7 changes: 7 additions & 0 deletions backend/app/services/analyzer_service.py
Original file line number Diff line number Diff line change
@@ -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())
7 changes: 7 additions & 0 deletions backend/app/services/evaluation_service.py
Original file line number Diff line number Diff line change
@@ -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")
7 changes: 7 additions & 0 deletions backend/app/services/report_service.py
Original file line number Diff line number Diff line change
@@ -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."))
9 changes: 9 additions & 0 deletions backend/app/services/review_service.py
Original file line number Diff line number Diff line change
@@ -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"}
11 changes: 11 additions & 0 deletions backend/app/services/settings_service.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions backend/app/services/taxonomy_service.py
Original file line number Diff line number Diff line change
@@ -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)
Loading