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.

flock/core/flock_agent.py CHANGED
@@ -374,10 +374,12 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
374
374
  mode="json", # Use json mode for better handling of standard types by Pydantic
375
375
  exclude_none=True, # Exclude None values for cleaner output
376
376
  )
377
+ logger.debug(f"Base agent data for '{self.name}': {list(data.keys())}")
377
378
 
378
379
  # --- Serialize Components using Registry Type Names ---
379
380
  # Evaluator
380
381
  if self.evaluator:
382
+ logger.debug(f"Serializing evaluator for agent '{self.name}'")
381
383
  evaluator_type_name = FlockRegistry.get_component_type_name(
382
384
  type(self.evaluator)
383
385
  )
@@ -388,6 +390,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
388
390
  )
389
391
  evaluator_dict["type"] = evaluator_type_name # Add type marker
390
392
  data["evaluator"] = evaluator_dict
393
+ logger.debug(
394
+ f"Added evaluator of type '{evaluator_type_name}' to agent '{self.name}'"
395
+ )
391
396
  else:
392
397
  logger.warning(
393
398
  f"Could not get registered type name for evaluator {type(self.evaluator).__name__} in agent '{self.name}'. Skipping serialization."
@@ -395,6 +400,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
395
400
 
396
401
  # Router
397
402
  if self.handoff_router:
403
+ logger.debug(f"Serializing router for agent '{self.name}'")
398
404
  router_type_name = FlockRegistry.get_component_type_name(
399
405
  type(self.handoff_router)
400
406
  )
@@ -406,6 +412,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
406
412
  )
407
413
  router_dict["type"] = router_type_name
408
414
  data["handoff_router"] = router_dict
415
+ logger.debug(
416
+ f"Added router of type '{router_type_name}' to agent '{self.name}'"
417
+ )
409
418
  else:
