flock-core 0.3.18__py3-none-any.whl → 0.3.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

flock/__init__.py CHANGED
@@ -3,6 +3,8 @@
3
3
  from flock.cli.constants import CLI_EXIT, CLI_NOTES, CLI_THEME_BUILDER
4
4
  from flock.cli.load_release_notes import load_release_notes
5
5
  from flock.core.logging.formatters.theme_builder import theme_builder
6
+ from flock.cli.settings import settings_editor
7
+ from rich.panel import Panel
6
8
 
7
9
 
8
10
  def main():
@@ -27,7 +29,9 @@ def main():
27
29
  while True:
28
30
  init_console()
29
31
 
30
- console.print("Flock Management Console\n", style="bold green")
32
+ #console.print("Flock Management Console\n", style="bold green")
33
+ console.print(Panel("[bold green]Flock Management Console[/]"), justify="center")
34
+ console.line()
31
35
 
32
36
  result = questionary.select(
33
37
  "What do you want to do?",
@@ -54,6 +58,8 @@ def main():
54
58
  load_flock()
55
59
  if result == CLI_THEME_BUILDER:
56
60
  theme_builder()
61
+ if result == CLI_SETTINGS:
62
+ settings_editor()
57
63
  if result == CLI_NOTES:
58
64
  load_release_notes()
59
65
  if result == CLI_EXIT:
flock/cli/settings.py CHANGED
@@ -1 +1,857 @@
1
- # TODO
1
+ """Settings editor for the Flock CLI.
2
+
3
+ This module provides functionality to view, edit, add, and delete
4
+ environment variables in the .env file.
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ from pathlib import Path
10
+ from typing import Dict, List, Optional, Tuple
11
+ import math
12
+
13
+ import questionary
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.table import Table
17
+ from rich.text import Text
18
+
19
+ from flock.core.util.cli_helper import init_console
20
+
21
+ # Constants
22
+ ENV_FILE = ".env"
23
+ ENV_TEMPLATE_FILE = ".env_template"
24
+ ENV_PROFILE_PREFIX = ".env_"
25
+ DEFAULT_PROFILE_COMMENT = "# Profile: {profile_name}"
26
+ SHOW_SECRETS_KEY = "SHOW_SECRETS"
27
+ VARS_PER_PAGE_KEY = "VARS_PER_PAGE"
28
+ DEFAULT_VARS_PER_PAGE = 20
29
+
30
+ console = Console()
31
+
32
+
33
+ def settings_editor():
34
+ """Main entry point for the settings editor."""
35
+ while True:
36
+ init_console()
37
+ console.print(Panel("[bold green]Environment Settings Editor[/]"), justify="center")
38
+
39
+ # Get current profile name
40
+ current_profile = get_current_profile()
41
+ if current_profile:
42
+ console.print(f"Current Profile: [bold cyan]{current_profile}[/]")
43
+ else:
44
+ console.print("No profile detected")
45
+
46
+ console.line()
47
+
48
+ choice = questionary.select(
49
+ "What would you like to do?",
50
+ choices=[
51
+ questionary.Separator(line=" "),
52
+ "View all environment variables",
53
+ "Edit an environment variable",
54
+ "Add a new environment variable",
55
+ "Delete an environment variable",
56
+ questionary.Separator(),
57
+ "Manage environment profiles",
58
+ questionary.Separator(),
59
+ "Toggle show secrets",
60
+ "Change variables per page",
61
+ questionary.Separator(),
62
+ "Back to main menu",
63
+ ],
64
+ ).ask()
65
+
66
+ if choice == "View all environment variables":
67
+ view_env_variables()
68
+ elif choice == "Edit an environment variable":
69
+ edit_env_variable()
70
+ elif choice == "Add a new environment variable":
71
+ add_env_variable()
72
+ elif choice == "Delete an environment variable":
73
+ delete_env_variable()
74
+ elif choice == "Manage environment profiles":
75
+ manage_profiles()
76
+ elif choice == "Toggle show secrets":
77
+ toggle_show_secrets()
78
+ elif choice == "Change variables per page":
79
+ change_vars_per_page()
80
+ elif choice == "Back to main menu":
81
+ break
82
+
83
+ if choice != "Back to main menu":
84
+ input("\nPress Enter to continue...")
85
+
86
+
87
+ def view_env_variables(page: int = 1, page_size: Optional[int] = None):
88
+ """View all environment variables with pagination.
89
+
90
+ Args:
91
+ page: Page number to display
92
+ page_size: Number of variables per page (if None, use the setting in .env)
93
+ """
94
+ env_vars = load_env_file()
95
+
96
+ # If page_size is not specified, get it from settings
97
+ if page_size is None:
98
+ page_size = get_vars_per_page_setting(env_vars)
99
+
100
+ # Calculate pagination
101
+ total_vars = len(env_vars)
102
+ total_pages = math.ceil(total_vars / page_size) if total_vars > 0 else 1
103
+
104
+ # Validate page number
105
+ page = min(max(1, page), total_pages)
106
+
107
+ start_idx = (page - 1) * page_size
108
+ end_idx = min(start_idx + page_size, total_vars)
109
+
110
+ # Get current page variables
111
+ current_page_vars = list(env_vars.items())[start_idx:end_idx]
112
+
113
+ # Check if secrets should be shown
114
+ show_secrets = get_show_secrets_setting(env_vars)
115
+
116
+ # Create table
117
+ table = Table(title=f"Environment Variables (Page {page}/{total_pages}, {page_size} per page)")
118
+ table.add_column("Name", style="cyan")
119
+ table.add_column("Value", style="green")
120
+
121
+ # Show secrets status
122
+ secrets_status = "[green]ON[/]" if show_secrets else "[red]OFF[/]"
123
+ init_console()
124
+ console.print(f"Show Secrets: {secrets_status}")
125
+
126
+ for key, value in current_page_vars:
127
+ # Skip lines that are comments or empty
128
+ if key.startswith('#') or not key:
129
+ continue
130
+
131
+ # Mask sensitive values if show_secrets is False
132
+ if is_sensitive(key) and not show_secrets:
133
+ masked_value = mask_sensitive_value(value)
134
+ table.add_row(key, masked_value)
135
+ else:
136
+ table.add_row(key, value)
137
+
138
+ console.print(table)
139
+
140
+ # Pagination controls with more intuitive shortcuts
141
+ console.print("\nNavigation: ", end="")
142
+ if page > 1:
143
+ console.print("[bold]Previous (p)[/] | ", end="")
144
+ if page < total_pages:
145
+ console.print("[bold]Next (n)[/] | ", end="")
146
+ if show_secrets:
147
+ console.print("[bold]Hide secrets (h)[/] | ", end="")
148
+ else:
149
+ console.print("[bold]Show secrets (s)[/] | ", end="")
150
+ console.print("[bold]Change variables per page (v)[/] | ", end="")
151
+ console.print("[bold]Back (b)[/]")
152
+
153
+ # Handle navigation
154
+ while True:
155
+ key = input("Enter option: ").lower()
156
+ if key == 'p' and page > 1:
157
+ view_env_variables(page - 1, page_size)
158
+ break
159
+ elif key == 'n' and page < total_pages:
160
+ view_env_variables(page + 1, page_size)
161
+ break
162
+ elif key == 's' and not show_secrets:
163
+ # Confirm showing secrets
164
+ confirm = questionary.confirm("Are you sure you want to show sensitive values?").ask()
165
+ if confirm:
166
+ set_show_secrets_setting(True)
167
+ view_env_variables(page, page_size)
168
+ break
169
+ elif key == 'h' and show_secrets:
170
+ set_show_secrets_setting(False)
171
+ view_env_variables(page, page_size)
172
+ break
173
+ elif key == 'v':
174
+ new_page_size = change_vars_per_page()
175
+ if new_page_size:
176
+ view_env_variables(1, new_page_size) # Reset to first page with new page size
177
+ break
178
+ elif key == 'b':
179
+ break
180
+
181
+
182
+ def change_vars_per_page():
183
+ """Change the number of variables displayed per page.
184
+
185
+ Returns:
186
+ The new page size or None if cancelled
187
+ """
188
+ env_vars = load_env_file()
189
+ current_setting = get_vars_per_page_setting(env_vars)
190
+
191
+ console.print(f"Current variables per page: [cyan]{current_setting}[/]")
192
+
193
+ # Predefined options plus custom option
194
+ page_size_options = ["10", "20", "30", "50", "Custom", "Cancel"]
195
+
196
+ choice = questionary.select(
197
+ "Select number of variables per page:",
198
+ choices=page_size_options,
199
+ ).ask()
200
+
201
+ if choice == "Cancel":
202
+ return None
203
+
204
+ if choice == "Custom":
205
+ while True:
206
+ try:
207
+ custom_size = questionary.text(
208
+ "Enter custom page size (5-100):",
209
+ default=str(current_setting)
210
+ ).ask()
211
+
212
+ if not custom_size:
213
+ return None
214
+
215
+ new_size = int(custom_size)
216
+ if 5 <= new_size <= 100:
217
+ break
218
+ else:
219
+ console.print("[yellow]Page size must be between 5 and 100.[/]")
220
+ except ValueError:
221
+ console.print("[yellow]Please enter a valid number.[/]")
222
+ else:
223
+ new_size = int(choice)
224
+
225
+ # Save the setting
226
+ set_vars_per_page_setting(new_size)
227
+ console.print(f"[green]Variables per page set to {new_size}.[/]")
228
+
229
+ return new_size
230
+
231
+
232
+ def get_vars_per_page_setting(env_vars: Dict[str, str] = None) -> int:
233
+ """Get the current variables per page setting.
234
+
235
+ Args:
236
+ env_vars: Optional dictionary of environment variables
237
+
238
+ Returns:
239
+ Number of variables per page
240
+ """
241
+ if env_vars is None:
242
+ env_vars = load_env_file()
243
+
244
+ if VARS_PER_PAGE_KEY in env_vars:
245
+ try:
246
+ page_size = int(env_vars[VARS_PER_PAGE_KEY])
247
+ # Ensure the value is within reasonable bounds
248
+ if 5 <= page_size <= 100:
249
+ return page_size
250
+ except ValueError:
251
+ pass
252
+
253
+ return DEFAULT_VARS_PER_PAGE
254
+
255
+
256
+ def set_vars_per_page_setting(page_size: int):
257
+ """Set the variables per page setting.
258
+
259
+ Args:
260
+ page_size: Number of variables to display per page
261
+ """
262
+ env_vars = load_env_file()
263
+ env_vars[VARS_PER_PAGE_KEY] = str(page_size)
264
+ save_env_file(env_vars)
265
+
266
+
267
+ def toggle_show_secrets():
268
+ """Toggle the show secrets setting."""
269
+ env_vars = load_env_file()
270
+ current_setting = get_show_secrets_setting(env_vars)
271
+
272
+ if current_setting:
273
+ console.print("Currently showing sensitive values. Do you want to hide them?")
274
+ confirm = questionary.confirm("Hide sensitive values?").ask()
275
+ if confirm:
276
+ set_show_secrets_setting(False)
277
+ console.print("[green]Sensitive values will now be masked.[/]")
278
+ else:
279
+ console.print("[yellow]Warning:[/] Showing sensitive values can expose sensitive information.")
280
+ confirm = questionary.confirm("Are you sure you want to show sensitive values?").ask()
281
+ if confirm:
282
+ set_show_secrets_setting(True)
283
+ console.print("[green]Sensitive values will now be shown.[/]")
284
+
285
+
286
+ def get_show_secrets_setting(env_vars: Dict[str, str] = None) -> bool:
287
+ """Get the current show secrets setting.
288
+
289
+ Args:
290
+ env_vars: Optional dictionary of environment variables
291
+
292
+ Returns:
293
+ True if secrets should be shown, False otherwise
294
+ """
295
+ if env_vars is None:
296
+ env_vars = load_env_file()
297
+
298
+ if SHOW_SECRETS_KEY in env_vars:
299
+ return env_vars[SHOW_SECRETS_KEY].lower() == 'true'
300
+
301
+ return False
302
+
303
+
304
+ def set_show_secrets_setting(show_secrets: bool):
305
+ """Set the show secrets setting.
306
+
307
+ Args:
308
+ show_secrets: Whether to show secrets
309
+ """
310
+ env_vars = load_env_file()
311
+ env_vars[SHOW_SECRETS_KEY] = str(show_secrets)
312
+ save_env_file(env_vars)
313
+
314
+
315
+ def edit_env_variable():
316
+ """Edit an environment variable."""
317
+ # Get list of variables
318
+ env_vars = load_env_file()
319
+
320
+ if not env_vars:
321
+ console.print("[yellow]No environment variables found to edit.[/]")
322
+ return
323
+
324
+ # Filter out comments
325
+ variables = [k for k in env_vars.keys() if not k.startswith('#') and k]
326
+
327
+ # Display variables with selection
328
+ init_console()
329
+ console.print("Select a variable to edit:")
330
+
331
+ # Let user select a variable to edit
332
+ var_name = questionary.select(
333
+ "Select a variable to edit:",
334
+ choices=variables + ["Cancel"],
335
+ ).ask()
336
+
337
+ if var_name == "Cancel":
338
+ return
339
+
340
+ current_value = env_vars[var_name]
341
+ is_sensitive_var = is_sensitive(var_name)
342
+
343
+ if is_sensitive_var:
344
+ console.print(f"[yellow]Warning:[/] You are editing a sensitive value: {var_name}")
345
+ confirm = questionary.confirm("Are you sure you want to continue?").ask()
346
+ if not confirm:
347
+ return
348
+
349
+ # Show current value (masked if sensitive and show_secrets is False)
350
+ show_secrets = get_show_secrets_setting(env_vars)
351
+ if is_sensitive_var and not show_secrets:
352
+ console.print(f"Current value: {mask_sensitive_value(current_value)}")
353
+ else:
354
+ console.print(f"Current value: {current_value}")
355
+
356
+ # Get new value with hint
357
+ console.print("[italic]Enter new value (or leave empty to cancel)[/]")
358
+ new_value = questionary.text("Enter new value:", default=current_value).ask()
359
+
360
+ if new_value is None:
361
+ console.print("[yellow]Edit cancelled.[/]")
362
+ return
363
+
364
+ if new_value == "":
365
+ # Confirm if user wants to set an empty value or cancel
366
+ confirm = questionary.confirm("Do you want to set an empty value? Select No to cancel.", default=False).ask()
367
+ if not confirm:
368
+ console.print("[yellow]Edit cancelled.[/]")
369
+ return
370
+
371
+ if new_value == current_value:
372
+ console.print("[yellow]No changes made.[/]")
373
+ return
374
+
375
+ # Update the value
376
+ env_vars[var_name] = new_value
377
+ save_env_file(env_vars)
378
+ console.print(f"[green]Updated {var_name} successfully.[/]")
379
+
380
+
381
+ def add_env_variable():
382
+ """Add a new environment variable."""
383
+ env_vars = load_env_file()
384
+
385
+ console.print("[italic]Enter variable name (or leave empty to go back)[/]")
386
+
387
+ # Get variable name
388
+ while True:
389
+ var_name = questionary.text("Enter variable name:").ask()
390
+
391
+ if not var_name:
392
+ # Ask if user wants to go back
393
+ go_back = questionary.confirm("Do you want to go back to the settings menu?", default=True).ask()
394
+ if go_back:
395
+ return
396
+ else:
397
+ console.print("[italic]Please enter a variable name (or leave empty to go back)[/]")
398
+ continue
399
+
400
+ if var_name in env_vars and not var_name.startswith('#'):
401
+ console.print(f"[yellow]Variable {var_name} already exists. Please use edit instead.[/]")
402
+ continue
403
+
404
+ break
405
+
406
+ # Get variable value
407
+ var_value = questionary.text("Enter variable value:").ask()
408
+
409
+ # Add to env_vars
410
+ env_vars[var_name] = var_value
411
+ save_env_file(env_vars)
412
+ console.print(f"[green]Added {var_name} successfully.[/]")
413
+
414
+
415
+ def delete_env_variable():
416
+ """Delete an environment variable."""
417
+ # Get list of variables
418
+ env_vars = load_env_file()
419
+
420
+ if not env_vars:
421
+ console.print("[yellow]No environment variables found to delete.[/]")
422
+ return
423
+
424
+ # Filter out comments
425
+ variables = [k for k in env_vars.keys() if not k.startswith('#') and k]
426
+
427
+ # Display variables with selection
428
+ init_console()
429
+ console.print("Select a variable to delete:")
430
+
431
+ # Let user select a variable to delete with hint
432
+ var_name = questionary.select(
433
+ "Select a variable to delete:",
434
+ choices=variables + ["Cancel"],
435
+ ).ask()
436
+
437
+ if var_name == "Cancel":
438
+ return
439
+
440
+ # Confirm deletion
441
+ confirm = questionary.confirm(f"Are you sure you want to delete {var_name}?").ask()
442
+ if not confirm:
443
+ console.print("[yellow]Deletion cancelled.[/]")
444
+ return
445
+
446
+ # Delete the variable
447
+ del env_vars[var_name]
448
+ save_env_file(env_vars)
449
+ console.print(f"[green]Deleted {var_name} successfully.[/]")
450
+
451
+
452
+ def manage_profiles():
453
+ """Manage environment profiles."""
454
+ init_console()
455
+ console.print(Panel("[bold green]Environment Profile Management[/]"), justify="center")
456
+
457
+ # Get current profile and available profiles
458
+ current_profile = get_current_profile()
459
+ available_profiles = get_available_profiles()
460
+
461
+ if current_profile:
462
+ console.print(f"Current Profile: [bold cyan]{current_profile}[/]")
463
+
464
+ if not available_profiles:
465
+ console.print("[yellow]No profiles found.[/]")
466
+ else:
467
+ console.print("Available Profiles:")
468
+ for profile in available_profiles:
469
+ if profile == current_profile:
470
+ console.print(f" [bold cyan]{profile} (active)[/]")
471
+ else:
472
+ console.print(f" {profile}")
473
+
474
+ console.line()
475
+
476
+ # Profile management options
477
+ choice = questionary.select(
478
+ "What would you like to do?",
479
+ choices=[
480
+ questionary.Separator(line=" "),
481
+ "Switch to a different profile",
482
+ "Create a new profile",
483
+ "Rename a profile",
484
+ "Delete a profile",
485
+ "Back to settings menu",
486
+ ],
487
+ ).ask()
488
+
489
+ if choice == "Switch to a different profile":
490
+ switch_profile()
491
+ elif choice == "Create a new profile":
492
+ create_profile()
493
+ elif choice == "Rename a profile":
494
+ rename_profile()
495
+ elif choice == "Delete a profile":
496
+ delete_profile()
497
+
498
+
499
+ def switch_profile():
500
+ """Switch to a different environment profile."""
501
+ available_profiles = get_available_profiles()
502
+ current_profile = get_current_profile()
503
+
504
+ if not available_profiles:
505
+ console.print("[yellow]No profiles available to switch to.[/]")
506
+ return
507
+
508
+ # Remove current profile from the list to avoid switching to the same profile
509
+ selectable_profiles = [p for p in available_profiles if p != current_profile]
510
+
511
+ if not selectable_profiles:
512
+ console.print("[yellow]No other profiles available to switch to.[/]")
513
+ return
514
+
515
+ target_profile = questionary.select(
516
+ "Select a profile to switch to:",
517
+ choices=selectable_profiles + ["Cancel"],
518
+ ).ask()
519
+
520
+ if target_profile == "Cancel":
521
+ return
522
+
523
+ # Confirm switch
524
+ confirm = questionary.confirm(f"Are you sure you want to switch to the {target_profile} profile?").ask()
525
+ if not confirm:
526
+ return
527
+
528
+ # Backup current .env file
529
+ backup_env_file()
530
+
531
+ # Copy selected profile to .env
532
+ source_file = f"{ENV_PROFILE_PREFIX}{target_profile}"
533
+ if os.path.exists(source_file):
534
+ shutil.copy2(source_file, ENV_FILE)
535
+ console.print(f"[green]Switched to {target_profile} profile successfully.[/]")
536
+ else:
537
+ console.print(f"[red]Error: Could not find profile file {source_file}.[/]")
538
+
539
+
540
+ def create_profile():
541
+ """Create a new environment profile."""
542
+ profile_name = questionary.text("Enter new profile name:").ask()
543
+
544
+ if not profile_name:
545
+ console.print("[yellow]Profile name cannot be empty.[/]")
546
+ return
547
+
548
+ # Check if profile already exists
549
+ target_file = f"{ENV_PROFILE_PREFIX}{profile_name}"
550
+ if os.path.exists(target_file):
551
+ console.print(f"[yellow]Profile {profile_name} already exists.[/]")
552
+ return
553
+
554
+ # Determine source file - use current .env or template
555
+ source_choices = ["Current environment (.env)", ".env_template"]
556
+ if os.path.exists(ENV_TEMPLATE_FILE):
557
+ source_choices.append(ENV_TEMPLATE_FILE)
558
+
559
+ source_choice = questionary.select(
560
+ "Create profile based on:",
561
+ choices=source_choices + ["Cancel"],
562
+ ).ask()
563
+
564
+ if source_choice == "Cancel":
565
+ return
566
+
567
+ source_file = ENV_FILE if source_choice == "Current environment (.env)" else ENV_TEMPLATE_FILE
568
+
569
+ if not os.path.exists(source_file):
570
+ console.print(f"[red]Error: Source file {source_file} not found.[/]")
571
+ return
572
+
573
+ # Create new profile file
574
+ try:
575
+ # Copy source file
576
+ shutil.copy2(source_file, target_file)
577
+
578
+ # Add profile header if it doesn't exist
579
+ with open(target_file, 'r') as file:
580
+ content = file.read()
581
+
582
+ if not content.startswith("# Profile:"):
583
+ with open(target_file, 'w') as file:
584
+ profile_header = DEFAULT_PROFILE_COMMENT.format(profile_name=profile_name)
585
+ file.write(f"{profile_header}\n{content}")
586
+
587
+ console.print(f"[green]Created {profile_name} profile successfully.[/]")
588
+ except Exception as e:
589
+ console.print(f"[red]Error creating profile: {str(e)}[/]")
590
+
591
+
592
+ def rename_profile():
593
+ """Rename an existing profile."""
594
+ available_profiles = get_available_profiles()
595
+ current_profile = get_current_profile()
596
+
597
+ if not available_profiles:
598
+ console.print("[yellow]No profiles available to rename.[/]")
599
+ return
600
+
601
+ # Let user select a profile to rename
602
+ profile_to_rename = questionary.select(
603
+ "Select a profile to rename:",
604
+ choices=available_profiles + ["Cancel"],
605
+ ).ask()
606
+
607
+ if profile_to_rename == "Cancel":
608
+ return
609
+
610
+ # Get new name
611
+ new_name = questionary.text("Enter new profile name:").ask()
612
+
613
+ if not new_name:
614
+ console.print("[yellow]New profile name cannot be empty.[/]")
615
+ return
616
+
617
+ if new_name in available_profiles:
618
+ console.print(f"[yellow]Profile {new_name} already exists.[/]")
619
+ return
620
+
621
+ # Rename profile file
622
+ source_file = f"{ENV_PROFILE_PREFIX}{profile_to_rename}"
623
+ target_file = f"{ENV_PROFILE_PREFIX}{new_name}"
624
+
625
+ try:
626
+ # Read content of the source file
627
+ with open(source_file, 'r') as file:
628
+ content = file.readlines()
629
+
630
+ # Update profile header if it exists
631
+ if content and content[0].startswith("# Profile:"):
632
+ content[0] = DEFAULT_PROFILE_COMMENT.format(profile_name=new_name) + "\n"
633
+
634
+ # Write to new file
635
+ with open(target_file, 'w') as file:
636
+ file.writelines(content)
637
+
638
+ # Remove old file
639
+ os.remove(source_file)
640
+
641
+ # If this was the current profile, update .env as well
642
+ if profile_to_rename == current_profile:
643
+ with open(ENV_FILE, 'r') as file:
644
+ content = file.readlines()
645
+
646
+ if content and content[0].startswith("# Profile:"):
647
+ content[0] = DEFAULT_PROFILE_COMMENT.format(profile_name=new_name) + "\n"
648
+
649
+ with open(ENV_FILE, 'w') as file:
650
+ file.writelines(content)
651
+
652
+ console.print(f"[green]Renamed {profile_to_rename} to {new_name} successfully.[/]")
653
+ except Exception as e:
654
+ console.print(f"[red]Error renaming profile: {str(e)}[/]")
655
+
656
+
657
+ def delete_profile():
658
+ """Delete an existing profile."""
659
+ available_profiles = get_available_profiles()
660
+ current_profile = get_current_profile()
661
+
662
+ if not available_profiles:
663
+ console.print("[yellow]No profiles available to delete.[/]")
664
+ return
665
+
666
+ # Let user select a profile to delete
667
+ profile_to_delete = questionary.select(
668
+ "Select a profile to delete:",
669
+ choices=available_profiles + ["Cancel"],
670
+ ).ask()
671
+
672
+ if profile_to_delete == "Cancel":
673
+ return
674
+
675
+ # Confirm deletion
676
+ confirm = questionary.confirm(
677
+ f"Are you sure you want to delete the {profile_to_delete} profile? This cannot be undone."
678
+ ).ask()
679
+
680
+ if not confirm:
681
+ return
682
+
683
+ # Delete profile file
684
+ profile_file = f"{ENV_PROFILE_PREFIX}{profile_to_delete}"
685
+
686
+ try:
687
+ os.remove(profile_file)
688
+
689
+ # Warn if deleting the current profile
690
+ if profile_to_delete == current_profile:
691
+ console.print(
692
+ f"[yellow]Warning: You deleted the currently active profile. "
693
+ f"The .env file still contains those settings but is no longer marked as a profile.[/]"
694
+ )
695
+
696
+ # Remove profile header from .env
697
+ with open(ENV_FILE, 'r') as file:
698
+ content = file.readlines()
699
+
700
+ if content and content[0].startswith("# Profile:"):
701
+ content = content[1:]
702
+ with open(ENV_FILE, 'w') as file:
703
+ file.writelines(content)
704
+
705
+ console.print(f"[green]Deleted {profile_to_delete} profile successfully.[/]")
706
+ except Exception as e:
707
+ console.print(f"[red]Error deleting profile: {str(e)}[/]")
708
+
709
+
710
+ def is_sensitive(key: str) -> bool:
711
+ """Check if a variable is considered sensitive.
712
+
713
+ Args:
714
+ key: The variable name
715
+
716
+ Returns:
717
+ True if sensitive, False otherwise
718
+ """
719
+ sensitive_patterns = ['key', 'token', 'secret', 'password', 'api', 'pat']
720
+ key_lower = key.lower()
721
+ return any(pattern in key_lower for pattern in sensitive_patterns)
722
+
723
+
724
+ def mask_sensitive_value(value: str) -> str:
725
+ """Mask a sensitive value.
726
+
727
+ Args:
728
+ value: The sensitive value
729
+
730
+ Returns:
731
+ Masked value
732
+ """
733
+ if not value:
734
+ return value
735
+
736
+ if len(value) <= 4:
737
+ return "••••"
738
+
739
+ # Show first 2 and last 2 characters
740
+ return value[:2] + "•" * (len(value) - 4) + value[-2:]
741
+
742
+
743
+ def get_current_profile() -> Optional[str]:
744
+ """Get the name of the current active profile.
745
+
746
+ Returns:
747
+ Profile name or None if no profile is active
748
+ """
749
+ if not os.path.exists(ENV_FILE):
750
+ return None
751
+
752
+ try:
753
+ with open(ENV_FILE, 'r') as file:
754
+ first_line = file.readline().strip()
755
+
756
+ if first_line.startswith("# Profile:"):
757
+ return first_line.replace("# Profile:", "").strip()
758
+ except Exception:
759
+ pass
760
+
761
+ return None
762
+
763
+
764
+ def get_available_profiles() -> List[str]:
765
+ """Get a list of available profiles.
766
+
767
+ Returns:
768
+ List of profile names
769
+ """
770
+ profiles = []
771
+
772
+ for file in os.listdir():
773
+ if file.startswith(ENV_PROFILE_PREFIX):
774
+ profile_name = file[len(ENV_PROFILE_PREFIX):]
775
+ profiles.append(profile_name)
776
+
777
+ return profiles
778
+
779
+
780
+ def backup_env_file():
781
+ """Create a backup of the current .env file."""
782
+ if not os.path.exists(ENV_FILE):
783
+ return
784
+
785
+ backup_file = f"{ENV_FILE}.bak"
786
+ shutil.copy2(ENV_FILE, backup_file)
787
+
788
+
789
+ def load_env_file() -> Dict[str, str]:
790
+ """Load the .env file into a dictionary.
791
+
792
+ Returns:
793
+ Dictionary of environment variables
794
+ """
795
+ env_vars = {}
796
+
797
+ if not os.path.exists(ENV_FILE):
798
+ console.print(f"[yellow]Warning: {ENV_FILE} file not found.[/]")
799
+ return env_vars
800
+
801
+ try:
802
+ with open(ENV_FILE, 'r') as file:
803
+ lines = file.readlines()
804
+
805
+ # Process each line
806
+ for line in lines:
807
+ line = line.strip()
808
+
809
+ # Skip empty lines
810
+ if not line:
811
+ env_vars[""] = ""
812
+ continue
813
+
814
+ # Handle comments
815
+ if line.startswith('#'):
816
+ env_vars[line] = ""
817
+ continue
818
+
819
+ # Handle regular variables
820
+ if '=' in line:
821
+ key, value = line.split('=', 1)
822
+ env_vars[key] = value
823
+ else:
824
+ # Handle lines without equals sign
825
+ env_vars[line] = ""
826
+
827
+ except Exception as e:
828
+ console.print(f"[red]Error loading .env file: {str(e)}[/]")
829
+
830
+ return env_vars
831
+
832
+
833
+ def save_env_file(env_vars: Dict[str, str]):
834
+ """Save environment variables back to the .env file.
835
+
836
+ Args:
837
+ env_vars: Dictionary of environment variables
838
+ """
839
+ # Create backup
840
+ backup_env_file()
841
+
842
+ try:
843
+ with open(ENV_FILE, 'w') as file:
844
+ for key, value in env_vars.items():
845
+ if key.startswith('#'):
846
+ # Write comments as is
847
+ file.write(f"{key}\n")
848
+ elif not key:
849
+ # Write empty lines
850
+ file.write("\n")
851
+ else:
852
+ # Write regular variables
853
+ file.write(f"{key}={value}\n")
854
+
855
+ console.print("[green]Settings saved successfully.[/]")
856
+ except Exception as e:
857
+ console.print(f"[red]Error saving .env file: {str(e)}[/]")
@@ -74,8 +74,8 @@ class FlockContext(Serializable, BaseModel):
74
74
  attributes={"agent": agent_name, "timestamp": timestamp},
75
75
  )
76
76
 
77
- def get_variable(self, key: str) -> Any:
78
- return self.state.get(key)
77
+ def get_variable(self, key: str, default: Any = None) -> Any:
78
+ return self.state.get(key, default)
79
79
 
80
80
  def set_variable(self, key: str, value: Any) -> None:
81
81
  old_value = self.state.get(key)
flock/core/flock_agent.py CHANGED
@@ -101,13 +101,6 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
101
101
  with tracer.start_as_current_span("agent.initialize") as span:
102
102
  span.set_attribute("agent.name", self.name)
103
103
  span.set_attribute("inputs", str(inputs))
104
- if not self.context:
105
- self.context = FlockContext()
106
-
107
- if self.name not in self.context.agent_definitions:
108
- self.context.add_agent_definition(
109
- type(self), self.name, self.to_dict()
110
- )
111
104
 
112
105
  try:
113
106
  for module in self.get_enabled_modules():
@@ -137,7 +130,7 @@ class FlockAgent(BaseModel, ABC, DSPyIntegrationMixin):
137
130
  )
138
131
  try:
139
132
  for module in self.get_enabled_modules():
140
- await module.terminate(self, inputs, inputs, self.context)
133
+ await module.terminate(self, inputs, result, self.context)
141
134
  except Exception as module_error:
142
135
  logger.error(
143
136
  "Error during terminate",
@@ -20,6 +20,10 @@ class FlockEvaluatorConfig(BaseModel):
20
20
  save_after_update: bool = Field(default=True)
21
21
  """
22
22
 
23
+ model: str = Field(
24
+ default="", description="The model to use for evaluation"
25
+ )
26
+
23
27
  @classmethod
24
28
  def with_fields(cls: type[T], **field_definitions) -> type[T]:
25
29
  """Create a new config class with additional fields."""
@@ -36,6 +36,7 @@ class FlockFactory:
36
36
  alert_latency_threshold_ms: int = 30000,
37
37
  no_output: bool = False,
38
38
  print_context: bool = False,
39
+ write_to_file: bool = False,
39
40
  ) -> FlockAgent:
