Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Pour tests performance K6
TEST_PERFORMANCE=false

# Configuration LLM et limite inférence
LLM_MODEL=mistral
LLM_TEMPERATURE=0.3
Expand Down
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,6 @@ Se résume à un seul script à lancer
$ python -m app [--debug]
```

Ancienne version :

```bash
$ python main_agent.py
```

ou pour obtenir plus d'informations

```bash
$ python main_agent.py --debug
```

## Tests

```bash
Expand All @@ -130,10 +118,18 @@ $ pytest tests/test_send_articles_email.py -v
## Interface UI pour les articles résumés

```bash
$ uvicorn web:app --reload
$ uvicorn web:app --loop asyncio [--reload]
```

Sur http://127.0.0.1:8000/
avec multi workers

```bash
$ uvicorn web:app --workers $(nproc) --loop asyncio [--reload]
```

`--loop asyncio` dépend de l'environnement d'exécution

L'application est accessible sur http://127.0.0.1:8000/

## L'automate exécuté

Expand Down Expand Up @@ -168,3 +164,9 @@ Les branches doivent suivre le format :
- `chore/` pour des tâches de maintenance.

Exemple : `feat/implement-source-weights`

## Performances tests K6

voir [README](tests/k6/README.md)


28 changes: 28 additions & 0 deletions add_latency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# add_latency.py
# Ajouter ce middleware à votre FastAPI pour simuler des conditions réelles

import asyncio
import random
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware

class LatencySimulatorMiddleware(BaseHTTPMiddleware):
"""
Simule la latence d'une base de données ou d'un service externe
pour rendre les tests plus réalistes
"""
async def dispatch(self, request: Request, call_next):
# Simuler une latence réseau/DB réaliste
# 10-50ms pour simuler une DB PostgreSQL sur le réseau
latency = random.uniform(0.01, 0.05) # 10-50ms

# Pour simuler des appels API externes lents (optionnel)
# latency = random.uniform(0.1, 0.3) # 100-300ms

await asyncio.sleep(latency)

response = await call_next(request)
return response

# Dans votre web.py, ajoutez :
# app.add_middleware(LatencySimulatorMiddleware)
10 changes: 8 additions & 2 deletions app/db/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from .db import init_db, save_to_db, DB_PATH
from .db import read_articles
from .db import read_articles_sync, read_articles_async

__all__ = ["init_db", "save_to_db", "DB_PATH", "read_articles"]
__all__ = [
"init_db",
"save_to_db",
"DB_PATH",
"read_articles_sync",
"read_articles_async",
]
78 changes: 60 additions & 18 deletions app/db/db.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import os
import logging
from sqlalchemy import create_engine, text
from sqlalchemy import Column, Integer, String, Text, DateTime, Index
from sqlalchemy import create_engine, select, text
from sqlalchemy import Column, Integer, String, Text, DateTime
from sqlalchemy.sql import func
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import event
from sqlalchemy import Enum as SQLAlchemyEnum
from contextlib import contextmanager

# async support
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
from contextlib import asynccontextmanager

from dotenv import load_dotenv
from datetime import datetime, timezone

Expand All @@ -17,13 +23,11 @@
load_dotenv()
DB_PATH = os.getenv("DB_PATH", "techno-watch.db")

# from core import logger
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

Base = declarative_base()


#
# Modèles
# /!\ Après le declarative_base()
Expand Down Expand Up @@ -156,7 +160,7 @@ def bulk_insert(cls, session, articles):
)

@classmethod
def search(cls, session, query, date_min=None, date_max=None, limit=10):
async def search(cls, session, query, date_min=None, date_max=None, limit=10):
"""
Recherche plein texte dans les articles avec filtre optionnel par date.
avec score basé sur rank().
Expand Down Expand Up @@ -222,14 +226,13 @@ def search(cls, session, query, date_min=None, date_max=None, limit=10):
"""

logger.info(f"SQL exécuté: {base_sql} avec {params}")

results = session.execute(text(base_sql), params)
results = await session.execute(text(base_sql), params)
results_as_dict = results.mappings().all()

logger.debug(f"results : {results_as_dict}")

return results_as_dict
# return results.fetchall()
return results_as_dict

