|
# retoor <retoor@molodetz.nl>
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import shutil
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
from devplacepy.config import ISSLOP_MEDIA_DIR
|
|
from devplacepy.database import get_table
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TABLE_ANALYSES = "isslop_analyses"
|
|
TABLE_EVENTS = "isslop_events"
|
|
TABLE_FILE_RESULTS = "isslop_file_results"
|
|
TABLE_IMAGE_RESULTS = "isslop_image_results"
|
|
TABLE_DOM_RESULTS = "isslop_dom_results"
|
|
TABLE_REPORTS = "isslop_reports"
|
|
|
|
EVENT_LIMIT_DEFAULT = 2000
|
|
EVENT_PAYLOAD_CAP = 60000
|
|
SIGNALS_CAP = 30000
|
|
LIST_LIMIT_DEFAULT = 50
|
|
|
|
|
|
def utc_now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def create_analysis(uid: str, source_url: str, owner_kind: str, owner_id: str) -> dict[str, Any]:
|
|
row: dict[str, Any] = {
|
|
"uid": uid,
|
|
"owner_kind": owner_kind,
|
|
"owner_id": owner_id,
|
|
"source_url": source_url,
|
|
"source_kind": "unknown",
|
|
"status": "pending",
|
|
"created_at": utc_now(),
|
|
"finished_at": None,
|
|
"content_hash": None,
|
|
"grade": None,
|
|
"slop_score": None,
|
|
"origin_score": None,
|
|
"quality_deficit_score": None,
|
|
"human_percent": None,
|
|
"ai_percent": None,
|
|
"category": None,
|
|
"confidence": None,
|
|
"files_total": 0,
|
|
"files_analyzed": 0,
|
|
"error_message_text": None,
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
}
|
|
get_table(TABLE_ANALYSES).insert(row)
|
|
return row
|
|
|
|
|
|
def update_analysis(uid: str, **fields: Any) -> None:
|
|
fields["uid"] = uid
|
|
get_table(TABLE_ANALYSES).update(fields, ["uid"])
|
|
|
|
|
|
def get_analysis(uid: str) -> Optional[dict[str, Any]]:
|
|
row = get_table(TABLE_ANALYSES).find_one(uid=uid, deleted_at=None)
|
|
return dict(row) if row else None
|
|
|
|
|
|
def list_analyses(owner_kind: str, owner_id: str, limit: int = LIST_LIMIT_DEFAULT) -> list[dict[str, Any]]:
|
|
rows = get_table(TABLE_ANALYSES).find(
|
|
owner_kind=owner_kind,
|
|
owner_id=owner_id,
|
|
deleted_at=None,
|
|
order_by=["-created_at"],
|
|
_limit=limit,
|
|
)
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def claim_guest_analyses(guest_id: str, user_uid: str) -> int:
|
|
table = get_table(TABLE_ANALYSES)
|
|
rows = list(table.find(owner_kind="guest", owner_id=guest_id, deleted_at=None))
|
|
for row in rows:
|
|
table.update({"uid": row["uid"], "owner_kind": "user", "owner_id": user_uid}, ["uid"])
|
|
if rows:
|
|
logger.info("Claimed %d guest isslop analyses for user %s", len(rows), user_uid)
|
|
return len(rows)
|
|
|
|
|
|
def insert_event(analysis_uid: str, seq: int, kind: str, message: str, payload: str) -> None:
|
|
get_table(TABLE_EVENTS).insert(
|
|
{
|
|
"analysis_uid": analysis_uid,
|
|
"seq": seq,
|
|
"kind": kind,
|
|
"message": message,
|
|
"payload": payload[:EVENT_PAYLOAD_CAP],
|
|
"created_at": utc_now(),
|
|
}
|
|
)
|
|
|
|
|
|
def events_for(analysis_uid: str, after_seq: int = 0, limit: int = EVENT_LIMIT_DEFAULT) -> list[dict[str, Any]]:
|
|
rows = get_table(TABLE_EVENTS).find(
|
|
analysis_uid=analysis_uid,
|
|
seq={">": after_seq},
|
|
order_by=["seq"],
|
|
_limit=limit,
|
|
)
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def insert_file_result(analysis_uid: str, record: dict[str, Any]) -> None:
|
|
record["analysis_uid"] = analysis_uid
|
|
get_table(TABLE_FILE_RESULTS).insert(record)
|
|
|
|
|
|
def file_result_for(analysis_uid: str, path: str) -> Optional[dict[str, Any]]:
|
|
row = get_table(TABLE_FILE_RESULTS).find_one(analysis_uid=analysis_uid, path=path)
|
|
return dict(row) if row else None
|
|
|
|
|
|
def file_results_for(analysis_uid: str) -> list[dict[str, Any]]:
|
|
rows = get_table(TABLE_FILE_RESULTS).find(analysis_uid=analysis_uid, order_by=["path"])
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def insert_image_result(analysis_uid: str, record: dict[str, Any]) -> None:
|
|
record["analysis_uid"] = analysis_uid
|
|
get_table(TABLE_IMAGE_RESULTS).insert(record)
|
|
|
|
|
|
def image_results_for(analysis_uid: str) -> list[dict[str, Any]]:
|
|
rows = get_table(TABLE_IMAGE_RESULTS).find(analysis_uid=analysis_uid, order_by=["-ai_probability"])
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def insert_dom_result(analysis_uid: str, record: dict[str, Any]) -> None:
|
|
record["analysis_uid"] = analysis_uid
|
|
get_table(TABLE_DOM_RESULTS).insert(record)
|
|
|
|
|
|
def dom_results_for(analysis_uid: str) -> list[dict[str, Any]]:
|
|
rows = get_table(TABLE_DOM_RESULTS).find(analysis_uid=analysis_uid, order_by=["-signal_count"])
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def save_report(analysis_uid: str, markdown: str, model_used: str) -> None:
|
|
get_table(TABLE_REPORTS).upsert(
|
|
{
|
|
"analysis_uid": analysis_uid,
|
|
"markdown": markdown,
|
|
"model_used": model_used,
|
|
"generated_at": utc_now(),
|
|
},
|
|
["analysis_uid"],
|
|
)
|
|
|
|
|
|
def get_report(analysis_uid: str) -> Optional[dict[str, Any]]:
|
|
row = get_table(TABLE_REPORTS).find_one(analysis_uid=analysis_uid)
|
|
return dict(row) if row else None
|
|
|
|
|
|
def decode_json(field: Any, fallback: Any) -> Any:
|
|
if not field:
|
|
return fallback
|
|
try:
|
|
parsed = json.loads(field)
|
|
except (ValueError, TypeError):
|
|
return fallback
|
|
return parsed if isinstance(parsed, type(fallback)) else fallback
|
|
|
|
|
|
def media_dir_for(uid: str) -> Path:
|
|
return ISSLOP_MEDIA_DIR / uid
|
|
|
|
|
|
def reset_evidence(uid: str) -> None:
|
|
get_table(TABLE_EVENTS).delete(analysis_uid=uid)
|
|
get_table(TABLE_FILE_RESULTS).delete(analysis_uid=uid)
|
|
get_table(TABLE_IMAGE_RESULTS).delete(analysis_uid=uid)
|
|
get_table(TABLE_DOM_RESULTS).delete(analysis_uid=uid)
|
|
get_table(TABLE_REPORTS).delete(analysis_uid=uid)
|
|
shutil.rmtree(media_dir_for(uid), ignore_errors=True)
|
|
|
|
|
|
def purge_analysis(uid: str) -> None:
|
|
reset_evidence(uid)
|
|
get_table(TABLE_ANALYSES).delete(uid=uid)
|