40
41
  """Creates a default FlockAgent.
41
42
 
@@ -67,6 +68,7 @@ class FlockFactory:
67
68
  wait_for_input=wait_for_input,
68
69
  no_output=no_output,
69
70
  print_context=print_context,
71
+ write_to_file=write_to_file,
70
72
  )
71
73
  output_module = OutputModule("output", config=output_config)
72
74
 
@@ -416,7 +416,6 @@ class ThemedAgentResultFormatter:
416
416
  max_length: int = -1,
417
417
  render_table: bool = True,
418
418
  wait_for_input: bool = False,
419
- write_to_file: bool = False,
420
419
  ):
421
420
  """Initialize the formatter with a theme and optional max length."""
422
421
  self.theme = theme
@@ -424,7 +423,6 @@ class ThemedAgentResultFormatter:
424
423
  self.max_length = max_length
425
424
  self.render_table = render_table
426
425
  self.wait_for_input = wait_for_input
427
- self.write_to_file = write_to_file
428
426
 
429
427
  def format_result(
430
428
  self,
@@ -484,11 +482,6 @@ class ThemedAgentResultFormatter:
484
482
 
485
483
  s = pformat(result, highlight=False)
486
484
 
487
- if self.write_to_file:
488
- output_file = pathlib.Path(f"{agent_name}_result.txt")
489
- with open(output_file, "w") as f:
490
- f.write(s)
491
-
492
485
  if self.render_table:
493
486
  return Panel(
494
487
  table,
@@ -0,0 +1,7 @@
1
+ """Serialization utilities for Flock objects."""
2
+
3
+ from flock.core.serialization.json_encoder import FlockJSONEncoder
4
+ from flock.core.serialization.secure_serializer import SecureSerializer
5
+ from flock.core.serialization.serializable import Serializable
6
+
7
+ __all__ = ["FlockJSONEncoder", "SecureSerializer", "Serializable"]
@@ -0,0 +1,41 @@
1
+ """JSON encoder utilities for Flock objects."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+
8
+ class FlockJSONEncoder(json.JSONEncoder):
9
+ """Custom JSON encoder for handling Pydantic models and other non-serializable objects."""
10
+
11
+ def default(self, obj: Any) -> Any:
12
+ from pydantic import BaseModel
13
+
14
+ # Handle Pydantic models
15
+ if isinstance(obj, BaseModel):
16
+ return obj.model_dump()
17
+
18
+ # Handle datetime objects
19
+ if isinstance(obj, datetime):
20
+ return obj.isoformat()
21
+
22
+ # Handle sets, convert to list
23
+ if isinstance(obj, set):
24
+ return list(obj)
25
+
26
+ # Handle objects with a to_dict method
27
+ if hasattr(obj, "to_dict") and callable(getattr(obj, "to_dict")):
28
+ return obj.to_dict()
29
+
30
+ # Handle objects with a __dict__ attribute
31
+ if hasattr(obj, "__dict__"):
32
+ return {
33
+ k: v for k, v in obj.__dict__.items() if not k.startswith("_")
34
+ }
35
+
36
+ # Let the parent class handle it or raise TypeError
37
+ try:
38
+ return super().default(obj)
39
+ except TypeError:
40
+ # If all else fails, convert to string
41
+ return str(obj)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.3.18
3
+ Version: 0.3.21
4
4
  Summary: Declarative LLM Orchestration at Scale
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -19,6 +19,7 @@ Requires-Dist: httpx>=0.28.1
19
19
  Requires-Dist: loguru>=0.7.3
