stefanches7 commited on
Commit
f957d0a
·
1 Parent(s): ba7c80f

implement logging to a text file

Browse files
Files changed (2) hide show
  1. cli.py +27 -5
  2. logging_utils.py +63 -0
cli.py CHANGED
@@ -1,8 +1,11 @@
1
  import argparse
 
2
  import os
3
  import re
4
  import sys
5
  from typing import List, Optional
 
 
6
 
7
  from agent import BIDSifierAgent
8
 
@@ -58,9 +61,11 @@ def short_divider(title: str) -> None:
58
  print(title)
59
  print("=" * 80 + "\n")
60
 
61
- def enter_feedback_loop(agent: BIDSifierAgent, context: dict) -> dict:
62
  feedback = input("\nAny comments or corrections to the summary? (press Enter to skip): ").strip()
63
  while feedback:
 
 
64
  context["user_feedback"] += feedback
65
  agent_response = agent.run_query(feedback)
66
  print(agent_response)
@@ -79,12 +84,17 @@ def main(argv: Optional[List[str]] = None) -> int:
79
  parser.add_argument("--output-root", dest="output_root", help="Target BIDS root directory", required=True)
80
  parser.add_argument("--provider", dest="provider", help="Provider name or identifier, default OpeanAI", required=False, default="openai")
81
  parser.add_argument("--model", dest="model", help="Model name to use", default=os.getenv("BIDSIFIER_MODEL", "gpt-4o-mini"))
 
82
  # Execution is intentionally disabled; we only display commands.
83
  # Keeping --dry-run for backward compatibility (no effect other than display).
84
  parser.add_argument("--dry-run", dest="dry_run", help="Display-only (default behavior)", action="store_true")
85
 
86
  args = parser.parse_args(argv)
87
 
 
 
 
 
88
  dataset_xml = _read_optional(args.dataset_xml_path)
89
  readme_text = _read_optional(args.readme_path)
90
  publication_text = _read_optional(args.publication_path)
@@ -112,8 +122,11 @@ def main(argv: Optional[List[str]] = None) -> int:
112
  short_divider("Step 1: Understand dataset")
113
  summary = agent.run_step("summary", context)
114
  print(summary)
115
- context = enter_feedback_loop(agent, context)
 
 
116
  if not prompt_yes_no("Proceed to create BIDS root?", default=True):
 
117
  return 0
118
 
119
  short_divider("Step 2: Propose commands to create metadata files")
@@ -121,8 +134,11 @@ def main(argv: Optional[List[str]] = None) -> int:
121
  print(meta_plan)
122
  cmds = parse_commands_from_markdown(meta_plan)
123
  _print_commands(cmds)
124
- context = enter_feedback_loop(agent, context)
 
 
125
  if not prompt_yes_no("Proceed to create empty BIDS structure?", default=True):
 
126
  return 0
127
 
128
  short_divider("Step 3: Propose commands to create dataset structure")
@@ -130,8 +146,11 @@ def main(argv: Optional[List[str]] = None) -> int:
130
  print(struct_plan)
131
  cmds = parse_commands_from_markdown(struct_plan)
132
  _print_commands(cmds)
133
- context = enter_feedback_loop(agent, context)
 
 
134
  if not prompt_yes_no("Proceed to propose renaming/moving?", default=True):
 
135
  return 0
136
 
137
  short_divider("Step 4: Propose commands to rename/move files")
@@ -139,9 +158,12 @@ def main(argv: Optional[List[str]] = None) -> int:
139
  print(move_plan)
140
  cmds = parse_commands_from_markdown(move_plan)
141
  _print_commands(cmds)
142
- context = enter_feedback_loop(agent, context)
 
 
143
 
144
  print("\nAll steps completed. Commands were only displayed - use them manually")
 
145
  return 0
146
 
147
 
 
1
  import argparse
2
+ import logging
3
  import os
4
  import re
5
  import sys
6
  from typing import List, Optional
7
+ from pathlib import Path
8
+ from logging_utils import setup_logging
9
 
10
  from agent import BIDSifierAgent
11
 
 
61
  print(title)
62
  print("=" * 80 + "\n")
63
 
64
+ def enter_feedback_loop(agent: BIDSifierAgent, context: dict, logger: Optional[logging.Logger] = None) -> dict:
65
  feedback = input("\nAny comments or corrections to the summary? (press Enter to skip): ").strip()
66
  while feedback:
67
+ if logger:
68
+ logger.info("User feedback: %s", feedback)
69
  context["user_feedback"] += feedback
70
  agent_response = agent.run_query(feedback)
71
  print(agent_response)
 
84
  parser.add_argument("--output-root", dest="output_root", help="Target BIDS root directory", required=True)
85
  parser.add_argument("--provider", dest="provider", help="Provider name or identifier, default OpeanAI", required=False, default="openai")
86
  parser.add_argument("--model", dest="model", help="Model name to use", default=os.getenv("BIDSIFIER_MODEL", "gpt-4o-mini"))
87
+ parser.add_argument("--project", dest="project", help="Project name for log file prefix", required=False)
88
  # Execution is intentionally disabled; we only display commands.
89
  # Keeping --dry-run for backward compatibility (no effect other than display).
90
  parser.add_argument("--dry-run", dest="dry_run", help="Display-only (default behavior)", action="store_true")
91
 
92
  args = parser.parse_args(argv)
93
 