410
419
  logger.warning(
411
420
  f"Could not get registered type name for router {type(self.handoff_router).__name__} in agent '{self.name}'. Skipping serialization."
@@ -413,6 +422,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
413
422
 
414
423
  # Modules
415
424
  if self.modules:
425
+ logger.debug(
426
+ f"Serializing {len(self.modules)} modules for agent '{self.name}'"
427
+ )
416
428
  serialized_modules = {}
417
429
  for name, module_instance in self.modules.items():
418
430
  module_type_name = FlockRegistry.get_component_type_name(
@@ -426,32 +438,55 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
426
438
  )
427
439
  module_dict["type"] = module_type_name
428
440
  serialized_modules[name] = module_dict
441
+ logger.debug(
442
+ f"Added module '{name}' of type '{module_type_name}' to agent '{self.name}'"
443
+ )
429
444
  else:
430
445
  logger.warning(
431
446
  f"Could not get registered type name for module {type(module_instance).__name__} ('{name}') in agent '{self.name}'. Skipping."
432
447
  )
433
448
  if serialized_modules:
434
449
  data["modules"] = serialized_modules
450
+ logger.debug(
451
+ f"Added {len(serialized_modules)} modules to agent '{self.name}'"
452
+ )
435
453
 
436
454
  # --- Serialize Tools (Callables) ---
437
455
  if self.tools:
456
+ logger.debug(
457
+ f"Serializing {len(self.tools)} tools for agent '{self.name}'"
458
+ )
438
459
  serialized_tools = []
439
460
  for tool in self.tools:
440
461
  if callable(tool) and not isinstance(tool, type):
441
462
  path_str = FlockRegistry.get_callable_path_string(tool)
442
463
  if path_str:
443
- serialized_tools.append({"__callable_ref__": path_str})
464
+ # Get just the function name from the path string
465
+ # If it's a namespaced path like module.submodule.function_name
466
+ # Just use the function_name part
467
+ func_name = path_str.split(".")[-1]
468
+ serialized_tools.append(func_name)
469
+ logger.debug(
470
+ f"Added tool '{func_name}' (from path '{path_str}') to agent '{self.name}'"
471
+ )
444
472
  else:
445
473
  logger.warning(
446
474
  f"Could not get path string for tool {tool} in agent '{self.name}'. Skipping."
447
475
  )
448
- # Silently skip non-callable items or log warning
449
- # else:
450
- # logger.warning(f"Non-callable item found in tools list for agent '{self.name}': {tool}. Skipping.")
476
+ else:
477
+ logger.warning(
478
+ f"Non-callable item found in tools list for agent '{self.name}': {tool}. Skipping."
479
+ )
451
480
  if serialized_tools:
452
481
  data["tools"] = serialized_tools
482
+ logger.debug(
483
+ f"Added {len(serialized_tools)} tools to agent '{self.name}'"
484
+ )
453
485
 
454
486
  # No need to call _filter_none_values here as model_dump(exclude_none=True) handles it
487
+ logger.info(
488
+ f"Serialization of agent '{self.name}' complete with {len(data)} fields"
489
+ )
455
490
  return data
456
491
 
457
492
  @classmethod
@@ -466,6 +501,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
466
501
  raise ValueError("Agent data must include a 'name' field.")
467
502
  FlockRegistry = get_registry()
468
503
  agent_name = data["name"] # For logging context
504
+ logger.info(f"Deserializing agent '{agent_name}'")
469
505
 
470
506
  # Pop complex components to handle them after basic agent instantiation
471
507
  evaluator_data = data.pop("evaluator", None)
@@ -473,6 +509,10 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
473
509
  modules_data = data.pop("modules", {})
474
510
  tools_data = data.pop("tools", [])
475
511
 
512
+ logger.debug(
513
+ f"Agent '{agent_name}' has {len(modules_data)} modules and {len(tools_data)} tools"
514
+ )
515
+
476
516
  # Deserialize remaining data recursively (handles nested basic types/callables)
477
517
  # Note: Pydantic v2 handles most basic deserialization well if types match.
478
518
  # Explicit deserialize_item might be needed if complex non-pydantic structures exist.
@@ -481,6 +521,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
481
521
 
482
522
  try:
483
523
  # Create the agent instance using Pydantic's constructor
524
+ logger.debug(
525
+ f"Creating agent instance with fields: {list(deserialized_basic_data.keys())}"
526
+ )
484
527
  agent = cls(**deserialized_basic_data)
485
528
  except Exception as e:
486
529
  logger.error(
@@ -495,13 +538,16 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
495
538
  # Evaluator
496
539
  if evaluator_data:
497
540
  try:
541
+ logger.debug(
542
+ f"Deserializing evaluator for agent '{agent_name}'"
543
+ )
498
544
  agent.evaluator = deserialize_component(
499
545
  evaluator_data, FlockEvaluator
500
546
  )
501
547
  if agent.evaluator is None:
502
548
  raise ValueError("deserialize_component returned None")
503
549
  logger.debug(
504
- f"Deserialized evaluator '{agent.evaluator.name}' for agent '{agent_name}'"
550
+ f"Deserialized evaluator '{agent.evaluator.name}' of type '{evaluator_data.get('type')}' for agent '{agent_name}'"
505
551
  )
506
552
  except Exception as e:
507
553
  logger.error(
@@ -514,13 +560,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
514
560
  # Router
515
561
  if router_data:
516
562
  try:
563
+ logger.debug(f"Deserializing router for agent '{agent_name}'")
517
564
  agent.handoff_router = deserialize_component(
518
565
  router_data, FlockRouter
519
566
  )
520
567
  if agent.handoff_router is None:
521
568
  raise ValueError("deserialize_component returned None")
522
569
  logger.debug(
523
- f"Deserialized router '{agent.handoff_router.name}' for agent '{agent_name}'"
570
+ f"Deserialized router '{agent.handoff_router.name}' of type '{router_data.get('type')}' for agent '{agent_name}'"
524
571
  )
525
572
  except Exception as e:
526
573
  logger.error(
@@ -532,8 +579,14 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
532
579
  # Modules
533
580
  if modules_data:
534
581
  agent.modules = {} # Ensure it's initialized
582
+ logger.debug(
583
+ f"Deserializing {len(modules_data)} modules for agent '{agent_name}'"
584
+ )
535
585
  for name, module_data in modules_data.items():
536
586
  try:
587
+ logger.debug(
588
+ f"Deserializing module '{name}' of type '{module_data.get('type')}' for agent '{agent_name}'"
589
+ )
537
590
  module_instance = deserialize_component(
538
591
  module_data, FlockModule
539
592
  )
@@ -543,6 +596,9 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
543
596
  agent.add_module(
544
597
  module_instance
545
598
  ) # Use add_module for consistency
599
+ logger.debug(
600
+ f"Successfully added module '{name}' to agent '{agent_name}'"
601
+ )
546
602
  else:
547
603
  raise ValueError("deserialize_component returned None")
548
604
  except Exception as e:
@@ -555,25 +611,61 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
555
611
  # --- Deserialize Tools ---
556
612
  agent.tools = [] # Initialize tools list
557
613
  if tools_data:
558
- for tool_ref in tools_data:
559
- if (
560
- isinstance(tool_ref, dict)
561
- and "__callable_ref__" in tool_ref
562
- ):
563
- path_str = tool_ref["__callable_ref__"]
564
- try:
565
- tool_func = FlockRegistry.get_callable(path_str)
566
- agent.tools.append(tool_func)
567
- except KeyError:
568
- logger.error(
569
- f"Tool callable '{path_str}' not found in registry for agent '{agent_name}'. Skipping."
614
+ # Get component registry to look up function imports
615
+ registry = get_registry()
616
+ components = getattr(registry, "_callables", {})
617
+ logger.debug(
618
+ f"Deserializing {len(tools_data)} tools for agent '{agent_name}'"
619
+ )
620
+ logger.debug(
621
+ f"Available callables in registry: {list(components.keys())}"
622
+ )
623
+
624
+ for tool_name in tools_data:
625
+ try:
626
+ logger.debug(f"Looking for tool '{tool_name}' in registry")
627
+ # First try to lookup by simple name in the registry's callables
628
+ found = False
629
+ for path_str, func in components.items():
630
+ if (
631
+ path_str.endswith("." + tool_name)
632
+ or path_str == tool_name
633
+ ):
634
+ agent.tools.append(func)
635
+ found = True
636
+ logger.info(
637
+ f"Found tool '{tool_name}' via path '{path_str}' for agent '{agent_name}'"
638
+ )
639
+ break
640
+
641
+ # If not found by simple name, try manual import
642
+ if not found:
643
+ logger.debug(
644
+ f"Attempting to import tool '{tool_name}' from modules"
570
645
  )
571
- else:
572
- logger.warning(
573
- f"Invalid tool format found during deserialization for agent '{agent_name}': {tool_ref}. Skipping."
646
+ # Check in relevant modules (could be customized based on project structure)
647
+ import __main__
648
+
649
+ if hasattr(__main__, tool_name):
650
+ agent.tools.append(getattr(__main__, tool_name))
651
+ found = True
652
+ logger.info(
653
+ f"Found tool '{tool_name}' in __main__ module for agent '{agent_name}'"
654
+ )
655
+
656
+ if not found:
657
+ logger.warning(
658
+ f"Could not find tool '{tool_name}' for agent '{agent_name}'"
659
+ )
660
+ except Exception as e:
661
+ logger.error(
662
+ f"Error adding tool '{tool_name}' to agent '{agent_name}': {e}",
663
+ exc_info=True,
574
664
  )
575
665
 
576
- logger.info(f"Successfully deserialized agent: {agent.name}")
666
+ logger.info(
667
+ f"Successfully deserialized agent '{agent_name}' with {len(agent.modules)} modules and {len(agent.tools)} tools"
668
+ )
577
669
  return agent
578
670
 
579
671
  # --- Pydantic v2 Configuration ---
@@ -162,16 +162,20 @@ class FlockRegistry:
162
162
  and self._callables[path_str] != func
163
163
  ):
164
164
  logger.warning(
165
- f"Callable '{path_str}' already registered. Overwriting."
165
+ f"Callable '{path_str}' already registered with a different function. Overwriting."
166
166
  )
167
167
  self._callables[path_str] = func
168
- logger.debug(f"Registered callable: {path_str}")
168
+ logger.debug(f"Registered callable: '{path_str}' ({func.__name__})")
169
169
  return path_str
170
+ logger.warning(
171
+ f"Could not register callable {func.__name__}: Unable to determine path string"
172
+ )
170
173
  return None
171
174
 
172
175
  def get_callable(self, path_str: str) -> Callable:
173
176
  """Retrieves a callable by its path string, attempting dynamic import if not found."""
174
177
  if path_str in self._callables:
178
+ logger.debug(f"Found callable '{path_str}' in registry")
175
179
  return self._callables[path_str]
176
180
 
177
181
  logger.debug(
@@ -179,14 +183,20 @@ class FlockRegistry:
179
183
  )
180
184
  try:
181
185
  if "." not in path_str: # Built-ins
186
+ logger.debug(f"Trying to import built-in callable '{path_str}'")
182
187
  builtins_module = importlib.import_module("builtins")
183
188
  if hasattr(builtins_module, path_str):
184
189
  func = getattr(builtins_module, path_str)
185
190
  if callable(func):
186
191
  self.register_callable(func, path_str) # Cache it
192
+ logger.info(
193
+ f"Successfully imported built-in callable '{path_str}'"
194
+ )
187
195
  return func
196
+ logger.error(f"Built-in callable '{path_str}' not found.")
188
197
  raise KeyError(f"Built-in callable '{path_str}' not found.")
189
198
 
199
+ logger.debug(f"Trying to import module callable '{path_str}'")
190
200
  module_name, func_name = path_str.rsplit(".", 1)
191
201
  module = importlib.import_module(module_name)
192
202
  func = getattr(module, func_name)
@@ -194,14 +204,21 @@ class FlockRegistry:
194
204
  self.register_callable(
195
205
  func, path_str
196
206
  ) # Cache dynamically imported
207
+ logger.info(
208
+ f"Successfully imported module callable '{path_str}'"
209
+ )
197
210
  return func
198
211
  else:
212
+ logger.error(
213
+ f"Dynamically imported object '{path_str}' is not callable."
214
+ )
199
215
  raise TypeError(
200
216
  f"Dynamically imported object '{path_str}' is not callable."
201
217
  )
202
218
  except (ImportError, AttributeError, KeyError, TypeError) as e:
203
219
  logger.error(
204
- f"Failed to dynamically load/find callable '{path_str}': {e}"
220
+ f"Failed to dynamically load/find callable '{path_str}': {e}",
221
+ exc_info=True,
205
222
  )
206
223
  raise KeyError(
207
224
  f"Callable '{path_str}' not found or failed to load: {e}"
@@ -209,11 +226,26 @@ class FlockRegistry:
209
226
 
210
227
  def get_callable_path_string(self, func: Callable) -> str | None:
211
228
  """Gets the path string for a callable, registering it if necessary."""
229
+ # First try to find by direct identity
212
230
  for path_str, registered_func in self._callables.items():
213
231
  if func == registered_func:
232
+ logger.debug(
233
+ f"Found existing path string for callable: '{path_str}'"
234
+ )
214
235
  return path_str
236
+
215
237
  # If not found by identity, generate path, register, and return
216
- return self.register_callable(func)
238
+ path_str = self.register_callable(func)
239
+ if path_str:
240
+ logger.debug(
241
+ f"Generated and registered new path string for callable: '{path_str}'"
242
+ )
243
+ else:
244
+ logger.warning(
245
+ f"Failed to generate path string for callable {func.__name__}"
246
+ )
247
+
248
+ return path_str
217
249
 
218
250
  # --- Type Registration ---
219
251
  def register_type(
@@ -2,7 +2,7 @@
2
2
  import json
3
3
  from abc import ABC, abstractmethod
4
4
  from pathlib import Path
5
- from typing import Any, TypeVar
5
+ from typing import Any, Literal, TypeVar
6
6
 
7
7
  # Use yaml if available, otherwise skip yaml methods
8
8
  try:
@@ -85,16 +85,32 @@ class Serializable(ABC):
85
85
  ) from e
86
86
 
87
87
  # --- YAML Methods ---
88
- def to_yaml(self, sort_keys=False, default_flow_style=False) -> str:
89
- """Serialize to YAML string."""
88
+ def to_yaml(
89
+ self,
90
+ path_type: Literal["absolute", "relative"],
91
+ sort_keys=False,
92
+ default_flow_style=False,
93
+ ) -> str:
94
+ """Serialize to YAML string.
95
+
96
+ Args:
97
+ path_type: How file paths should be formatted ('absolute' or 'relative')
98
+ sort_keys: Whether to sort dictionary keys
99
+ default_flow_style: YAML flow style setting
100
+ """
90
101
  if not YAML_AVAILABLE:
91
102
  raise NotImplementedError(
92
103
  "YAML support requires PyYAML: pip install pyyaml"
93
104
  )
94
105
  try:
95
- # to_dict should prepare a structure suitable for YAML dumping
106
+ # If to_dict supports path_type, pass it; otherwise use standard to_dict
107
+ if "path_type" in self.to_dict.__code__.co_varnames:
108
+ dict_data = self.to_dict(path_type=path_type)
109
+ else:
110
+ dict_data = self.to_dict()
111
+
96
112
  return yaml.dump(
97
- self.to_dict(),
113
+ dict_data,
98
114
  sort_keys=sort_keys,
99
115
  default_flow_style=default_flow_style,
100
116
  allow_unicode=True,
@@ -125,8 +141,19 @@ class Serializable(ABC):
125
141
  f"Failed to deserialize {cls.__name__} from YAML: {e}"
126
142
  ) from e
127
143
 
128
- def to_yaml_file(self, path: Path | str, **yaml_dump_kwargs) -> None:
129
- """Serialize to YAML file."""
144
+ def to_yaml_file(
145
+ self,
146
+ path: Path | str,
147
+ path_type: Literal["absolute", "relative"] = "absolute",
148
+ **yaml_dump_kwargs,
149
+ ) -> None:
150
+ """Serialize to YAML file.
151
+
152
+ Args:
153
+ path: File path to write to
154
+ path_type: How file paths should be formatted ('absolute' or 'relative')
155
+ **yaml_dump_kwargs: Additional arguments to pass to yaml.dump
156
+ """
130
157
  if not YAML_AVAILABLE:
131
158
  raise NotImplementedError(
132
159
  "YAML support requires PyYAML: pip install pyyaml"
@@ -134,7 +161,7 @@ class Serializable(ABC):
134
161
  path = Path(path)
135
162
  try:
136
163
  path.parent.mkdir(parents=True, exist_ok=True)
137
- yaml_str = self.to_yaml(**yaml_dump_kwargs)
164
+ yaml_str = self.to_yaml(path_type=path_type, **yaml_dump_kwargs)
138
165
  path.write_text(yaml_str, encoding="utf-8")
139
166
  except Exception as e:
140
167
  raise RuntimeError(
@@ -52,7 +52,7 @@ def init_console(clear_screen: bool = True):
52
52
  │ ▒█▀▀▀ █░░ █▀▀█ █▀▀ █░█ │
53
53
  │ ▒█▀▀▀ █░░ █░░█ █░░ █▀▄ │
54
54
  │ ▒█░░░ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀░▀ │
55
- ╰━━━━━━━━━v{__version__}━━━━━━━━╯
55
+ ╰━━━━━━━━v{__version__}━━━━━━━━╯
56
56
  🦆 🐤 🐧 🐓
57
57
  """,
58
58
  justify="center",
@@ -62,7 +62,7 @@ def init_console(clear_screen: bool = True):
62
62
  console.clear()
63
63
  console.print(banner_text)
64
64
  console.print(
65
- f"[italic]'Hummingbird'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
65
+ f"[italic]'Magpie'[/] milestone - [bold]white duck GmbH[/] - [cyan]https://whiteduck.de[/]\n"
66
66
  )
67
67
 
68
68
 
@@ -0,0 +1,223 @@
1
+ """Utility functions for handling file paths in Flock.
2
+
3
+ This module provides utilities for working with file paths,
4
+ especially for components that may be loaded from file system paths
5
+ rather than module imports.
6
+ """
7
+
8
+ import importlib.util
9
+ import inspect
10
+ import os
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+
16
+ def get_file_path(obj: Any) -> str | None:
17
+ """Get the file path for a Python object.
18
+
19
+ Args:
20
+ obj: The object to get the file path for
21
+
22
+ Returns:
23
+ The file path if it can be determined, None otherwise
24
+ """
25
+ try:
26
+ if inspect.ismodule(obj):
27
+ return obj.__file__
28
+ elif inspect.isclass(obj) or inspect.isfunction(obj):
29
+ return inspect.getfile(obj)
30
+ return None
31
+ except (TypeError, ValueError):
32
+ return None
33
+
34
+
35
+ def normalize_path(path: str) -> str:
36
+ """Normalize a path for consistent representation.
37
+
38
+ Args:
39
+ path: The path to normalize
40
+
41
+ Returns:
42
+ The normalized path
43
+ """
44
+ return os.path.normpath(path)
45
+
46
+
47
+ def is_same_path(path1: str, path2: str) -> bool:
48
+ """Check if two paths point to the same file.
49
+
50
+ Args:
51
+ path1: The first path
52
+ path2: The second path
53
+
54
+ Returns:
55
+ True if the paths point to the same file, False otherwise
56
+ """
57
+ return os.path.normpath(os.path.abspath(path1)) == os.path.normpath(
58
+ os.path.abspath(path2)
59
+ )
60
+
61
+
62
+ def get_relative_path(path: str, base_path: str | None = None) -> str:
63
+ """Get a path relative to a base path.
64
+
65
+ Args:
66
+ path: The path to make relative
67
+ base_path: The base path (defaults to current working directory)
68
+
69
+ Returns:
70
+ The relative path
71
+ """
72
+ if base_path is None:
73
+ base_path = os.getcwd()
74
+
75
+ return os.path.relpath(path, base_path)
76
+
77
+
78
+ def load_class_from_file(file_path: str, class_name: str) -> type | None:
79
+ """Load a class from a file.
80
+
81
+ Args:
82
+ file_path: The path to the file
83
+ class_name: The name of the class to load
84
+
85
+ Returns:
86
+ The loaded class, or None if it could not be loaded
87
+ """
88
+ try:
89
+ # Generate a unique module name to avoid conflicts
90
+ module_name = f"flock_dynamic_import_{hash(file_path)}"
91
+
92
+ # Create a spec for the module
93
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
94
+ if not spec or not spec.loader:
95
+ return None
96
+
97
+ # Create and load the module
98
+ module = importlib.util.module_from_spec(spec)
99
+ sys.modules[module_name] = module
100
+ spec.loader.exec_module(module)
101
+
102
+ # Get the class from the module
103
+ if not hasattr(module, class_name):
104
+ return None
105
+
106
+ return getattr(module, class_name)
107
+ except Exception:
108
+ return None
109
+
110
+
111
+ def get_project_root() -> Path:
112
+ """Get the project root directory.
113
+
114
+ Returns:
115
+ The project root path
116
+ """
117
+ # Try to find the directory containing pyproject.toml or setup.py
118
+ current_dir = Path(os.getcwd())
119
+
120
+ # Walk up the directory tree looking for project markers
121
+ for path in [current_dir, *current_dir.parents]:
122
+ if (path / "pyproject.toml").exists() or (path / "setup.py").exists():
123
+ return path
124
+
125
+ # Default to current directory if no project markers found
126
+ return current_dir
127
+
128
+
129
+ def component_path_to_file_path(component_path: str) -> str | None:
130
+ """Convert a component path (module.ClassName) to a file path.
131
+
132
+ Args:
133
+ component_path: The component path in the form module.ClassName
134
+
135
+ Returns:
136
+ The file path if it can be determined, None otherwise
137
+ """
138
+ try:
139
+ # Split into module path and class name
140
+ if "." not in component_path:
141
+ return None
142
+
143
+ module_path, class_name = component_path.rsplit(".", 1)
144
+
145
+ # Import the module
146
+ module = importlib.import_module(module_path)
147
+
148
+ # Get the file path
149
+ if hasattr(module, "__file__"):
150
+ return module.__file__
151
+
152
+ return None
153
+ except (ImportError, AttributeError):
154
+ return None
155
+
156
+
157
+ def file_path_to_component_path(file_path: str, class_name: str) -> str | None:
158
+ """Convert a file path and class name to a component path (module.ClassName).
159
+
160
+ This is approximate and may not work in all cases, especially for non-standard
161
+ module structures.
162
+
163
+ Args:
164
+ file_path: The file path to the module
165
+ class_name: The name of the class
166
+
167
+ Returns:
168
+ The component path if it can be determined, None otherwise
169
+ """
170
+ try:
171
+ # Convert the file path to an absolute path
172
+ abs_path = os.path.abspath(file_path)
173
+
174
+ # Get the project root
175
+ root = get_project_root()
176
+
177
+ # Get the relative path from the project root
178
+ rel_path = os.path.relpath(abs_path, root)
179
+
180
+ # Convert to a module path
181
+ module_path = os.path.splitext(rel_path)[0].replace(os.sep, ".")
182
+
183
+ # Remove 'src.' prefix if present (common in Python projects)
184
+ if module_path.startswith("src."):
185
+ module_path = module_path[4:]
186
+
187
+ # Combine with the class name
188
+ return f"{module_path}.{class_name}"
189
+ except Exception:
190
+ return None
191
+
192
+
193
+ def register_file_paths_in_registry(
194
+ component_paths: dict[str, str], registry: Any | None = None
195
+ ) -> bool:
196
+ """Register file paths in the registry.
197
+
198
+ Args:
199
+ component_paths: Dictionary mapping component paths to file paths
200
+ registry: The registry to register in (defaults to the global registry)
201
+
202
+ Returns:
203
+ True if all paths were registered, False otherwise
204
+ """
205
+ try:
206
+ # Get the global registry if none provided
207
+ if registry is None:
208
+ from flock.core.flock_registry import get_registry
209
+
210
+ registry = get_registry()
211
+
212
+ # Initialize component_file_paths if needed
213
+ if not hasattr(registry, "_component_file_paths"):
214
+ registry._component_file_paths = {}
215
+
216
+ # Register each path
217
+ for component_name, file_path in component_paths.items():
218
+ if component_name in registry._components:
219
+ registry._component_file_paths[component_name] = file_path
220
+
221
+ return True
222
+ except Exception:
223
+ return False