20
20
  Requires-Dist: matplotlib>=3.10.0
21
21
  Requires-Dist: msgpack>=1.1.0
22
+ Requires-Dist: notion-client>=2.3.0
22
23
  Requires-Dist: opentelemetry-api>=1.30.0
23
24
  Requires-Dist: opentelemetry-exporter-jaeger-proto-grpc>=1.21.0
24
25
  Requires-Dist: opentelemetry-exporter-jaeger>=1.21.0
@@ -1,4 +1,4 @@
1
- flock/__init__.py,sha256=9BfhaVhkV3ZadIXp6xpZYlt8_N_OBmQSxxJHwpXYPic,1867
1
+ flock/__init__.py,sha256=49_Z1uLb-fA2InJtNHQleB3NDpfamZGq2S01yttJoVI,2122
2
2
  flock/config.py,sha256=O5QJGlStf4DWSK4ovZsKw01ud4YK3_ij6Ay8sWU8ih0,1522
3
3
  flock/cli/constants.py,sha256=UWshD25IGos5Ks4P9paGPalY8BXy9YynzHk6HB4xfOo,669
4
4
  flock/cli/create_agent.py,sha256=DkeLUlrb7rGx3nZ04aADU9HXXu5mZTf_DBwT0xhzIv4,7
@@ -7,17 +7,17 @@ flock/cli/load_agent.py,sha256=DkeLUlrb7rGx3nZ04aADU9HXXu5mZTf_DBwT0xhzIv4,7
7
7
  flock/cli/load_examples.py,sha256=DkeLUlrb7rGx3nZ04aADU9HXXu5mZTf_DBwT0xhzIv4,7