94
+ project_name = args.project or Path(args.output_root).name or Path(os.getcwd()).name
95
+ logger, _listener = setup_logging(project_name=project_name)
96
+ logger.info("Initialized logging for project '%s'", project_name)
97
+
98
  dataset_xml = _read_optional(args.dataset_xml_path)
99
  readme_text = _read_optional(args.readme_path)
100
  publication_text = _read_optional(args.publication_path)
 
122
  short_divider("Step 1: Understand dataset")
123
  summary = agent.run_step("summary", context)
124
  print(summary)
125
+ logger.info(summary)
126
+ logger.info("Summary step completed (length=%d chars)", len(summary))
127
+ context = enter_feedback_loop(agent, context, logger)
128
  if not prompt_yes_no("Proceed to create BIDS root?", default=True):
129
+ logger.info("User aborted after summary step.")
130
  return 0
131
 
132
  short_divider("Step 2: Propose commands to create metadata files")
 
134
  print(meta_plan)
135
  cmds = parse_commands_from_markdown(meta_plan)
136
  _print_commands(cmds)
137
+ logger.info("Metadata plan produced %s", cmds)
138
+ logger.info("Metadata plan produced %d commands", len(cmds))
139
+ context = enter_feedback_loop(agent, context, logger)
140
  if not prompt_yes_no("Proceed to create empty BIDS structure?", default=True):
141
+ logger.info("User aborted after metadata plan.")
142
  return 0
143
 
144
  short_divider("Step 3: Propose commands to create dataset structure")
 
146
  print(struct_plan)
147
  cmds = parse_commands_from_markdown(struct_plan)
148
  _print_commands(cmds)
149
+ logger.info("Structure plan produced %s", cmds)
150
+ logger.info("Structure plan produced %d commands", len(cmds))
151
+ context = enter_feedback_loop(agent, context, logger)
152
  if not prompt_yes_no("Proceed to propose renaming/moving?", default=True):
153
+ logger.info("User aborted after structure plan.")
154
  return 0
155
 
156
  short_divider("Step 4: Propose commands to rename/move files")
 
158
  print(move_plan)
159
  cmds = parse_commands_from_markdown(move_plan)
160
  _print_commands(cmds)
161
+ logger.info("Rename/move plan produced %s", cmds)
162
+ logger.info("Rename/move plan produced %d commands", len(cmds))
163
+ context = enter_feedback_loop(agent, context, logger)
164
 
165
  print("\nAll steps completed. Commands were only displayed - use them manually")
166
+ logger.info("All steps completed successfully.")
167
  return 0
168
 
169
 
logging_utils.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import logging.handlers
3
+ import multiprocessing
4
+ import datetime
5
+ from pathlib import Path
6
+ import atexit
7
+ from typing import Tuple, Optional
8
+
9
+ DEFAULT_LOGS_DIR = "logs"
10
+
11
+
12
+ def setup_logging(project_name: str, logs_dir: str = DEFAULT_LOGS_DIR, level: int = logging.INFO,
13
+ queue_logging: bool = True) -> Tuple[logging.Logger, Optional[logging.handlers.QueueListener]]:
14
+ """
15
+ Configure parallel-safe logging.
16
+
17
+ Creates a log file named ``{project_name}-{timestamp}.log`` inside ``logs_dir``.
18
+ If ``queue_logging`` is True, uses ``multiprocessing.Queue`` with ``QueueHandler``/``QueueListener``
19
+ for process-safe logging. Returns the application logger and the listener (if any).
20
+ Caller does not need to manage handlers individually; the listener is auto-stopped at exit.
21
+ """
22
+ timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
23
+ logs_path = Path(logs_dir)
24
+ logs_path.mkdir(parents=True, exist_ok=True)
25
+ logfile = logs_path / f"{project_name}-{timestamp}.log"
26
+
27
+ formatter = logging.Formatter(
28
+ fmt="%(asctime)s | %(levelname)s | %(processName)s | %(name)s | %(message)s",
29
+ datefmt="%Y-%m-%d %H:%M:%S"
30
+ )
31
+
32
+ logger = logging.getLogger("bidsifier")
33
+ logger.setLevel(level)
34
+
35
+ listener: Optional[logging.handlers.QueueListener] = None
36
+
37
+ if queue_logging:
38
+ log_queue: multiprocessing.Queue = multiprocessing.Queue(-1)
39
+ queue_handler = logging.handlers.QueueHandler(log_queue)
40
+ file_handler = logging.FileHandler(str(logfile), encoding="utf-8")
41
+ file_handler.setFormatter(formatter)
42
+ listener = logging.handlers.QueueListener(log_queue, file_handler)
43
+ listener.start()
44
+ logger.addHandler(queue_handler)
45
+
46
+ def _stop_listener() -> None:
47
+ try:
48
+ listener.stop()
49
+ except Exception:
50
+ pass
51
+ atexit.register(_stop_listener)
52
+ else:
53
+ file_handler = logging.FileHandler(str(logfile), encoding="utf-8")
54
+ file_handler.setFormatter(formatter)
55
+ logger.addHandler(file_handler)
56
+
57
+ # Also add a simple stderr stream handler for immediate feedback.
58
+ stream_handler = logging.StreamHandler()
59
+ stream_handler.setFormatter(formatter)
60
+ logger.addHandler(stream_handler)
61
+
62
+ logger.debug("Logging initialized: %s", logfile)
63
+ return logger, listener