Skip to content
Closed
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
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ EU Fact Force is a collaborative platform developed by [EUPHA](https://www.eupha
### Use Case

> **Marie**, a health communicator at a national public health association, sees a viral post claiming "vaccines cause autism." She needs to respond quickly with solid evidence.
>
>
> She searches **"vaccines autism"** on EU Fact Force and immediately sees:
> - An interactive graph showing 15+ peer-reviewed articles that refute this claim
> - The scientific consensus: **"Refuted with high confidence"**
> - Current disinformation trends: 1,200 mentions this week, peak in France/Belgium
> - Key evidence to cite in her response
>
>
> **Time to find relevant evidence: <30 seconds**

## Key Features
Expand Down Expand Up @@ -112,7 +112,7 @@ uv run pytest

### Déploiement de l'application

L'application se compose d'un serveur Django, d'une base PostgreSQL (avec pgvector) et de LocalStack pour le stockage S3.
L'application se compose d'un serveur Django, d'une base PostgreSQL (avec pgvector) et de LocalStack pour le stockage S3.
Pour déployer et utiliser l'application en local :

**1. Prérequis**
Expand Down Expand Up @@ -176,3 +176,44 @@ AWS_S3_REGION_NAME=eu-west-1
```

Sans ces variables, l'application utilise le stockage fichier local par défaut.


**7. Démarrer la web-app d'ingestion vers le S3 local**

Pour rapatrier l'upload d'un couple PDF/métadatas :

***Lancer le containeur Docker***
```bash
docker compose up -d
```

Cela démarre PostgreSQL (port 5432) et LocalStack S3 (port 4566) et écoute sur
ce port.
Le bucket configuré est créé automatiquement au démarrage de LocalStack.

***Installer les dépendances et appliquer les migrations***

```bash
uv sync
uv run python manage.py migrate
```

***Lancer le pipeline Dash-app > API > Localstack***

***Démarrer le serveur Django :***

```bash
uv run python manage.py runserver
```

***Démarrer le script FastAPI***

```bash
uv run python ingestion/front_upload/api_front_upload.py
```

***Démarrer la webapp Dash***

```bash
uv run python ingestion/front_upload/api_front_upload.py
```
30 changes: 16 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,22 @@ services:
retries: 5

localstack:
image: localstack/localstack:latest
ports:
- 4566
environment:
SERVICES: s3
PERSISTENCE: 1
AWS_DEFAULT_REGION: ${AWS_S3_REGION_NAME:-eu-west-1}
AWS_STORAGE_BUCKET_NAME: ${AWS_STORAGE_BUCKET_NAME:-eu-fact-force-files}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test}
DEBUG: ${DEBUG:-0}
volumes:
- ./s3:/var/lib/localstack
- ./docker/localstack/init-ready.d:/etc/localstack/init/ready.d:ro
image: localstack/localstack:3.3 #latest conflicted w/ FastAPI
ports:
- "4566:4566" #required listening port for FastAPI
environment:
SERVICES: s3
PERSISTENCE: 1
AWS_DEFAULT_REGION: ${AWS_S3_REGION_NAME:-eu-west-1}
AWS_STORAGE_BUCKET_NAME: ${AWS_STORAGE_BUCKET_NAME:-eu-fact-force-files}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-test}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-test}
DEBUG: ${DEBUG:-0}
LOCALSTACK_ACKNOWLEDGE_ACCOUNT_REQUIREMENT: 1 #required licensing
volumes:
- ./s3:/var/lib/localstack
- ./docker/localstack/init-ready.d:/etc/localstack/init/ready.d:ro
- /var/run/docker.sock:/var/run/docker.sock

volumes:
postgres_data:
95 changes: 95 additions & 0 deletions eu_fact_force/dash-app/api/upload_to_s3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import json
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from botocore.client import Config
from fastapi.middleware.cors import CORSMiddleware
import boto3
import uvicorn
from dotenv import load_dotenv

# 1. Environment var loading
load_dotenv()

app = FastAPI(title="EUFactForce API")

# 2. Dash-app URL
origins = [
"http://localhost:8050",
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# 3. S3 instancing
s3_client = boto3.client(
"s3",
endpoint_url=os.getenv("AWS_S3_ENDPOINT_URL"),
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
region_name=os.getenv("AWS_REGION"),
config=Config(s3={'addressing_style': 'path'}) # <-- Ajoute cette ligne impérativement
)

BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")

# API print debugging
try:
s3_client.create_bucket(Bucket=BUCKET_NAME)
print(f"Bucket '{BUCKET_NAME}' créé ou déjà existant.")
except Exception as e:
print(f"Note: Le bucket existe peut-être déjà : {e}")

@app.get("/")
async def root():
return {"message": "API EUFactForce opérationnelle"}

# 4. Upload routine
@app.post("/upload/")
async def upload_file(
file: UploadFile = File(...),
metadata: str = Form(...)
):
try:
# filename cleanup
filename = file.filename
json_filename = f"{os.path.splitext(filename)[0]}.json"

# A. PDF upload on S3
file_content = await file.read()
s3_client.put_object(
Bucket=BUCKET_NAME,
Key=filename,
Body=file_content,
ContentType="application/pdf"
)

# B. JSON Metadatas S3 upload
# Json type check
try:
json_data = json.loads(metadata)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Métadonnées JSON invalides")

s3_client.put_object(
Bucket=BUCKET_NAME,
Key=json_filename,
Body=json.dumps(json_data),
ContentType="application/json"
)

return {
"status": "success",
"message": f"Fichiers {filename} et {json_filename} téléchargés avec succès."
}

except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
# Uvicorn server exposition on 8001
uvicorn.run(app, host="0.0.0.0", port=8001)
48 changes: 40 additions & 8 deletions eu_fact_force/dash-app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import plotly.io as pio
import plotly.graph_objects as go

# PDF ingestion
import base64
import io
import json
from pathlib import Path
import requests
import uuid

from utils.colors import EUPHAColors
Expand All @@ -16,7 +19,7 @@
from pages import readme, ingest, graph

# Plotly template
with open("assets/template.json", "r") as f:
with open(Path(__file__).parent / "assets/template.json", "r") as f:
debate_template = json.load(f)
pio.templates["app_template"] = go.layout.Template(debate_template)
pio.templates.default = "app_template"
Expand Down Expand Up @@ -368,6 +371,9 @@ def lock_authors(is_correct, ids):
@app.callback(
Output('final-output', 'children'),
Input('btn-final-upload', 'n_clicks'),
# AJOUT DES DEUX STATES MANQUANTS ICI
State('upload-pdf', 'contents'),
State('upload-pdf', 'filename'),
State('input-doi', 'value'),
State('input-abstract', 'value'),
State('input-journal', 'value'),
Expand All @@ -381,14 +387,20 @@ def lock_authors(is_correct, ids):
State({'type': 'auth-email', 'index': ALL}, 'value'),
prevent_initial_call=True
)
def finalize_and_display_json(n_clicks, doi, abstract, journal, date, link, category, study_type, title, names, surnames, emails):
def finalize_and_send(n_clicks, pdf_base64, filename, doi, abstract, journal, date, link, category, study_type, title, names, surnames, emails):
# Dash vérifie maintenant que pdf_base64 reçoit bien 'contents' et filename reçoit 'filename'

if not n_clicks or pdf_base64 is None:
return no_update

print(f"Tentative d'envoi pour : {filename}") # DEBUG CLI

authors_list = [
{"name": n, "surname": s, "email": e}
for n, s, e in zip(names, surnames, emails) if n or s
]

metadata_json = {
metadata_payload = {
"title": title,
"category": category,
"study_type": study_type,
Expand All @@ -400,11 +412,31 @@ def finalize_and_display_json(n_clicks, doi, abstract, journal, date, link, cate
"authors": authors_list
}

return html.Div([
dbc.Alert("Successfully contributed, thank you!", color="success"),
html.H4("Metadata JSON"),
html.Pre(json.dumps(metadata_json, indent=4), style={'backgroundColor': '#f8f9fa', 'padding': '15px', 'borderRadius': '8px', 'border': '1px solid #dee2e6'})
])
try:
# Décodage propre
content_type, content_string = pdf_base64.split(',')
pdf_bytes = base64.b64decode(content_string)

url = "http://localhost:8001/upload/"

files = {
'file': (filename, pdf_bytes, 'application/pdf')
}
data = {
'metadata': json.dumps(metadata_payload)
}

# Timeout ajouté pour éviter que Dash ne freeze si FastAPI est éteint
response = requests.post(url, files=files, data=data, timeout=10)

if response.status_code == 200:
return dbc.Alert(f"Succès ! {filename} est sur S3.", color="success")
else:
return dbc.Alert(f"Erreur API : {response.text}", color="danger")

except Exception as e:
print(f"Erreur détaillée : {str(e)}") # Visible dans ton terminal Dash
return dbc.Alert(f"Erreur lors de l'envoi : {str(e)}", color="danger")

if __name__ == "__main__":
app.run(debug=True)
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ dependencies = [
"dash-bootstrap-components>=2.0.4",
"dash-cytoscape>=1.0.2",
"pymupdf>=1.27.1",
"fastapi>=0.135.2",
"uvicorn>=0.42.0",
"python-multipart>=0.0.22",
]

[tool.pytest.ini_options]
Expand Down
Loading