8
8
  flock/cli/load_flock.py,sha256=3JdECvt5X7uyOG2vZS3-Zk5C5SI_84_QZjcsB3oJmfA,932
9
9
  flock/cli/load_release_notes.py,sha256=qFcgUrMddAE_TP6x1P-6ZywTUjTknfhTDW5LTxtg1yk,599
10
- flock/cli/settings.py,sha256=DkeLUlrb7rGx3nZ04aADU9HXXu5mZTf_DBwT0xhzIv4,7
10
+ flock/cli/settings.py,sha256=Z_TXBzCYlCmSaKrJ_CQCdYy-Cj29gpI4kbC_2KzoKqg,27025
11
11
  flock/cli/assets/release_notes.md,sha256=bqnk50jxM3w5uY44Dc7MkdT8XmRREFxrVBAG9XCOSSU,4896
12
12
  flock/core/__init__.py,sha256=mPlvKc0SxC2qCvSlgYeP_7EyV8ptmdn24NO8mlQoCSo,559
13
13
  flock/core/flock.py,sha256=IURlcuNvdsnqKkvgXtX4v_pGWQ8Lfb60X--MT0zvxHo,19881
14
- flock/core/flock_agent.py,sha256=QPyRSa1X_aAK2MSgqLNHBiL-_cnYHOSSnrFup2YTzss,12509
14
+ flock/core/flock_agent.py,sha256=F1wl54ivLQijSHlv_Q_znw_ElzdDlqpi-KugVUyHTRE,12238
15
15
  flock/core/flock_api.py,sha256=2rHnmEdtT5KPZYwGesRT7LqwbrgKClODHT-O56u7pcQ,7140
