flock-core 0.3.41__py3-none-any.whl → 0.4.0b1__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.

@@ -1,8 +1,10 @@
1
1
  """Registry Management Module for the Flock CLI."""
2
2
 
3
+ import datetime
3
4
  import importlib
4
5
  import inspect
5
6
  import os
7
+ from dataclasses import is_dataclass
6
8
  from pathlib import Path
7
9
  from typing import Any
8
10
 
@@ -137,9 +139,21 @@ def display_registry_section(
137
139
  table.add_column("Name/Path", style="cyan")
138
140
  table.add_column("Type", style="green")
139
141
 
142
+ # Add file path column for components
143
+ if title == "Components":
144
+ table.add_column("File Path", style="yellow")
145
+
140
146
  for name, item in filtered_items.items():
141
147
  item_type = type(item).__name__
142
- table.add_row(name, item_type)
148
+
149
+ if title == "Components":
150
+ # Try to get the file path for component classes
151
+ file_path = (
152
+ inspect.getfile(item) if inspect.isclass(item) else "N/A"
153
+ )
154
+ table.add_row(name, item_type, file_path)
155
+ else:
156
+ table.add_row(name, item_type)
143
157
 
144
158
  console.print(table)
145
159
  console.print(f"Total: {len(filtered_items)} {title.lower()}")
@@ -154,11 +168,72 @@ def add_item_to_registry() -> None:
154
168
  choices=["agent", "callable", "type", "component"],
155
169
  ).ask()
156
170
 
157
- module_path = questionary.text(
158
- "Enter the module path (e.g., 'your_module.submodule'):"
159
- ).ask()
171
+ # For component types, offer file path option
172
+ use_file_path = False
173
+ if item_type == "component":
174
+ path_type = questionary.select(
175
+ "How do you want to specify the component?",
176
+ choices=["Module Path", "File Path"],
177
+ ).ask()
178
+ use_file_path = path_type == "File Path"
179
+
180
+ if use_file_path:
181
+ file_path = questionary.path(
182
+ "Enter the file path to the component:", only_directories=False
183
+ ).ask()
184
+
185
+ if not file_path or not os.path.exists(file_path):
186
+ console.print(f"[red]Error: File {file_path} does not exist[/]")
187
+ return False
188
+
189
+ module_name = questionary.text(
190
+ "Enter the component class name in the file:"
191
+ ).ask()
192
+
193
+ try:
194
+ # Use dynamic import to load the module from file path
195
+ import importlib.util
160
196
 
161
- item_name = questionary.text("Enter the item name within the module:").ask()
197
+ spec = importlib.util.spec_from_file_location(
198
+ "temp_module", file_path
199
+ )
200
+ module = importlib.util.module_from_spec(spec)
201
+ spec.loader.exec_module(module)
202
+
203
+ if not hasattr(module, module_name):
204
+ console.print(
205
+ f"[red]Error: {module_name} not found in {file_path}[/]"
206
+ )
207
+ return False
208
+
209
+ item = getattr(module, module_name)
210
+ except Exception as e:
211
+ console.print(f"[red]Error importing from file: {e!s}[/]")
212
+ return False
213
+ else:
214
+ module_path = questionary.text(
215
+ "Enter the module path (e.g., 'your_module.submodule'):"
216
+ ).ask()
217
+
218
+ item_name = questionary.text(
219
+ "Enter the item name within the module:"
220
+ ).ask()
221
+
222
+ try:
223
+ # Attempt to import the module
224
+ module = importlib.import_module(module_path)
225
+
226
+ # Get the item from the module
227
+ if not hasattr(module, item_name):
228
+ console.print(
229
+ f"[red]Error: {item_name} not found in {module_path}[/]"
230
+ )
231
+ return False
232
+
233
+ item = getattr(module, item_name)
234
+ except Exception as e:
235
+ console.print(f"[red]Error importing module: {e!s}[/]")
236
+ return False
162
237
 