@classmethod
def search_with_bm25(cls, session, query, date_min=None, date_max=None, limit=10):
Expand Down Expand Up @@ -292,16 +295,29 @@ def search_with_bm25(cls, session, query, date_min=None, date_max=None, limit=10

#
# Configuration SQL Alchemy
# sync et async
#
SQLALCHEMY_DB = f"sqlite:///{DB_PATH}"
engine = create_engine(
SQLALCHEMY_DB,
pool_size=20, # Nombre de connexions par worker
max_overflow=10, # Connexions supplémentaires si besoin
pool_pre_ping=True, # Vérifier que la connexion est vivante
connect_args={
"check_same_thread": False
}, # Nécessaire pour SQLite avec FastAPI et le multi-threads
echo=True, # Affiche les requêtes SQL (optionnel, pour le debug))
# echo=True, # Affiche les requêtes SQL (optionnel, pour le debug))
)
async_engine = create_async_engine(
SQLALCHEMY_DB.replace("sqlite:///", "sqlite+aiosqlite:///"),
pool_size=20, # Nombre de connexions par worker
max_overflow=10, # Connexions supplémentaires si besoin
pool_pre_ping=True, # Vérifier que la connexion est vivante
connect_args={
"check_same_thread": False
}, # Nécessaire pour SQLite avec FastAPI et le multi-threads
# echo=True, # Affiche les requêtes SQL (optionnel, pour le debug))
)


def init_db():
"""Initialise la base de données SQLite."""
Expand All @@ -312,9 +328,15 @@ def init_db():

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

AsyncSessionLocal = sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False
)

#
# Fonctions utilitaires DB
# sync et async
#
@contextmanager
def get_db():
Expand All @@ -325,18 +347,38 @@ def get_db():
finally:
session.close()

@asynccontextmanager
async def get_db_async():
"""Générateur de session SQLAlchemy asynchrone avec gestion automatique pour le close()."""
session = AsyncSessionLocal()
try:
yield session
finally:
await session.close()

async def read_articles_async(date: str = None):
"""Lit les articles résumés qui ont été retenus pour la veille techno"""
async with get_db_async() as session:
if date:
stmt = select(Article).filter(Article.published.like(f"%{date}%"))
else:
stmt = select(Article).order_by(Article.published.desc())

def read_articles(date: str = None):
result = await session.execute(stmt)
articles = result.scalars().all()
return articles

def read_articles_sync(date: str = None):
"""Lit les articles résumés qui ont été retenus pour la veille techno"""
with get_db() as session:
if date:
articles = (
session.query(Article).filter(Article.published.like(f"%{date}%")).all()
)
stmt = select(Article).filter(Article.published.like(f"%{date}%"))
else:
articles = session.query(Article).order_by(Article.published.desc()).all()
return articles
stmt = select(Article).order_by(Article.published.desc())

result = session.execute(stmt)
articles = result.scalars().all()
return articles

def _validate_and_get_articles_summaries(summaries):
for item in summaries:
Expand Down Expand Up @@ -399,5 +441,5 @@ def search_fts(keywords: str):


if __name__ == "__main__":
articles = read_articles()
articles = read_articles_sync()
print(f"Nombre d'articles en base: {len(articles)}")
10 changes: 10 additions & 0 deletions app/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import asyncio
import sys

if sys.platform == 'win32':
# Force l'utilisation de ProactorEventLoop sur Windows
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

if __name__ == "__main__":
import uvicorn
uvicorn.run("web:app", host="0.0.0.0", port=8000, loop="asyncio")
21 changes: 16 additions & 5 deletions ollama.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ services:
ollama:
container_name: ollama
image: ollama/ollama
# env_file:
# - ../.env # Chemin relatif vers le .env à la racine
env_file:
- .env

# runtime: nvidia
environment:
LLM_MODEL: "mistral"
environment:
NVIDIA_VISIBLE_DEVICES: "all"
OLLAMA_KV_CACHE_TYPE: "q4_0"
OLLAMA_KEEP_ALIVE: "24h"
Expand All @@ -26,7 +25,19 @@ services:
volumes:
- ollama:/root/.ollama
restart: unless-stopped
entrypoint: /bin/sh -c "ollama serve & sleep 3 && ollama pull ${LLM_MODEL} && wait"

entrypoint: >
/bin/sh -c "
echo '========================================' &&
echo 'LLM_MODEL: ${LLM_MODEL:-mistral}' &&
echo '========================================' &&
ollama serve &
sleep 3 &&
echo 'Pulling model ${LLM_MODEL:-mistral}...' &&
ollama pull $${LLM_MODEL:-mistral} &&
wait
"
# entrypoint: /bin/sh -c "ollama serve & sleep 3 && ollama pull ${LLM_MODEL} && wait"
# entrypoint: /bin/sh -c "ollama serve & sleep 3 && ollama pull $${LLM_MODEL} && wait"

volumes:
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ dependencies = [
"grandalf>=0.8",
"matplotlib>=3.10.6",
"nltk>=3.9.1",
"sqlalchemy>=2.0.43",
"sqlalchemy[asyncio]>=2.0.43",
"fastapi>=0.116.1",
"uvicorn>=0.35.0",
"pytz>=2025.2",
"praw>=7.8.1",
"tiktoken>=0.11.0",
"atproto>=0.0.62",
"aiosqlite>=0.21.0",
"pandas>=2.3.3",
"gunicorn>=23.0.0",
"faker>=38.2.0",
"mypy>=1.19.1",
]

[tool.uv.sources]
Expand Down
Loading