16
- flock/core/flock_evaluator.py,sha256=j7riJj_KsWoBnKmLiGp-U0CRhxDyJbgEdLGN26tfKm8,1588
17
- flock/core/flock_factory.py,sha256=7nV0WAh197INdBckJ-NhnhSSZOSzZC1MlYfeZm2x8Xc,2750
16
+ flock/core/flock_evaluator.py,sha256=dOXZeDOGZcAmJ9ahqq_2bdGUU1VOXY4skmwTVpAjiVw,1685
17
+ flock/core/flock_factory.py,sha256=MGTkJCP1WGpV614f87r1vwe0tqAvBCoH9PlqtqDyJDk,2828
18
18
  flock/core/flock_module.py,sha256=96aFVYAgwpKN53xGbivQDUpikOYGFCxK5mqhclOcxY0,3003
19
19
  flock/core/flock_router.py,sha256=A5GaxcGvtiFlRLHBTW7okh5RDm3BdKam2uXvRHRaj7k,2187
20
- flock/core/context/context.py,sha256=3sj5BrnY7OBG8Xk-uoT0yPKQjCzUU6a3VH26xuHo2KI,6407
20
+ flock/core/context/context.py,sha256=8bjRLZ74oacRNBaHmDNXdQKfB-95poF7Pp03n2k0zcQ,6437
21
21
  flock/core/context/context_manager.py,sha256=FANSWa6DEhdhtZ7t_9Gza0v80UdpoDOhHbfVOccmjkA,1181
