KevinIsInCoding claude[bot] KevinIsInCoding commited on
Commit
083d098
Β·
unverified Β·
1 Parent(s): 7389f19

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>

Files changed (3) hide show
  1. app.py +8 -0
  2. beacon_logging.py +65 -0
  3. 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
- all_studies.extend(body.get("studies", []))
 
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
- return PatientProfile(
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)"