Spaces:
Sleeping
Sleeping
KevinIsInCoding claude[bot] KevinIsInCoding commited on
feat: add Hugging Face-compatible logging infrastructure (#9)
Browse files- New `beacon_logging.py` module with rotating JSON file handler and
a console handler using HF's `%(levelname)s:%(name)s:%(message)s`
format. Controlled via `BEACON_LOG_LEVEL` / `BEACON_LOG_DIR` env vars.
- `clinical_trials_guru.py`: logs patient intake profile JSON (INFO) and
full ClinicalTrials.gov request payload + per-page response metadata (DEBUG/INFO).
- `app.py`: logs patient intake profile JSON (INFO) after web-UI intake.
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: KevinIsInCoding <KevinIsInCoding@users.noreply.github.com>
- app.py +8 -0
- beacon_logging.py +65 -0
- clinical_trials_guru.py +25 -3
app.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
import datetime
|
| 4 |
import json
|
| 5 |
from typing import Generator
|
| 6 |
|
| 7 |
import anthropic
|
| 8 |
import gradio as gr
|
|
|
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
|
| 11 |
from clinical_trials_guru import (
|
|
@@ -24,6 +26,8 @@ from translations import LANGUAGE_DIRECTIVE, LANGUAGES, UI
|
|
| 24 |
|
| 25 |
load_dotenv()
|
| 26 |
|
|
|
|
|
|
|
| 27 |
|
| 28 |
def _intake_turn(
|
| 29 |
user_text: str, messages: list, lang: str = "en"
|
|
@@ -68,6 +72,10 @@ def _intake_turn(
|
|
| 68 |
include_observational=data.get("include_observational", False),
|
| 69 |
lang=lang,
|
| 70 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
return text or UI[lang]["got_it"], messages, profile
|
| 72 |
|
| 73 |
return text, messages, None
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import dataclasses
|
| 4 |
import datetime
|
| 5 |
import json
|
| 6 |
from typing import Generator
|
| 7 |
|
| 8 |
import anthropic
|
| 9 |
import gradio as gr
|
| 10 |
+
from beacon_logging import get_logger
|
| 11 |
from dotenv import load_dotenv
|
| 12 |
|
| 13 |
from clinical_trials_guru import (
|
|
|
|
| 26 |
|
| 27 |
load_dotenv()
|
| 28 |
|
| 29 |
+
_logger = get_logger("app")
|
| 30 |
+
|
| 31 |
|
| 32 |
def _intake_turn(
|
| 33 |
user_text: str, messages: list, lang: str = "en"
|
|
|
|
| 72 |
include_observational=data.get("include_observational", False),
|
| 73 |
lang=lang,
|
| 74 |
)
|
| 75 |
+
_logger.info(
|
| 76 |
+
"Patient intake complete (web)",
|
| 77 |
+
extra={"data": {"intake_summary": dataclasses.asdict(profile)}},
|
| 78 |
+
)
|
| 79 |
return text or UI[lang]["got_it"], messages, profile
|
| 80 |
|
| 81 |
return text, messages, None
|
beacon_logging.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import logging.handlers
|
| 6 |
+
import os
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
|
| 9 |
+
# Mirrors Hugging Face's LOG_LEVEL convention; use BEACON_ prefix to avoid collisions.
|
| 10 |
+
_LOG_LEVEL = os.getenv("BEACON_LOG_LEVEL", "WARNING").upper()
|
| 11 |
+
_LOG_DIR = os.getenv("BEACON_LOG_DIR", "logs")
|
| 12 |
+
|
| 13 |
+
_configured = False
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class _JSONFormatter(logging.Formatter):
|
| 17 |
+
"""Structured JSON lines for the rotating file handler."""
|
| 18 |
+
|
| 19 |
+
def format(self, record: logging.LogRecord) -> str:
|
| 20 |
+
entry: dict = {
|
| 21 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 22 |
+
"level": record.levelname,
|
| 23 |
+
"logger": record.name,
|
| 24 |
+
"message": record.getMessage(),
|
| 25 |
+
}
|
| 26 |
+
if hasattr(record, "data"):
|
| 27 |
+
entry["data"] = record.data
|
| 28 |
+
return json.dumps(entry, default=str)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _setup() -> None:
|
| 32 |
+
global _configured
|
| 33 |
+
if _configured:
|
| 34 |
+
return
|
| 35 |
+
_configured = True
|
| 36 |
+
|
| 37 |
+
level = getattr(logging, _LOG_LEVEL, logging.WARNING)
|
| 38 |
+
|
| 39 |
+
root = logging.getLogger("beacon")
|
| 40 |
+
root.setLevel(logging.DEBUG) # individual handlers apply their own level
|
| 41 |
+
root.propagate = False
|
| 42 |
+
|
| 43 |
+
# Console β same format string as HuggingFace transformers/datasets
|
| 44 |
+
console = logging.StreamHandler()
|
| 45 |
+
console.setLevel(level)
|
| 46 |
+
console.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s"))
|
| 47 |
+
root.addHandler(console)
|
| 48 |
+
|
| 49 |
+
# Rotating JSON file β always DEBUG so nothing is silently dropped
|
| 50 |
+
os.makedirs(_LOG_DIR, exist_ok=True)
|
| 51 |
+
fh = logging.handlers.RotatingFileHandler(
|
| 52 |
+
os.path.join(_LOG_DIR, "beacon.log"),
|
| 53 |
+
maxBytes=10 * 1024 * 1024,
|
| 54 |
+
backupCount=5,
|
| 55 |
+
encoding="utf-8",
|
| 56 |
+
)
|
| 57 |
+
fh.setLevel(logging.DEBUG)
|
| 58 |
+
fh.setFormatter(_JSONFormatter())
|
| 59 |
+
root.addHandler(fh)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def get_logger(name: str) -> logging.Logger:
|
| 63 |
+
"""Return a ``beacon.<name>`` logger, configuring handlers on first call."""
|
| 64 |
+
_setup()
|
| 65 |
+
return logging.getLogger(f"beacon.{name}")
|
clinical_trials_guru.py
CHANGED
|
@@ -3,9 +3,10 @@ from __future__ import annotations
|
|
| 3 |
import json
|
| 4 |
import math
|
| 5 |
import time
|
| 6 |
-
from dataclasses import dataclass, field
|
| 7 |
from typing import Optional, TypedDict
|
| 8 |
|
|
|
|
| 9 |
from translations import LANGUAGE_DIRECTIVE
|
| 10 |
|
| 11 |
import anthropic
|
|
@@ -17,6 +18,8 @@ from rich.panel import Panel
|
|
| 17 |
from rich.text import Text
|
| 18 |
from langgraph.graph import StateGraph, START, END
|
| 19 |
|
|
|
|
|
|
|
| 20 |
CTGOV_BASE = "https://clinicaltrials.gov/api/v2/studies"
|
| 21 |
INTAKE_MODEL = "claude-sonnet-4-6"
|
| 22 |
RESEARCH_MODEL = "claude-opus-4-7"
|
|
@@ -344,6 +347,11 @@ def search_trials_api(
|
|
| 344 |
else:
|
| 345 |
params["aggFilters"] = "studyType:int"
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
all_studies: list[dict] = []
|
| 348 |
while True:
|
| 349 |
for attempt in range(3):
|
|
@@ -358,12 +366,21 @@ def search_trials_api(
|
|
| 358 |
wait = 2 ** attempt
|
| 359 |
console.print(f"[yellow]API warning:[/yellow] {exc} β retrying in {wait}s (attempt {attempt + 1}/3)β¦")
|
| 360 |
time.sleep(wait)
|
| 361 |
-
|
|
|
|
| 362 |
next_token = body.get("nextPageToken")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
if not next_token:
|
| 364 |
break
|
| 365 |
params["pageToken"] = next_token
|
| 366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
return all_studies
|
| 368 |
|
| 369 |
|
|
@@ -477,7 +494,7 @@ def run_intake_agent(client: anthropic.Anthropic) -> PatientProfile:
|
|
| 477 |
except Exception as exc:
|
| 478 |
console.print(f"[yellow]Warning:[/yellow] Geocoding failed ({exc}) β coordinates set to 0,0.")
|
| 479 |
lat, lon = 0.0, 0.0
|
| 480 |
-
|
| 481 |
disease=data["disease"],
|
| 482 |
age=data["age"],
|
| 483 |
onset_months=data["onset_months"],
|
|
@@ -492,6 +509,11 @@ def run_intake_agent(client: anthropic.Anthropic) -> PatientProfile:
|
|
| 492 |
include_eap=data.get("include_eap", False),
|
| 493 |
include_observational=data.get("include_observational", False),
|
| 494 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
|
| 496 |
messages.append({"role": "assistant", "content": response.content})
|
| 497 |
user_input = input("\nYou: ").strip() or "(no response)"
|
|
|
|
| 3 |
import json
|
| 4 |
import math
|
| 5 |
import time
|
| 6 |
+
from dataclasses import asdict, dataclass, field
|
| 7 |
from typing import Optional, TypedDict
|
| 8 |
|
| 9 |
+
from beacon_logging import get_logger
|
| 10 |
from translations import LANGUAGE_DIRECTIVE
|
| 11 |
|
| 12 |
import anthropic
|
|
|
|
| 18 |
from rich.text import Text
|
| 19 |
from langgraph.graph import StateGraph, START, END
|
| 20 |
|
| 21 |
+
_logger = get_logger("clinical_trials_guru")
|
| 22 |
+
|
| 23 |
CTGOV_BASE = "https://clinicaltrials.gov/api/v2/studies"
|
| 24 |
INTAKE_MODEL = "claude-sonnet-4-6"
|
| 25 |
RESEARCH_MODEL = "claude-opus-4-7"
|
|
|
|
| 347 |
else:
|
| 348 |
params["aggFilters"] = "studyType:int"
|
| 349 |
|
| 350 |
+
_logger.info(
|
| 351 |
+
"ClinicalTrials.gov API request",
|
| 352 |
+
extra={"data": {"endpoint": CTGOV_BASE, "params": dict(params)}},
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
all_studies: list[dict] = []
|
| 356 |
while True:
|
| 357 |
for attempt in range(3):
|
|
|
|
| 366 |
wait = 2 ** attempt
|
| 367 |
console.print(f"[yellow]API warning:[/yellow] {exc} β retrying in {wait}s (attempt {attempt + 1}/3)β¦")
|
| 368 |
time.sleep(wait)
|
| 369 |
+
page_studies = body.get("studies", [])
|
| 370 |
+
all_studies.extend(page_studies)
|
| 371 |
next_token = body.get("nextPageToken")
|
| 372 |
+
_logger.debug(
|
| 373 |
+
"ClinicalTrials.gov API page received",
|
| 374 |
+
extra={"data": {"page_count": len(page_studies), "has_next_page": bool(next_token)}},
|
| 375 |
+
)
|
| 376 |
if not next_token:
|
| 377 |
break
|
| 378 |
params["pageToken"] = next_token
|
| 379 |
|
| 380 |
+
_logger.info(
|
| 381 |
+
"ClinicalTrials.gov API response complete",
|
| 382 |
+
extra={"data": {"total_studies": len(all_studies)}},
|
| 383 |
+
)
|
| 384 |
return all_studies
|
| 385 |
|
| 386 |
|
|
|
|
| 494 |
except Exception as exc:
|
| 495 |
console.print(f"[yellow]Warning:[/yellow] Geocoding failed ({exc}) β coordinates set to 0,0.")
|
| 496 |
lat, lon = 0.0, 0.0
|
| 497 |
+
profile = PatientProfile(
|
| 498 |
disease=data["disease"],
|
| 499 |
age=data["age"],
|
| 500 |
onset_months=data["onset_months"],
|
|
|
|
| 509 |
include_eap=data.get("include_eap", False),
|
| 510 |
include_observational=data.get("include_observational", False),
|
| 511 |
)
|
| 512 |
+
_logger.info(
|
| 513 |
+
"Patient intake complete (CLI)",
|
| 514 |
+
extra={"data": {"intake_summary": asdict(profile)}},
|
| 515 |
+
)
|
| 516 |
+
return profile
|
| 517 |
|
| 518 |
messages.append({"role": "assistant", "content": response.content})
|
| 519 |
user_input = input("\nYou: ").strip() or "(no response)"
|