22
22
  flock/core/context/context_vars.py,sha256=zYTMi9b6mNSSEHowEQUOTpEDurmAjaUcyBCgfKY6-cU,300
23
23
  flock/core/execution/local_executor.py,sha256=rnIQvaJOs6zZORUcR3vvyS6LPREDJTjaygl_Db0M8ao,952
@@ -29,7 +29,7 @@ flock/core/logging/telemetry.py,sha256=3E9Tyj6AUR6A5RlIufcdCdWm5BAA7tbOsCa7lHoUQ
29
29
  flock/core/logging/trace_and_logged.py,sha256=5vNrK1kxuPMoPJ0-QjQg-EDJL1oiEzvU6UNi6X8FiMs,2117
30
30
  flock/core/logging/formatters/enum_builder.py,sha256=LgEYXUv84wK5vwHflZ5h8HBGgvLH3sByvUQe8tZiyY0,981
31
31
  flock/core/logging/formatters/theme_builder.py,sha256=Wnaal3HuUDA4HFg9tdql1BxYwK83ACOZBBQy-DXnxcA,17342
32
- flock/core/logging/formatters/themed_formatter.py,sha256=6WO9RPr6Su05rJEX9bRXd8peE-QzGCQeO5veIjQH-Vc,21086
32
+ flock/core/logging/formatters/themed_formatter.py,sha256=kCmGXLD8yzhfzENJUQOsqX3Sdo2PuN8JIZvBBWO22JI,20834
33
33
  flock/core/logging/formatters/themes.py,sha256=80BRJJB0LZr11N0yQw2f8vdb_9179qjQO8PoeBaLMN0,10680