163
238
  alias = questionary.text(
164
239
  "Enter an alias (optional, press Enter to skip):"
@@ -167,20 +242,8 @@ def add_item_to_registry() -> None:
167
242
  if not alias:
168
243
  alias = None
169
244
 
245
+ # Register the item based on its type
170
246
  try:
171
- # Attempt to import the module
172
- module = importlib.import_module(module_path)
173
-
174
- # Get the item from the module
175
- if not hasattr(module, item_name):
176
- console.print(
177
- f"[red]Error: {item_name} not found in {module_path}[/]"
178
- )
179
- return False
180
-
181
- item = getattr(module, item_name)
182
-
183
- # Register the item based on its type
184
247
  if item_type == "agent":
185
248
  registry.register_agent(item)
186
249
  console.print(
@@ -196,18 +259,19 @@ def add_item_to_registry() -> None:
196
259
  console.print(f"[green]Successfully registered type: {result}[/]")
197
260
  elif item_type == "component":
198
261
  result = registry.register_component(item, alias)
262
+ # Store the file path information if we loaded from a file
263
+ if use_file_path and hasattr(registry, "_component_file_paths"):
264
+ # Check if the registry has component file paths attribute
265
+ # This will be added to registry in our update
266
+ registry._component_file_paths[result] = file_path
199
267
  console.print(
200
268
  f"[green]Successfully registered component: {result}[/]"
201
269
  )
202
-
203
- return True
204
-
205
- except ImportError:
206
- console.print(f"[red]Error: Could not import module {module_path}[/]")
207
270
  except Exception as e:
208
- console.print(f"[red]Error: {e!s}[/]")
271
+ console.print(f"[red]Error registering item: {e!s}[/]")
272
+ return False
209
273
 
210
- return False
274
+ return True
211
275
 
212
276
 
213
277
  def remove_item_from_registry() -> None:
@@ -275,56 +339,123 @@ def remove_item_from_registry() -> None:
275
339
 
276
340
 
277
341
  def auto_registration_scanner() -> None:
278
- """Scan directory for potential registry items and optionally register them."""
279
- # Ask for the target path
280
- target_path = questionary.text(
281
- "Enter the path to scan (file or directory):",
282
- default=os.getcwd(),
342
+ """Launch the auto-registration scanner interface."""
343
+ console.clear()
344
+ console.print(
345
+ Panel("[bold blue]Auto-Registration Scanner[/]"), justify="center"
346
+ )
347
+ console.line()
348
+
349
+ console.print(
350
+ "This utility will scan Python files for components, types, callables (tools), and agents that can be registered."
351
+ )
352
+ console.print(
353
+ "[yellow]Note: Registration is required for proper serialization and deserialization of your Flock.[/]"
354
+ )
355
+ console.line()
356
+
357
+ # Target directory selection
358
+ def path_filter(path):
359
+ """Filter paths for selection."""
360
+ if os.path.isdir(path):
361
+ return True
362
+ return path.endswith(".py")
363
+
364
+ target_path = questionary.path(
365
+ "Select directory to scan:", file_filter=path_filter
283
366
  ).ask()
284
367
 
285
- # Ask if we should recursively scan directories
286
- recursive = True
287
- if os.path.isdir(target_path):
288
- recursive = questionary.confirm(
289
- "Scan recursively through subdirectories?",
290
- default=True,
291
- ).ask()
368
+ if not target_path or not os.path.exists(target_path):
369
+ console.print("[red]Invalid path selected. Aborting.[/]")
370
+ return
371
+
372
+ is_recursive = questionary.confirm(
373
+ "Scan recursively (include subdirectories)?", default=True
374
+ ).ask()
292
375
 
293
- # Ask if we should auto-register or just preview
294
376
  auto_register = questionary.confirm(
295
- "Auto-register discovered items? (No for preview only)",
296
- default=False,
377
+ "Automatically register items found during scan?", default=True
297
378
  ).ask()
298
379
 
299
- # Perform the scan
300
- scan_results = scan_for_registry_items(
301
- target_path, recursive, auto_register
380
+ # Special callout for tools/callables
381
+ console.print(
382
+ "[bold blue]Tool Registration:[/] This scanner will look for functions that can be used as tools."
302
383
  )
384
+ console.print(
385
+ "These will be registered as callables and can be properly serialized in your Flock YAML."
386
+ )
387
+ console.line()
388
+
389
+ with Progress(
390
+ SpinnerColumn(),
391
+ TextColumn("[bold blue]{task.description}"),
392
+ BarColumn(),
393
+ TextColumn("[bold green]{task.completed}/{task.total}"),
394
+ console=console,
395
+ ) as progress:
396
+ task_id = progress.add_task(
397
+ "Scanning for registry items...", total=None
398
+ )
399
+
400
+ # Perform the scan
401
+ results = scan_for_registry_items(
402
+ target_path, recursive=is_recursive, auto_register=auto_register
403
+ )
404
+
405
+ # Mark task as complete
406
+ progress.update(task_id, completed=1, total=1)
407
+ console.line()
303
408
 
304
409
  # Display results
305
- console.print(Panel("[bold green]Scan Results[/]"), justify="center")
410
+ console.print("[bold green]Scan Complete![/]")
411
+ console.line()
412
+
413
+ total_found = sum(len(items) for items in results.values())
414
+ total_categories = sum(1 for items in results.values() if items)
415
+
416
+ console.print(
417
+ f"Found {total_found} items across {total_categories} categories."
418
+ )
419
+
420
+ # Enhanced report section
421
+ table = Table(title="Scan Results")
422
+ table.add_column("Category", style="cyan")
423
+ table.add_column("Count", style="green")
424
+ table.add_column("Example Items", style="blue")
306
425
 
307
- for category, items in scan_results.items():
426
+ for category, items in results.items():
308
427
  if items:
309
- console.print(f"\n[cyan]{category}:[/] {len(items)} items")
310
- for item in items:
311
- console.print(f" - {item}")
428
+ examples = ", ".join(items[:3])
429
+ if len(items) > 3:
430
+ examples += ", ..."
431
+ table.add_row(category, str(len(items)), examples)
432
+ else:
433
+ table.add_row(category, "0", "")
312
434
 
313
- if auto_register:
314
- console.print("\n[green]Items have been registered to the registry.[/]")
315
- else:
316
- # Ask if we want to register the detected items
317
- register_now = questionary.confirm(
318
- "Register these items now?",
319
- default=False,
435
+ console.print(table)
436
+ console.line()
437
+
438
+ # Callout for tools and future serialization
439
+ if results.get("callables"):
440
+ console.print(
441
+ "[bold green]Note:[/] Found callable functions that can be used as tools."
442
+ )
443
+ console.print(
444
+ "These functions will now be properly serialized as callable references in your Flock YAML."
445
+ )
446
+ console.print(
447
+ "When sharing Flocks, ensure these callables are registered on the target system."
448
+ )
449
+ console.line()
450
+
451
+ # Show details options
452
+ if total_found > 0:
453
+ view_details = questionary.confirm(
454
+ "Would you like to view detailed results?", default=True
320
455
  ).ask()
321
456
 
322
- if register_now:
323
- # Re-scan with auto-register=True
324
- scan_for_registry_items(target_path, recursive, True)
325
- console.print(
326
- "\n[green]Items have been registered to the registry.[/]"
327
- )
457
+ if view_details:
458
+ view_registry_contents() # Show the registry contents after scan
328
459
 
329
460
 
330
461
  def scan_for_registry_items(
@@ -517,8 +648,6 @@ def has_component_base(cls: type) -> bool:
517
648
  def is_potential_type(cls: type) -> bool:
518
649
  """Check if a class is a Pydantic model or dataclass."""
519
650
  try:
520
- from dataclasses import is_dataclass
521
-
522
651
  from pydantic import BaseModel
523
652
 
524
653
  return issubclass(cls, BaseModel) or is_dataclass(cls)
@@ -559,59 +688,201 @@ def is_potential_registry_candidate(obj: Any) -> bool:
559
688
 
560
689
 
561
690
  def export_registry() -> None:
562
- """Export the current registry state to a file."""
691
+ """Export registry contents to a file."""
563
692
  registry = get_registry()
564
693
 
565
- # Choose export format
694
+ # Select what to export
695
+ export_items = questionary.checkbox(
696
+ "Select what to export:",
697
+ choices=[
698
+ questionary.Choice("Agents", checked=True),
699
+ questionary.Choice("Callables (Tools)", checked=True),
700
+ questionary.Choice("Types", checked=True),
701
+ questionary.Choice("Components", checked=True),
702
+ questionary.Choice("File Paths", checked=True),
703
+ ],
704
+ ).ask()
705
+
706
+ if not export_items:
707
+ console.print("[yellow]No items selected for export.[/]")
708
+ return
709
+
710
+ # Select export format
566
711
  export_format = questionary.select(
567
712
  "Select export format:",
568
- choices=["YAML", "JSON", "Text Report"],
713
+ choices=["YAML", "JSON", "Python"],
714
+ ).ask()
715
+
716
+ # Select path type for serialization
717
+ path_type = questionary.select(
718
+ "How should file paths be formatted?",
719
+ choices=[
720
+ "absolute (full paths, best for local use)",
721
+ "relative (relative paths, better for sharing)",
722
+ ],
723
+ default="absolute (full paths, best for local use)",
569
724
  ).ask()
570
725
 
571
- # Choose export path
572
- export_path = questionary.text(
573
- "Enter export file path:",
726
+ # Extract just the first word
727
+ path_type = path_type.split()[0]
728
+
729
+ console.print(
730
+ f"\n[bold]Path type selected: [green]{path_type}[/green][/bold]"
731
+ )
732
+ if path_type == "relative":
733
+ console.print(
734
+ "Relative paths are recommended when sharing Flocks between systems.\n"
735
+ "They'll be converted to paths relative to the current directory."
736
+ )
737
+ else:
738
+ console.print(
739
+ "Absolute paths work best for local usage but may not work correctly\n"
740
+ "when sharing with others or moving files."
741
+ )
742
+ console.line()
743
+
744
+ # Get file path for export
745
+ file_path = questionary.path(
746
+ "Enter file path for export:",
574
747
  default=f"flock_registry_export.{export_format.lower()}",
575
748
  ).ask()
576
749
 
577
- try:
578
- export_data = {
579
- "agents": list(registry._agents.keys()),
580
- "callables": list(registry._callables.keys()),
581
- "types": list(registry._types.keys()),
582
- "components": list(registry._components.keys()),
583
- }
750
+ if not file_path:
751
+ return
752
+
753
+ # Prepare export data
754
+ export_data = {}
755
+
756
+ if "Agents" in export_items:
757
+ export_data["agents"] = list(registry._agents.keys())
758
+
759
+ if "Callables (Tools)" in export_items:
760
+ export_data["callables"] = list(registry._callables.keys())
761
+
762
+ # Add serialization format information for tools
763
+ callable_details = {}
764
+ for callable_name in registry._callables.keys():
765
+ callable_obj = registry._callables[callable_name]
766
+ file_path_value = (
767
+ inspect.getfile(callable_obj)
768
+ if callable_obj and inspect.isfunction(callable_obj)
769
+ else "Unknown"
770
+ )
771
+
772
+ # Convert to relative path if needed
773
+ if path_type == "relative" and file_path_value != "Unknown":
774
+ try:
775
+ file_path_value = os.path.relpath(file_path_value)
776
+ except ValueError:
777
+ # Keep as absolute if can't make relative
778
+ pass
779
+
780
+ callable_details[callable_name] = {
781
+ "module": callable_obj.__module__,
782
+ "file": file_path_value,
783
+ "type": "function"
784
+ if inspect.isfunction(callable_obj)
785
+ else "other_callable",
786
+ }
787
+ export_data["callable_details"] = callable_details
788
+
789
+ if "Types" in export_items:
790
+ export_data["types"] = list(registry._types.keys())
791
+
792
+ if "Components" in export_items:
793
+ export_data["components"] = list(registry._components.keys())
794
+
795
+ # Include file paths if selected
796
+ if "File Paths" in export_items and hasattr(
797
+ registry, "_component_file_paths"
798
+ ):
799
+ export_data["component_file_paths"] = {}
800
+ for component_name in registry._components.keys():
801
+ # Get the file path if available
802
+ if component_name in registry._component_file_paths:
803
+ file_path_value = registry._component_file_paths[
804
+ component_name
805
+ ]
806
+
807
+ # Convert to relative path if needed
808
+ if path_type == "relative" and file_path_value:
809
+ try:
810
+ file_path_value = os.path.relpath(file_path_value)
811
+ except ValueError:
812
+ # Keep as absolute if can't make relative
813
+ pass
814
+
815
+ export_data["component_file_paths"][component_name] = (
816
+ file_path_value
817
+ )
584
818
 
819
+ # Add metadata about serialization format
820
+ export_data["metadata"] = {
821
+ "export_date": datetime.datetime.now().isoformat(),
822
+ "flock_version": "0.3.41", # Update with actual version
823
+ "serialization_format": {
824
+ "tools": "Callable reference names",
825
+ "components": "Module and class names",
826
+ "types": "Module and class names",
827
+ },
828
+ "path_type": path_type,
829
+ }
830
+
831
+ # Add serialization settings as a top-level element
832
+ export_data["serialization_settings"] = {"path_type": path_type}
833
+
834
+ try:
835
+ # Export the data
585
836
  if export_format == "YAML":
586
837
  import yaml
587
838
 
588
- with open(export_path, "w") as f:
589
- yaml.dump(export_data, f, sort_keys=False, indent=2)
590
-
839
+ with open(file_path, "w") as f:
840
+ yaml.dump(export_data, f, default_flow_style=False)
591
841
  elif export_format == "JSON":
592
842
  import json
593
843
 
594
- with open(export_path, "w") as f:
844
+ with open(file_path, "w") as f:
595
845
  json.dump(export_data, f, indent=2)
846
+ elif export_format == "Python":
847
+ with open(file_path, "w") as f:
848
+ f.write("# Flock Registry Export\n")
849
+ f.write(f"# Generated on {datetime.datetime.now()}\n\n")
850
+ f.write("registry_data = ")
851
+ f.write(repr(export_data))
852
+ f.write("\n")
853
+
854
+ console.print(f"[green]Registry exported to {file_path}[/]")
855
+ console.print(f"[green]Paths formatted as: {path_type}[/]")
856
+
857
+ # Print information about tool serialization if tools were exported
858
+ if "Callables (Tools)" in export_items and registry._callables:
859
+ console.print("\n[bold blue]Tool Serialization Information:[/]")
860
+ console.print(
861
+ "Tools in Flock are now serialized as callable references rather than dictionaries."
862
+ )
863
+ console.print(
864
+ "This makes YAML files more readable and simplifies tool management."
865
+ )
866
+ console.print("When loading a Flock with tools:")
867
+ console.print(" 1. Tools must be registered in the registry")
868
+ console.print(" 2. The tools' modules must be importable")
869
+ console.print(
870
+ " 3. Tool functions have the same signature across systems"
871
+ )
596
872
 
597
- elif export_format == "Text Report":
598
- with open(export_path, "w") as f:
599
- f.write("FLOCK REGISTRY EXPORT\n")
600
- f.write("====================\n\n")
601
-
602
- for category, items in export_data.items():
603
- f.write(f"{category.upper()} ({len(items)})\n")
604
- f.write(
605
- "-" * (len(category) + 2 + len(str(len(items)))) + "\n"
606
- )
607
- for item in sorted(items):
608
- f.write(f" - {item}\n")
609
- f.write("\n")
610
-
611
- console.print(f"[green]Registry exported to {export_path}[/]")
873
+ # Show example of how a tool would appear in YAML
874
+ if registry._callables:
875
+ console.print("\n[bold green]Example tool in YAML:[/]")
876
+ example_callable = next(iter(registry._callables.keys()))
877
+ console.print(
878
+ f" - {example_callable} # Function name reference"
879
+ )
880
+ console.print("instead of the old format:")
881
+ console.print(f" - __callable_ref__: {example_callable}")
612
882
 
613
883
  except Exception as e:
614
- console.print(f"[red]Error exporting registry: {e!s}[/]")
884
+ console.print(f"[red]Error exporting registry: {e}[/]")
885
+ logger.error(f"Failed to export registry: {e}", exc_info=True)
615
886
 
616
887
 
617
888
  if __name__ == "__main__":