34
34
  flock/core/logging/span_middleware/baggage_span_processor.py,sha256=gJfRl8FeB6jdtghTaRHCrOaTo4fhPMRKgjqtZj-8T48,1118
35
35
  flock/core/logging/telemetry_exporter/base_exporter.py,sha256=rQJJzS6q9n2aojoSqwCnl7ZtHrh5LZZ-gkxUuI5WfrQ,1124
@@ -38,6 +38,8 @@ flock/core/logging/telemetry_exporter/sqlite_exporter.py,sha256=CDsiMb9QcqeXelZ6
38
38
  flock/core/mixin/dspy_integration.py,sha256=P5G4Y04nl5hFwFbJXCkQ-0TMR1L4skLL2IM_FlUjH_c,8364
39
39
  flock/core/mixin/prompt_parser.py,sha256=eOqI-FK3y17gVqpc_y5GF-WmK1Jv8mFlkZxTcgweoxI,5121
40
40
  flock/core/registry/agent_registry.py,sha256=TUClh9e3eA6YzZC1CMTlsTPvQeqb9jYHewi-zPpcWM8,4987
41
+ flock/core/serialization/__init__.py,sha256=j0uw1g-7SOEhD5Ozgc8WpbZNpDWRmu58UOGg4_QTu8U,320
42
+ flock/core/serialization/json_encoder.py,sha256=gAKj2zU_8wQiNvdkby2hksSA4fbPNwTjup_yz1Le1Vw,1229
41
43
  flock/core/serialization/secure_serializer.py,sha256=n5-zRvvXddgJv1FFHsaQ2wuYdL3WUSGPvG_LGaffEJo,6144
42
44
  flock/core/serialization/serializable.py,sha256=SymJ0YrjBx48mOBItYSqoRpKuzIc4vKWRS6ScTzre7s,2573
43
45
  flock/core/tools/azure_tools.py,sha256=9Bi6IrB5pzBTBhBSxpCVMgx8HBud8nl4gDp8aN0NT6c,17031
@@ -58,7 +60,6 @@ flock/modules/callback/callback_module.py,sha256=volGGgHtY19qj1wHR6m5a_hmXSbV3Ca
58
60
  flock/modules/memory/memory_module.py,sha256=dZ30eOFqIlAz0a5IKJMoXgJ-VyPEqApAOX0OQjhGA1I,14733
59
61
  flock/modules/memory/memory_parser.py,sha256=FLH7GL8XThvHiCMfX3eQH7Sz-f62fzhAUmO6_gaDI7U,4372
60
62
  flock/modules/memory/memory_storage.py,sha256=CNcLDMmvv0x7Z3YMKr6VveS_VCa7rKPw8l2d-XgqokA,27246
61
- flock/modules/output/output_module.py,sha256=MPs5QV5g5DZQGqR07dS0eBug_TGpkdjX5zTrcrDphq8,7467
62
63
  flock/modules/performance/metrics_module.py,sha256=UD9OjY4-zAvauMD7YyDYqE1gyIhzpdr3JkBT8j9knxY,16790
63
64
  flock/modules/zep/zep_module.py,sha256=x7JG6O6xnwwum0RETIqKYbA3xzdcvX2aUuns0Cl0c2Q,6014
64
65
  flock/platform/docker_tools.py,sha256=fpA7-6rJBjPOUBLdQP4ny2QPgJ_042nmqRn5GtKnoYw,1445
@@ -412,8 +413,8 @@ flock/workflow/activities.py,sha256=yah-lHjMW6_Ww1gt7hMXBis1cJRlcbHx0uLsMB9oNZ0,
412
413
  flock/workflow/agent_activities.py,sha256=NhBZscflEf2IMfSRa_pBM_TRP7uVEF_O0ROvWZ33eDc,963
413
414
  flock/workflow/temporal_setup.py,sha256=VWBgmBgfTBjwM5ruS_dVpA5AVxx6EZ7oFPGw4j3m0l0,1091
414
415
  flock/workflow/workflow.py,sha256=I9MryXW_bqYVTHx-nl2epbTqeRy27CAWHHA7ZZA0nAk,1696
415
- flock_core-0.3.18.dist-info/METADATA,sha256=NiwExibjtEysHFQNtBSV0KMH36KFkeU6WFh0VEohXe4,20584
416
- flock_core-0.3.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
417
- flock_core-0.3.18.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
418
- flock_core-0.3.18.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
419
- flock_core-0.3.18.dist-info/RECORD,,
416
+ flock_core-0.3.21.dist-info/METADATA,sha256=IUx4L0DUqkIx1ZWXBoztYmNDzD85S_zy8PdnPNGXiDU,20620
417
+ flock_core-0.3.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
418
+ flock_core-0.3.21.dist-info/entry_points.txt,sha256=rWaS5KSpkTmWySURGFZk6PhbJ87TmvcFQDi2uzjlagQ,37
419
+ flock_core-0.3.21.dist-info/licenses/LICENSE,sha256=iYEqWy0wjULzM9GAERaybP4LBiPeu7Z1NEliLUdJKSc,1072
420
+ flock_core-0.3.21.dist-info/RECORD,,
@@ -1,211 +0,0 @@
1
- """Output formatting and display functionality for agents."""
2
-
3
- import json
4
- import os
5
- from datetime import datetime
6
- from typing import Any
7
-
8
- from pydantic import Field
9
-
10
- from flock.core import FlockAgent
11
- from flock.core.context.context import FlockContext
12
- from flock.core.flock_module import FlockModule, FlockModuleConfig
13
- from flock.core.logging.formatters.themed_formatter import (
14
- ThemedAgentResultFormatter,
15
- )
16
- from flock.core.logging.formatters.themes import OutputTheme
17
- from flock.core.logging.logging import get_logger
18
-
19
- logger = get_logger("module.output")
20
-
21
-
22
- class OutputModuleConfig(FlockModuleConfig):
23
- """Configuration for output formatting and display."""
24
-
25
- theme: OutputTheme = Field(
26
- default=OutputTheme.afterglow, description="Theme for output formatting"
27
- )
28
- render_table: bool = Field(
29
- default=False, description="Whether to render output as a table"
30
- )
31
- max_length: int = Field(
32
- default=1000, description="Maximum length for displayed output"
33
- )
34
- wait_for_input: bool = Field(
35
- default=False,
36
- description="Whether to wait for user input after display",
37
- )
38
- write_to_file: bool = Field(
39
- default=False, description="Whether to save output to file"
40
- )
41
- output_dir: str = Field(
42
- default="output/", description="Directory for saving output files"
43
- )
44
- truncate_long_values: bool = Field(
45
- default=True, description="Whether to truncate long values in display"
46
- )
47
- show_metadata: bool = Field(
48
- default=True, description="Whether to show metadata like timestamps"
49
- )
50
- format_code_blocks: bool = Field(
51
- default=True,
52
- description="Whether to apply syntax highlighting to code blocks",
53
- )
54
- custom_formatters: dict[str, str] = Field(
55
- default_factory=dict,
56
- description="Custom formatters for specific output types",
57
- )
58
- no_output: bool = Field(
59
- default=False,
60
- description="Whether to suppress output",
61
- )
62
- print_context: bool = Field(
63
- default=False,
64
- description="Whether to print the context",
65
- )
66
-
67
-
68
- class OutputModule(FlockModule):
69
- """Module that handles output formatting and display."""
70
-
71
- name: str = "output"
72
- config: OutputModuleConfig = Field(
73
- default_factory=OutputModuleConfig, description="Output configuration"
74
- )
75
-
76
- def __init__(self, name: str, config: OutputModuleConfig):
77
- super().__init__(name=name, config=config)
78
- if self.config.write_to_file:
79
- os.makedirs(self.config.output_dir, exist_ok=True)
80
- self._formatter = ThemedAgentResultFormatter(
81
- theme=self.config.theme,
82
- max_length=self.config.max_length,
83
- render_table=self.config.render_table,
84
- wait_for_input=self.config.wait_for_input,
85
- write_to_file=self.config.write_to_file,
86
- )
87
-
88
- def _format_value(self, value: Any, key: str) -> str:
89
- """Format a single value based on its type and configuration."""
90
- # Check for custom formatter
91
- if key in self.config.custom_formatters:
92
- formatter_name = self.config.custom_formatters[key]
93
- if hasattr(self, f"_format_{formatter_name}"):
94
- return getattr(self, f"_format_{formatter_name}")(value)
95
-
96
- # Default formatting based on type
97
- if isinstance(value, dict):
98
- return self._format_dict(value)
99
- elif isinstance(value, list):
100
- return self._format_list(value)
101
- elif isinstance(value, str) and self.config.format_code_blocks:
102
- return self._format_potential_code(value)
103
- else:
104
- return str(value)
105
-
106
- def _format_dict(self, d: dict[str, Any], indent: int = 0) -> str:
107
- """Format a dictionary with proper indentation."""
108
- lines = []
109
- for k, v in d.items():
110
- formatted_value = self._format_value(v, k)
111
- if (
112
- self.config.truncate_long_values
113
- and len(formatted_value) > self.config.max_length
114
- ):
115
- formatted_value = (
116
- formatted_value[: self.config.max_length] + "..."
117
- )
118
- lines.append(f"{' ' * indent}{k}: {formatted_value}")
119
- return "\n".join(lines)
120
-
121
- def _format_list(self, lst: list[Any]) -> str:
122
- """Format a list with proper indentation."""
123
- return "\n".join(f"- {self._format_value(item, '')}" for item in lst)
124
-
125
- def _format_potential_code(self, text: str) -> str:
126
- """Format text that might contain code blocks."""
127
- import re
128
-
129
- def replace_code_block(match):
130
- code = match.group(2)
131
- lang = match.group(1) if match.group(1) else ""
132
- # Here you could add syntax highlighting
133
- return f"```{lang}\n{code}\n```"
134
-
135
- # Replace code blocks with formatted versions
136
- text = re.sub(
137
- r"```(\w+)?\n(.*?)\n```", replace_code_block, text, flags=re.DOTALL
138
- )
139
- return text
140
-
141
- def _save_output(self, agent_name: str, result: dict[str, Any]) -> None:
142
- """Save output to file if configured."""
143
- if not self.config.write_to_file:
144
- return
145
-
146
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
147
- filename = f"{agent_name}_output_{timestamp}.json"
148
- filepath = os.path.join(self.config.output_dir, filename)
149
-
150
- output_data = {
151
- "agent": agent_name,
152
- "timestamp": timestamp,
153
- "result": result,
154
- }
155
-
156
- if self.config.show_metadata:
157
- output_data["metadata"] = {
158
- "formatted_at": datetime.now().isoformat(),
159
- "theme": self.config.theme.value,
160
- "max_length": self.config.max_length,
161
- }
162
-
163
- with open(filepath, "w") as f:
164
- json.dump(output_data, f, indent=2)
165
-
166
- async def post_evaluate(
167
- self,
168
- agent: FlockAgent,
169
- inputs: dict[str, Any],
170
- result: dict[str, Any],
171
- context: FlockContext | None = None,
172
- ) -> dict[str, Any]:
173
- """Format and display the output."""
174
- logger.debug("Formatting and displaying output")
175
- if self.config.no_output:
176
- return result
177
- if self.config.print_context:
178
- result["context"] = context
179
- # Display the result using the formatter
180
- self._formatter.display_result(result, agent.name)
181
-
182
- # Save to file if configured
183
- self._save_output(agent.name, result)
184
-
185
- return result
186
-
187
- def update_theme(self, new_theme: OutputTheme) -> None:
188
- """Update the output theme."""
189
- self.config.theme = new_theme
190
- self._formatter = ThemedAgentResultFormatter(
191
- theme=self.config.theme,
192
- max_length=self.config.max_length,
193
- render_table=self.config.render_table,
194
- wait_for_input=self.config.wait_for_input,
195
- write_to_file=self.config.write_to_file,
196
- )
197
-
198
- def add_custom_formatter(self, key: str, formatter_name: str) -> None:
199
- """Add a custom formatter for a specific output key."""
200
- self.config.custom_formatters[key] = formatter_name
201
-
202
- def get_output_files(self) -> list[str]:
203
- """Get list of saved output files."""
204
- if not self.config.write_to_file:
205
- return []
206
-
207
- return [
208
- f
209
- for f in os.listdir(self.config.output_dir)
210
- if f.endswith("_output.json")
211
- ]