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/__init__.py +31 -0
- flock/cli/create_flock.py +58 -3
- flock/cli/load_flock.py +135 -1
- flock/cli/registry_management.py +367 -96
- flock/cli/yaml_editor.py +119 -6
- flock/core/__init__.py +13 -1
- flock/core/flock.py +865 -26
- flock/core/flock_agent.py +114 -22
- flock/core/flock_registry.py +36 -4
- flock/core/serialization/serializable.py +35 -8
- flock/core/util/cli_helper.py +2 -2
- flock/core/util/file_path_utils.py +223 -0
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b1.dist-info}/METADATA +1 -1
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b1.dist-info}/RECORD +17 -16
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b1.dist-info}/WHEEL +0 -0
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b1.dist-info}/entry_points.txt +0 -0
- {flock_core-0.3.41.dist-info → flock_core-0.4.0b1.dist-info}/licenses/LICENSE +0 -0
flock/core/flock.py
CHANGED
|
@@ -7,7 +7,7 @@ import asyncio
|
|
|
7
7
|
import os
|
|
8
8
|
import uuid
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
11
11
|
|
|
12
12
|
from box import Box
|
|
13
13
|
from opentelemetry import trace
|
|
@@ -446,33 +446,421 @@ class Flock(BaseModel, Serializable):
|
|
|
446
446
|
|
|
447
447
|
# --- ADDED Serialization Methods ---
|
|
448
448
|
|
|
449
|
-
def to_dict(
|
|
450
|
-
"""
|
|
449
|
+
def to_dict(
|
|
450
|
+
self, path_type: Literal["absolute", "relative"] = "absolute"
|
|
451
|
+
) -> dict[str, Any]:
|
|
452
|
+
"""Convert Flock instance to dictionary representation.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
456
|
+
"""
|
|
451
457
|
logger.debug("Serializing Flock instance to dict.")
|
|
452
458
|
# Use Pydantic's dump for base fields
|
|
453
459
|
data = self.model_dump(mode="json", exclude_none=True)
|
|
460
|
+
logger.info(
|
|
461
|
+
f"Serializing Flock '{self.name}' with {len(self._agents)} agents"
|
|
462
|
+
)
|
|
454
463
|
|
|
455
464
|
# Manually add serialized agents
|
|
456
465
|
data["agents"] = {}
|
|
466
|
+
|
|
467
|
+
# Track custom types used across all agents
|
|
468
|
+
custom_types = {}
|
|
469
|
+
# Track components used across all agents
|
|
470
|
+
components = {}
|
|
471
|
+
|
|
457
472
|
for name, agent_instance in self._agents.items():
|
|
458
473
|
try:
|
|
474
|
+
logger.debug(f"Serializing agent '{name}'")
|
|
459
475
|
# Agents handle their own serialization via their to_dict
|
|
460
|
-
|
|
476
|
+
agent_data = agent_instance.to_dict()
|
|
477
|
+
data["agents"][name] = agent_data
|
|
478
|
+
|
|
479
|
+
# Extract type information from agent outputs
|
|
480
|
+
if agent_instance.output:
|
|
481
|
+
logger.debug(
|
|
482
|
+
f"Extracting type information from agent '{name}' output: {agent_instance.output}"
|
|
483
|
+
)
|
|
484
|
+
output_types = self._extract_types_from_signature(
|
|
485
|
+
agent_instance.output
|
|
486
|
+
)
|
|
487
|
+
if output_types:
|
|
488
|
+
logger.debug(
|
|
489
|
+
f"Found output types in agent '{name}': {output_types}"
|
|
490
|
+
)
|
|
491
|
+
custom_types.update(
|
|
492
|
+
self._get_type_definitions(output_types)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# Extract component information
|
|
496
|
+
if (
|
|
497
|
+
"evaluator" in agent_data
|
|
498
|
+
and "type" in agent_data["evaluator"]
|
|
499
|
+
):
|
|
500
|
+
component_type = agent_data["evaluator"]["type"]
|
|
501
|
+
logger.debug(
|
|
502
|
+
f"Adding evaluator component '{component_type}' from agent '{name}'"
|
|
503
|
+
)
|
|
504
|
+
components[component_type] = self._get_component_definition(
|
|
505
|
+
component_type, path_type
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# Extract module component information
|
|
509
|
+
if "modules" in agent_data:
|
|
510
|
+
for module_name, module_data in agent_data[
|
|
511
|
+
"modules"
|
|
512
|
+
].items():
|
|
513
|
+
if "type" in module_data:
|
|
514
|
+
component_type = module_data["type"]
|
|
515
|
+
logger.debug(
|
|
516
|
+
f"Adding module component '{component_type}' from module '{module_name}' in agent '{name}'"
|
|
517
|
+
)
|
|
518
|
+
components[component_type] = (
|
|
519
|
+
self._get_component_definition(
|
|
520
|
+
component_type, path_type
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Extract tool (callable) information
|
|
525
|
+
if agent_data.get("tools"):
|
|
526
|
+
logger.debug(
|
|
527
|
+
f"Extracting tool information from agent '{name}': {agent_data['tools']}"
|
|
528
|
+
)
|
|
529
|
+
# Get references to the actual tool objects
|
|
530
|
+
tool_objs = (
|
|
531
|
+
agent_instance.tools if agent_instance.tools else []
|
|
532
|
+
)
|
|
533
|
+
for i, tool_name in enumerate(agent_data["tools"]):
|
|
534
|
+
if i < len(tool_objs):
|
|
535
|
+
tool = tool_objs[i]
|
|
536
|
+
if callable(tool) and not isinstance(tool, type):
|
|
537
|
+
# Get the fully qualified name for registry lookup
|
|
538
|
+
path_str = (
|
|
539
|
+
get_registry().get_callable_path_string(
|
|
540
|
+
tool
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
if path_str:
|
|
544
|
+
logger.debug(
|
|
545
|
+
f"Adding tool '{tool_name}' (from path '{path_str}') to components"
|
|
546
|
+
)
|
|
547
|
+
# Add definition using just the function name as the key
|
|
548
|
+
components[tool_name] = (
|
|
549
|
+
self._get_callable_definition(
|
|
550
|
+
path_str, tool_name, path_type
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
461
554
|
except Exception as e:
|
|
462
555
|
logger.error(
|
|
463
|
-
f"Failed to serialize agent '{name}' within Flock: {e}"
|
|
556
|
+
f"Failed to serialize agent '{name}' within Flock: {e}",
|
|
557
|
+
exc_info=True,
|
|
464
558
|
)
|
|
465
559
|
# Optionally skip problematic agents or raise error
|
|
466
560
|
# data["agents"][name] = {"error": f"Serialization failed: {e}"}
|
|
467
561
|
|
|
468
|
-
#
|
|
469
|
-
|
|
470
|
-
|
|
562
|
+
# Add type definitions to the serialized output if any were found
|
|
563
|
+
if custom_types:
|
|
564
|
+
logger.info(
|
|
565
|
+
f"Adding {len(custom_types)} custom type definitions to serialized output"
|
|
566
|
+
)
|
|
567
|
+
data["types"] = custom_types
|
|
568
|
+
|
|
569
|
+
# Add component definitions to the serialized output if any were found
|
|
570
|
+
if components:
|
|
571
|
+
logger.info(
|
|
572
|
+
f"Adding {len(components)} component definitions to serialized output"
|
|
573
|
+
)
|
|
574
|
+
data["components"] = components
|
|
575
|
+
|
|
576
|
+
# Add dependencies section
|
|
577
|
+
data["dependencies"] = self._get_dependencies()
|
|
578
|
+
|
|
579
|
+
# Add serialization settings
|
|
580
|
+
data["metadata"] = {"path_type": path_type}
|
|
581
|
+
|
|
582
|
+
logger.debug(
|
|
583
|
+
f"Flock serialization complete with {len(data['agents'])} agents, {len(custom_types)} types, {len(components)} components"
|
|
584
|
+
)
|
|
471
585
|
|
|
472
|
-
# Filter final dict (optional, Pydantic's exclude_none helps)
|
|
473
|
-
# return self._filter_none_values(data)
|
|
474
586
|
return data
|
|
475
587
|
|
|
588
|
+
def _extract_types_from_signature(self, signature: str) -> list[str]:
|
|
589
|
+
"""Extract type names from an input/output signature string."""
|
|
590
|
+
if not signature:
|
|
591
|
+
return []
|
|
592
|
+
|
|
593
|
+
# Basic type extraction - handles simple cases like "result: TypeName" or "list[TypeName]"
|
|
594
|
+
custom_types = []
|
|
595
|
+
|
|
596
|
+
# Look for type annotations (everything after ":")
|
|
597
|
+
parts = signature.split(":")
|
|
598
|
+
if len(parts) > 1:
|
|
599
|
+
type_part = parts[1].strip()
|
|
600
|
+
|
|
601
|
+
# Extract from list[Type]
|
|
602
|
+
if "list[" in type_part:
|
|
603
|
+
inner_type = type_part.split("list[")[1].split("]")[0].strip()
|
|
604
|
+
if inner_type and inner_type.lower() not in [
|
|
605
|
+
"str",
|
|
606
|
+
"int",
|
|
607
|
+
"float",
|
|
608
|
+
"bool",
|
|
609
|
+
"dict",
|
|
610
|
+
"list",
|
|
611
|
+
]:
|
|
612
|
+
custom_types.append(inner_type)
|
|
613
|
+
|
|
614
|
+
# Extract direct type references
|
|
615
|
+
elif type_part and type_part.lower() not in [
|
|
616
|
+
"str",
|
|
617
|
+
"int",
|
|
618
|
+
"float",
|
|
619
|
+
"bool",
|
|
620
|
+
"dict",
|
|
621
|
+
"list",
|
|
622
|
+
]:
|
|
623
|
+
custom_types.append(
|
|
624
|
+
type_part.split()[0]
|
|
625
|
+
) # Take the first word in case there's a description
|
|
626
|
+
|
|
627
|
+
return custom_types
|
|
628
|
+
|
|
629
|
+
def _get_type_definitions(self, type_names: list[str]) -> dict[str, Any]:
|
|
630
|
+
"""Get definitions for the specified custom types."""
|
|
631
|
+
from flock.core.flock_registry import get_registry
|
|
632
|
+
|
|
633
|
+
type_definitions = {}
|
|
634
|
+
registry = get_registry()
|
|
635
|
+
|
|
636
|
+
for type_name in type_names:
|
|
637
|
+
try:
|
|
638
|
+
# Try to get the type from registry
|
|
639
|
+
type_obj = registry._types.get(type_name)
|
|
640
|
+
if type_obj:
|
|
641
|
+
type_def = self._extract_type_definition(
|
|
642
|
+
type_name, type_obj
|
|
643
|
+
)
|
|
644
|
+
if type_def:
|
|
645
|
+
type_definitions[type_name] = type_def
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.warning(
|
|
648
|
+
f"Could not extract definition for type {type_name}: {e}"
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
return type_definitions
|
|
652
|
+
|
|
653
|
+
def _extract_type_definition(
|
|
654
|
+
self, type_name: str, type_obj: type
|
|
655
|
+
) -> dict[str, Any]:
|
|
656
|
+
"""Extract a definition for a custom type."""
|
|
657
|
+
import inspect
|
|
658
|
+
from dataclasses import is_dataclass
|
|
659
|
+
|
|
660
|
+
type_def = {
|
|
661
|
+
"module_path": type_obj.__module__,
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Handle Pydantic models
|
|
665
|
+
if hasattr(type_obj, "model_json_schema") and callable(
|
|
666
|
+
getattr(type_obj, "model_json_schema")
|
|
667
|
+
):
|
|
668
|
+
type_def["type"] = "pydantic.BaseModel"
|
|
669
|
+
try:
|
|
670
|
+
schema = type_obj.model_json_schema()
|
|
671
|
+
# Clean up schema to remove unnecessary fields
|
|
672
|
+
if "title" in schema and schema["title"] == type_name:
|
|
673
|
+
del schema["title"]
|
|
674
|
+
type_def["schema"] = schema
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.warning(
|
|
677
|
+
f"Could not extract schema for Pydantic model {type_name}: {e}"
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Handle dataclasses
|
|
681
|
+
elif is_dataclass(type_obj):
|
|
682
|
+
type_def["type"] = "dataclass"
|
|
683
|
+
fields = {}
|
|
684
|
+
for field_name, field in type_obj.__dataclass_fields__.items():
|
|
685
|
+
fields[field_name] = {
|
|
686
|
+
"type": str(field.type),
|
|
687
|
+
"default": str(field.default)
|
|
688
|
+
if field.default is not inspect.Parameter.empty
|
|
689
|
+
else None,
|
|
690
|
+
}
|
|
691
|
+
type_def["fields"] = fields
|
|
692
|
+
|
|
693
|
+
# Handle other types - just store basic information
|
|
694
|
+
else:
|
|
695
|
+
type_def["type"] = "custom"
|
|
696
|
+
|
|
697
|
+
# Extract import statement (simplified version)
|
|
698
|
+
type_def["imports"] = [f"from {type_obj.__module__} import {type_name}"]
|
|
699
|
+
|
|
700
|
+
return type_def
|
|
701
|
+
|
|
702
|
+
def _get_component_definition(
|
|
703
|
+
self, component_type: str, path_type: Literal["absolute", "relative"]
|
|
704
|
+
) -> dict[str, Any]:
|
|
705
|
+
"""Get definition for a component type."""
|
|
706
|
+
import os
|
|
707
|
+
import sys
|
|
708
|
+
|
|
709
|
+
from flock.core.flock_registry import get_registry
|
|
710
|
+
|
|
711
|
+
registry = get_registry()
|
|
712
|
+
component_def = {}
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
# Try to get the component class from registry
|
|
716
|
+
component_class = registry._components.get(component_type)
|
|
717
|
+
if component_class:
|
|
718
|
+
# Get the standard module path
|
|
719
|
+
module_path = component_class.__module__
|
|
720
|
+
|
|
721
|
+
# Get the actual file system path if possible
|
|
722
|
+
file_path = None
|
|
723
|
+
try:
|
|
724
|
+
if (
|
|
725
|
+
hasattr(component_class, "__module__")
|
|
726
|
+
and component_class.__module__
|
|
727
|
+
):
|
|
728
|
+
module = sys.modules.get(component_class.__module__)
|
|
729
|
+
if module and hasattr(module, "__file__"):
|
|
730
|
+
file_path = os.path.abspath(module.__file__)
|
|
731
|
+
# Convert to relative path if needed
|
|
732
|
+
if path_type == "relative" and file_path:
|
|
733
|
+
try:
|
|
734
|
+
file_path = os.path.relpath(file_path)
|
|
735
|
+
except ValueError:
|
|
736
|
+
# Keep as absolute if can't make relative
|
|
737
|
+
logger.warning(
|
|
738
|
+
f"Could not convert path to relative: {file_path}"
|
|
739
|
+
)
|
|
740
|
+
except Exception as e:
|
|
741
|
+
# If we can't get the file path, we'll just use the module path
|
|
742
|
+
logger.warning(
|
|
743
|
+
f"Error getting file path for component {component_type}: {e}"
|
|
744
|
+
)
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
component_def = {
|
|
748
|
+
"type": "flock_component",
|
|
749
|
+
"module_path": module_path,
|
|
750
|
+
"file_path": file_path, # Include actual file system path
|
|
751
|
+
"description": getattr(
|
|
752
|
+
component_class, "__doc__", ""
|
|
753
|
+
).strip()
|
|
754
|
+
or f"{component_type} component",
|
|
755
|
+
}
|
|
756
|
+
except Exception as e:
|
|
757
|
+
logger.warning(
|
|
758
|
+
f"Could not extract definition for component {component_type}: {e}"
|
|
759
|
+
)
|
|
760
|
+
# Provide minimal information if we can't extract details
|
|
761
|
+
component_def = {
|
|
762
|
+
"type": "flock_component",
|
|
763
|
+
"module_path": "unknown",
|
|
764
|
+
"file_path": None,
|
|
765
|
+
"description": f"{component_type} component (definition incomplete)",
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return component_def
|
|
769
|
+
|
|
770
|
+
def _get_callable_definition(
|
|
771
|
+
self,
|
|
772
|
+
callable_ref: str,
|
|
773
|
+
func_name: str,
|
|
774
|
+
path_type: Literal["absolute", "relative"],
|
|
775
|
+
) -> dict[str, Any]:
|
|
776
|
+
"""Get definition for a callable reference.
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
callable_ref: The fully qualified path to the callable
|
|
780
|
+
func_name: The simple function name (for display purposes)
|
|
781
|
+
path_type: How file paths should be formatted ('absolute' or 'relative')
|
|
782
|
+
"""
|
|
783
|
+
import inspect
|
|
784
|
+
import os
|
|
785
|
+
import sys
|
|
786
|
+
|
|
787
|
+
from flock.core.flock_registry import get_registry
|
|
788
|
+
|
|
789
|
+
registry = get_registry()
|
|
790
|
+
callable_def = {}
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
# Try to get the callable from registry
|
|
794
|
+
logger.debug(
|
|
795
|
+
f"Getting callable definition for '{callable_ref}' (display name: '{func_name}')"
|
|
796
|
+
)
|
|
797
|
+
func = registry.get_callable(callable_ref)
|
|
798
|
+
if func:
|
|
799
|
+
# Get the standard module path
|
|
800
|
+
module_path = func.__module__
|
|
801
|
+
|
|
802
|
+
# Get the actual file system path if possible
|
|
803
|
+
file_path = None
|
|
804
|
+
try:
|
|
805
|
+
if func.__module__ and func.__module__ != "builtins":
|
|
806
|
+
module = sys.modules.get(func.__module__)
|
|
807
|
+
if module and hasattr(module, "__file__"):
|
|
808
|
+
file_path = os.path.abspath(module.__file__)
|
|
809
|
+
# Convert to relative path if needed
|
|
810
|
+
if path_type == "relative" and file_path:
|
|
811
|
+
try:
|
|
812
|
+
file_path = os.path.relpath(file_path)
|
|
813
|
+
except ValueError:
|
|
814
|
+
# Keep as absolute if can't make relative
|
|
815
|
+
logger.warning(
|
|
816
|
+
f"Could not convert path to relative: {file_path}"
|
|
817
|
+
)
|
|
818
|
+
except Exception as e:
|
|
819
|
+
# If we can't get the file path, just use the module path
|
|
820
|
+
logger.warning(
|
|
821
|
+
f"Error getting file path for callable {callable_ref}: {e}"
|
|
822
|
+
)
|
|
823
|
+
pass
|
|
824
|
+
|
|
825
|
+
# Get the docstring for description
|
|
826
|
+
docstring = (
|
|
827
|
+
inspect.getdoc(func) or f"Callable function {func_name}"
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
callable_def = {
|
|
831
|
+
"type": "flock_callable",
|
|
832
|
+
"module_path": module_path,
|
|
833
|
+
"file_path": file_path,
|
|
834
|
+
"description": docstring.strip(),
|
|
835
|
+
}
|
|
836
|
+
logger.debug(
|
|
837
|
+
f"Created callable definition for '{func_name}': module={module_path}, file={file_path}"
|
|
838
|
+
)
|
|
839
|
+
except Exception as e:
|
|
840
|
+
logger.warning(
|
|
841
|
+
f"Could not extract definition for callable {callable_ref}: {e}"
|
|
842
|
+
)
|
|
843
|
+
# Provide minimal information
|
|
844
|
+
callable_def = {
|
|
845
|
+
"type": "flock_callable",
|
|
846
|
+
"module_path": callable_ref.split(".")[0]
|
|
847
|
+
if "." in callable_ref
|
|
848
|
+
else "unknown",
|
|
849
|
+
"file_path": None,
|
|
850
|
+
"description": f"Callable {func_name} (definition incomplete)",
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return callable_def
|
|
854
|
+
|
|
855
|
+
def _get_dependencies(self) -> list[str]:
|
|
856
|
+
"""Get list of dependencies required by this Flock."""
|
|
857
|
+
# This is a simplified version - in production, you might want to detect
|
|
858
|
+
# actual versions of installed packages
|
|
859
|
+
return [
|
|
860
|
+
"pydantic>=2.0.0",
|
|
861
|
+
"flock>=0.3.41", # Assuming this is the package name
|
|
862
|
+
]
|
|
863
|
+
|
|
476
864
|
@classmethod
|
|
477
865
|
def from_dict(cls: type[T], data: dict[str, Any]) -> T:
|
|
478
866
|
"""Create Flock instance from dictionary representation."""
|
|
@@ -480,6 +868,30 @@ class Flock(BaseModel, Serializable):
|
|
|
480
868
|
f"Deserializing Flock from dict. Provided keys: {list(data.keys())}"
|
|
481
869
|
)
|
|
482
870
|
|
|
871
|
+
# Check for serialization settings
|
|
872
|
+
serialization_settings = data.pop("serialization_settings", {})
|
|
873
|
+
path_type = serialization_settings.get("path_type", "absolute")
|
|
874
|
+
logger.debug(
|
|
875
|
+
f"Using path_type '{path_type}' from serialization settings"
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
# First, handle type definitions if present
|
|
879
|
+
if "types" in data:
|
|
880
|
+
logger.info(f"Processing {len(data['types'])} type definitions")
|
|
881
|
+
cls._register_type_definitions(data["types"])
|
|
882
|
+
|
|
883
|
+
# Then, handle component definitions if present
|
|
884
|
+
if "components" in data:
|
|
885
|
+
logger.info(
|
|
886
|
+
f"Processing {len(data['components'])} component definitions"
|
|
887
|
+
)
|
|
888
|
+
cls._register_component_definitions(data["components"], path_type)
|
|
889
|
+
|
|
890
|
+
# Check dependencies if present
|
|
891
|
+
if "dependencies" in data:
|
|
892
|
+
logger.debug(f"Checking {len(data['dependencies'])} dependencies")
|
|
893
|
+
cls._check_dependencies(data["dependencies"])
|
|
894
|
+
|
|
483
895
|
# Ensure FlockAgent is importable for type checking later
|
|
484
896
|
try:
|
|
485
897
|
from flock.core.flock_agent import FlockAgent as ConcreteFlockAgent
|
|
@@ -491,11 +903,22 @@ class Flock(BaseModel, Serializable):
|
|
|
491
903
|
|
|
492
904
|
# Extract agent data before initializing Flock base model
|
|
493
905
|
agents_data = data.pop("agents", {})
|
|
906
|
+
logger.info(f"Found {len(agents_data)} agents to deserialize")
|
|
907
|
+
|
|
908
|
+
# Remove types, components, and dependencies sections as they're not part of Flock fields
|
|
909
|
+
data.pop("types", None)
|
|
910
|
+
data.pop("components", None)
|
|
911
|
+
data.pop("dependencies", None)
|
|
912
|
+
# Remove metadata if present
|
|
913
|
+
data.pop("metadata", None)
|
|
494
914
|
|
|
495
915
|
# Create Flock instance using Pydantic constructor for basic fields
|
|
496
916
|
try:
|
|
497
917
|
# Pass only fields defined in Flock's Pydantic model
|
|
498
918
|
init_data = {k: v for k, v in data.items() if k in cls.model_fields}
|
|
919
|
+
logger.debug(
|
|
920
|
+
f"Creating Flock instance with fields: {list(init_data.keys())}"
|
|
921
|
+
)
|
|
499
922
|
flock_instance = cls(**init_data)
|
|
500
923
|
except Exception as e:
|
|
501
924
|
logger.error(
|
|
@@ -508,6 +931,7 @@ class Flock(BaseModel, Serializable):
|
|
|
508
931
|
# Deserialize and add agents AFTER Flock instance exists
|
|
509
932
|
for name, agent_data in agents_data.items():
|
|
510
933
|
try:
|
|
934
|
+
logger.debug(f"Deserializing agent '{name}'")
|
|
511
935
|
# Ensure agent_data has the name, or add it from the key
|
|
512
936
|
agent_data.setdefault("name", name)
|
|
513
937
|
# Use FlockAgent's from_dict method
|
|
@@ -515,6 +939,7 @@ class Flock(BaseModel, Serializable):
|
|
|
515
939
|
flock_instance.add_agent(
|
|
516
940
|
agent_instance
|
|
517
941
|
) # Adds to _agents and registers
|
|
942
|
+
logger.debug(f"Successfully added agent '{name}' to Flock")
|
|
518
943
|
except Exception as e:
|
|
519
944
|
logger.error(
|
|
520
945
|
f"Failed to deserialize or add agent '{name}' during Flock deserialization: {e}",
|
|
@@ -522,9 +947,412 @@ class Flock(BaseModel, Serializable):
|
|
|
522
947
|
)
|
|
523
948
|
# Decide: skip agent or raise error?
|
|
524
949
|
|
|
525
|
-
logger.info(
|
|
950
|
+
logger.info(
|
|
951
|
+
f"Successfully deserialized Flock instance '{flock_instance.name}' with {len(flock_instance._agents)} agents"
|
|
952
|
+
)
|
|
526
953
|
return flock_instance
|
|
527
954
|
|
|
955
|
+
@classmethod
|
|
956
|
+
def _register_type_definitions(cls, type_defs: dict[str, Any]) -> None:
|
|
957
|
+
"""Register type definitions from serialized data."""
|
|
958
|
+
import importlib
|
|
959
|
+
|
|
960
|
+
from flock.core.flock_registry import get_registry
|
|
961
|
+
|
|
962
|
+
registry = get_registry()
|
|
963
|
+
|
|
964
|
+
for type_name, type_def in type_defs.items():
|
|
965
|
+
logger.debug(f"Registering type: {type_name}")
|
|
966
|
+
|
|
967
|
+
try:
|
|
968
|
+
# First try to import the type directly
|
|
969
|
+
module_path = type_def.get("module_path")
|
|
970
|
+
if module_path:
|
|
971
|
+
try:
|
|
972
|
+
module = importlib.import_module(module_path)
|
|
973
|
+
if hasattr(module, type_name):
|
|
974
|
+
type_obj = getattr(module, type_name)
|
|
975
|
+
registry.register_type(type_obj, type_name)
|
|
976
|
+
logger.info(
|
|
977
|
+
f"Registered type {type_name} from module {module_path}"
|
|
978
|
+
)
|
|
979
|
+
continue
|
|
980
|
+
except ImportError:
|
|
981
|
+
logger.debug(
|
|
982
|
+
f"Could not import {module_path}, trying dynamic type creation"
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
# If direct import fails, try to create the type dynamically
|
|
986
|
+
if (
|
|
987
|
+
type_def.get("type") == "pydantic.BaseModel"
|
|
988
|
+
and "schema" in type_def
|
|
989
|
+
):
|
|
990
|
+
cls._create_pydantic_model(type_name, type_def)
|
|
991
|
+
elif (
|
|
992
|
+
type_def.get("type") == "dataclass" and "fields" in type_def
|
|
993
|
+
):
|
|
994
|
+
cls._create_dataclass(type_name, type_def)
|
|
995
|
+
else:
|
|
996
|
+
logger.warning(
|
|
997
|
+
f"Unsupported type definition for {type_name}, type: {type_def.get('type')}"
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
except Exception as e:
|
|
1001
|
+
logger.error(f"Failed to register type {type_name}: {e}")
|
|
1002
|
+
|
|
1003
|
+
@classmethod
|
|
1004
|
+
def _create_pydantic_model(
|
|
1005
|
+
cls, type_name: str, type_def: dict[str, Any]
|
|
1006
|
+
) -> None:
|
|
1007
|
+
"""Dynamically create a Pydantic model from a schema definition."""
|
|
1008
|
+
from pydantic import create_model
|
|
1009
|
+
|
|
1010
|
+
from flock.core.flock_registry import get_registry
|
|
1011
|
+
|
|
1012
|
+
registry = get_registry()
|
|
1013
|
+
schema = type_def.get("schema", {})
|
|
1014
|
+
|
|
1015
|
+
try:
|
|
1016
|
+
# Extract field definitions from schema
|
|
1017
|
+
fields = {}
|
|
1018
|
+
properties = schema.get("properties", {})
|
|
1019
|
+
required = schema.get("required", [])
|
|
1020
|
+
|
|
1021
|
+
for field_name, field_schema in properties.items():
|
|
1022
|
+
# Determine the field type based on schema
|
|
1023
|
+
field_type = cls._get_type_from_schema(field_schema)
|
|
1024
|
+
|
|
1025
|
+
# Determine if field is required
|
|
1026
|
+
default = ... if field_name in required else None
|
|
1027
|
+
|
|
1028
|
+
# Add to fields dict
|
|
1029
|
+
fields[field_name] = (field_type, default)
|
|
1030
|
+
|
|
1031
|
+
# Create the model
|
|
1032
|
+
DynamicModel = create_model(type_name, **fields)
|
|
1033
|
+
|
|
1034
|
+
# Register it
|
|
1035
|
+
registry.register_type(DynamicModel, type_name)
|
|
1036
|
+
logger.info(f"Created and registered Pydantic model: {type_name}")
|
|
1037
|
+
|
|
1038
|
+
except Exception as e:
|
|
1039
|
+
logger.error(f"Failed to create Pydantic model {type_name}: {e}")
|
|
1040
|
+
|
|
1041
|
+
@classmethod
|
|
1042
|
+
def _get_type_from_schema(cls, field_schema: dict[str, Any]) -> Any:
|
|
1043
|
+
"""Convert JSON schema type to Python type."""
|
|
1044
|
+
schema_type = field_schema.get("type")
|
|
1045
|
+
|
|
1046
|
+
# Basic type mapping
|
|
1047
|
+
type_mapping = {
|
|
1048
|
+
"string": str,
|
|
1049
|
+
"integer": int,
|
|
1050
|
+
"number": float,
|
|
1051
|
+
"boolean": bool,
|
|
1052
|
+
"array": list,
|
|
1053
|
+
"object": dict,
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
# Handle basic types
|
|
1057
|
+
if schema_type in type_mapping:
|
|
1058
|
+
return type_mapping[schema_type]
|
|
1059
|
+
|
|
1060
|
+
# Handle enums
|
|
1061
|
+
if "enum" in field_schema:
|
|
1062
|
+
from typing import Literal
|
|
1063
|
+
|
|
1064
|
+
return Literal[tuple(field_schema["enum"])]
|
|
1065
|
+
|
|
1066
|
+
# Default
|
|
1067
|
+
return Any
|
|
1068
|
+
|
|
1069
|
+
@classmethod
|
|
1070
|
+
def _create_dataclass(
|
|
1071
|
+
cls, type_name: str, type_def: dict[str, Any]
|
|
1072
|
+
) -> None:
|
|
1073
|
+
"""Dynamically create a dataclass from a field definition."""
|
|
1074
|
+
from dataclasses import make_dataclass
|
|
1075
|
+
|
|
1076
|
+
from flock.core.flock_registry import get_registry
|
|
1077
|
+
|
|
1078
|
+
registry = get_registry()
|
|
1079
|
+
fields_def = type_def.get("fields", {})
|
|
1080
|
+
|
|
1081
|
+
try:
|
|
1082
|
+
fields = []
|
|
1083
|
+
for field_name, field_props in fields_def.items():
|
|
1084
|
+
field_type = eval(
|
|
1085
|
+
field_props.get("type", "str")
|
|
1086
|
+
) # Note: eval is used here for simplicity
|
|
1087
|
+
fields.append((field_name, field_type))
|
|
1088
|
+
|
|
1089
|
+
# Create the dataclass
|
|
1090
|
+
DynamicDataclass = make_dataclass(type_name, fields)
|
|
1091
|
+
|
|
1092
|
+
# Register it
|
|
1093
|
+
registry.register_type(DynamicDataclass, type_name)
|
|
1094
|
+
logger.info(f"Created and registered dataclass: {type_name}")
|
|
1095
|
+
|
|
1096
|
+
except Exception as e:
|
|
1097
|
+
logger.error(f"Failed to create dataclass {type_name}: {e}")
|
|
1098
|
+
|
|
1099
|
+
@classmethod
|
|
1100
|
+
def _register_component_definitions(
|
|
1101
|
+
cls,
|
|
1102
|
+
component_defs: dict[str, Any],
|
|
1103
|
+
path_type: Literal["absolute", "relative"],
|
|
1104
|
+
) -> None:
|
|
1105
|
+
"""Register component definitions from serialized data."""
|
|
1106
|
+
import importlib
|
|
1107
|
+
import importlib.util
|
|
1108
|
+
import os
|
|
1109
|
+
import sys
|
|
1110
|
+
|
|
1111
|
+
from flock.core.flock_registry import get_registry
|
|
1112
|
+
|
|
1113
|
+
registry = get_registry()
|
|
1114
|
+
|
|
1115
|
+
for component_name, component_def in component_defs.items():
|
|
1116
|
+
logger.debug(f"Registering component: {component_name}")
|
|
1117
|
+
component_type = component_def.get("type", "flock_component")
|
|
1118
|
+
|
|
1119
|
+
try:
|
|
1120
|
+
# Handle callables differently than components
|
|
1121
|
+
if component_type == "flock_callable":
|
|
1122
|
+
# For callables, component_name is just the function name
|
|
1123
|
+
func_name = component_name
|
|
1124
|
+
module_path = component_def.get("module_path")
|
|
1125
|
+
file_path = component_def.get("file_path")
|
|
1126
|
+
|
|
1127
|
+
# Convert relative path to absolute if needed
|
|
1128
|
+
if (
|
|
1129
|
+
path_type == "relative"
|
|
1130
|
+
and file_path
|
|
1131
|
+
and not os.path.isabs(file_path)
|
|
1132
|
+
):
|
|
1133
|
+
try:
|
|
1134
|
+
# Make absolute based on current directory
|
|
1135
|
+
file_path = os.path.abspath(file_path)
|
|
1136
|
+
logger.debug(
|
|
1137
|
+
f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
|
|
1138
|
+
)
|
|
1139
|
+
except Exception as e:
|
|
1140
|
+
logger.warning(
|
|
1141
|
+
f"Could not convert relative path to absolute: {e}"
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
logger.debug(
|
|
1145
|
+
f"Processing callable '{func_name}' from module '{module_path}', file: {file_path}"
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
# Try direct import first
|
|
1149
|
+
if module_path:
|
|
1150
|
+
try:
|
|
1151
|
+
logger.debug(
|
|
1152
|
+
f"Attempting to import module: {module_path}"
|
|
1153
|
+
)
|
|
1154
|
+
module = importlib.import_module(module_path)
|
|
1155
|
+
if hasattr(module, func_name):
|
|
1156
|
+
callable_obj = getattr(module, func_name)
|
|
1157
|
+
# Register with just the name for easier lookup
|
|
1158
|
+
registry.register_callable(
|
|
1159
|
+
callable_obj, func_name
|
|
1160
|
+
)
|
|
1161
|
+
logger.info(
|
|
1162
|
+
f"Registered callable with name: {func_name}"
|
|
1163
|
+
)
|
|
1164
|
+
# Also register with fully qualified path for compatibility
|
|
1165
|
+
if module_path != "__main__":
|
|
1166
|
+
full_path = f"{module_path}.{func_name}"
|
|
1167
|
+
registry.register_callable(
|
|
1168
|
+
callable_obj, full_path
|
|
1169
|
+
)
|
|
1170
|
+
logger.info(
|
|
1171
|
+
f"Also registered callable with full path: {full_path}"
|
|
1172
|
+
)
|
|
1173
|
+
logger.info(
|
|
1174
|
+
f"Successfully registered callable {func_name} from module {module_path}"
|
|
1175
|
+
)
|
|
1176
|
+
continue
|
|
1177
|
+
else:
|
|
1178
|
+
logger.warning(
|
|
1179
|
+
f"Function '{func_name}' not found in module {module_path}"
|
|
1180
|
+
)
|
|
1181
|
+
except ImportError:
|
|
1182
|
+
logger.debug(
|
|
1183
|
+
f"Could not import module {module_path}, trying file path"
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
# Try file path if module import fails
|
|
1187
|
+
if file_path and os.path.exists(file_path):
|
|
1188
|
+
try:
|
|
1189
|
+
logger.debug(
|
|
1190
|
+
f"Attempting to load file: {file_path}"
|
|
1191
|
+
)
|
|
1192
|
+
# Create a module name from file path
|
|
1193
|
+
mod_name = f"{func_name}_module"
|
|
1194
|
+
spec = importlib.util.spec_from_file_location(
|
|
1195
|
+
mod_name, file_path
|
|
1196
|
+
)
|
|
1197
|
+
if spec and spec.loader:
|
|
1198
|
+
module = importlib.util.module_from_spec(spec)
|
|
1199
|
+
sys.modules[spec.name] = module
|
|
1200
|
+
spec.loader.exec_module(module)
|
|
1201
|
+
logger.debug(
|
|
1202
|
+
f"Successfully loaded module from file, searching for function '{func_name}'"
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
# Look for the function in the loaded module
|
|
1206
|
+
if hasattr(module, func_name):
|
|
1207
|
+
callable_obj = getattr(module, func_name)
|
|
1208
|
+
registry.register_callable(
|
|
1209
|
+
callable_obj, func_name
|
|
1210
|
+
)
|
|
1211
|
+
logger.info(
|
|
1212
|
+
f"Successfully registered callable {func_name} from file {file_path}"
|
|
1213
|
+
)
|
|
1214
|
+
else:
|
|
1215
|
+
logger.warning(
|
|
1216
|
+
f"Function {func_name} not found in file {file_path}"
|
|
1217
|
+
)
|
|
1218
|
+
else:
|
|
1219
|
+
logger.warning(
|
|
1220
|
+
f"Could not create import spec for {file_path}"
|
|
1221
|
+
)
|
|
1222
|
+
except Exception as e:
|
|
1223
|
+
logger.error(
|
|
1224
|
+
f"Error loading callable {func_name} from file {file_path}: {e}",
|
|
1225
|
+
exc_info=True,
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
# Handle regular components (existing code)
|
|
1229
|
+
else:
|
|
1230
|
+
# First try using the module path (Python import)
|
|
1231
|
+
module_path = component_def.get("module_path")
|
|
1232
|
+
if module_path and module_path != "unknown":
|
|
1233
|
+
try:
|
|
1234
|
+
logger.debug(
|
|
1235
|
+
f"Attempting to import module '{module_path}' for component '{component_name}'"
|
|
1236
|
+
)
|
|
1237
|
+
module = importlib.import_module(module_path)
|
|
1238
|
+
# Find the component class in the module
|
|
1239
|
+
for attr_name in dir(module):
|
|
1240
|
+
if attr_name == component_name:
|
|
1241
|
+
component_class = getattr(module, attr_name)
|
|
1242
|
+
registry.register_component(
|
|
1243
|
+
component_class, component_name
|
|
1244
|
+
)
|
|
1245
|
+
logger.info(
|
|
1246
|
+
f"Registered component {component_name} from {module_path}"
|
|
1247
|
+
)
|
|
1248
|
+
break
|
|
1249
|
+
else:
|
|
1250
|
+
logger.warning(
|
|
1251
|
+
f"Component {component_name} not found in module {module_path}"
|
|
1252
|
+
)
|
|
1253
|
+
# If we didn't find the component, try using file_path next
|
|
1254
|
+
raise ImportError(
|
|
1255
|
+
f"Component {component_name} not found in module {module_path}"
|
|
1256
|
+
)
|
|
1257
|
+
except ImportError:
|
|
1258
|
+
# If module import fails, try file_path approach
|
|
1259
|
+
file_path = component_def.get("file_path")
|
|
1260
|
+
|
|
1261
|
+
# Convert relative path to absolute if needed
|
|
1262
|
+
if (
|
|
1263
|
+
path_type == "relative"
|
|
1264
|
+
and file_path
|
|
1265
|
+
and not os.path.isabs(file_path)
|
|
1266
|
+
):
|
|
1267
|
+
try:
|
|
1268
|
+
# Make absolute based on current directory
|
|
1269
|
+
file_path = os.path.abspath(file_path)
|
|
1270
|
+
logger.debug(
|
|
1271
|
+
f"Converted relative path '{component_def.get('file_path')}' to absolute: '{file_path}'"
|
|
1272
|
+
)
|
|
1273
|
+
except Exception as e:
|
|
1274
|
+
logger.warning(
|
|
1275
|
+
f"Could not convert relative path to absolute: {e}"
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
if file_path and os.path.exists(file_path):
|
|
1279
|
+
logger.debug(
|
|
1280
|
+
f"Attempting to load {component_name} from file: {file_path}"
|
|
1281
|
+
)
|
|
1282
|
+
try:
|
|
1283
|
+
# Load the module from file path
|
|
1284
|
+
spec = (
|
|
1285
|
+
importlib.util.spec_from_file_location(
|
|
1286
|
+
f"{component_name}_module",
|
|
1287
|
+
file_path,
|
|
1288
|
+
)
|
|
1289
|
+
)
|
|
1290
|
+
if spec and spec.loader:
|
|
1291
|
+
module = (
|
|
1292
|
+
importlib.util.module_from_spec(
|
|
1293
|
+
spec
|
|
1294
|
+
)
|
|
1295
|
+
)
|
|
1296
|
+
sys.modules[spec.name] = module
|
|
1297
|
+
spec.loader.exec_module(module)
|
|
1298
|
+
logger.debug(
|
|
1299
|
+
f"Successfully loaded module from file, searching for component class '{component_name}'"
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
# Find the component class in the loaded module
|
|
1303
|
+
for attr_name in dir(module):
|
|
1304
|
+
if attr_name == component_name:
|
|
1305
|
+
component_class = getattr(
|
|
1306
|
+
module, attr_name
|
|
1307
|
+
)
|
|
1308
|
+
registry.register_component(
|
|
1309
|
+
component_class,
|
|
1310
|
+
component_name,
|
|
1311
|
+
)
|
|
1312
|
+
logger.info(
|
|
1313
|
+
f"Registered component {component_name} from file {file_path}"
|
|
1314
|
+
)
|
|
1315
|
+
break
|
|
1316
|
+
else:
|
|
1317
|
+
logger.warning(
|
|
1318
|
+
f"Component {component_name} not found in file {file_path}"
|
|
1319
|
+
)
|
|
1320
|
+
except Exception as e:
|
|
1321
|
+
logger.error(
|
|
1322
|
+
f"Error loading component {component_name} from file {file_path}: {e}",
|
|
1323
|
+
exc_info=True,
|
|
1324
|
+
)
|
|
1325
|
+
else:
|
|
1326
|
+
logger.warning(
|
|
1327
|
+
f"No valid file path found for component {component_name}"
|
|
1328
|
+
)
|
|
1329
|
+
else:
|
|
1330
|
+
logger.warning(
|
|
1331
|
+
f"Missing or unknown module path for component {component_name}"
|
|
1332
|
+
)
|
|
1333
|
+
except Exception as e:
|
|
1334
|
+
logger.error(
|
|
1335
|
+
f"Failed to register component {component_name}: {e}",
|
|
1336
|
+
exc_info=True,
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
@classmethod
|
|
1340
|
+
def _check_dependencies(cls, dependencies: list[str]) -> None:
|
|
1341
|
+
"""Check if required dependencies are available."""
|
|
1342
|
+
import importlib
|
|
1343
|
+
import re
|
|
1344
|
+
|
|
1345
|
+
for dependency in dependencies:
|
|
1346
|
+
# Extract package name and version
|
|
1347
|
+
match = re.match(r"([^>=<]+)([>=<].+)?", dependency)
|
|
1348
|
+
if match:
|
|
1349
|
+
package_name = match.group(1)
|
|
1350
|
+
try:
|
|
1351
|
+
importlib.import_module(package_name.replace("-", "_"))
|
|
1352
|
+
logger.debug(f"Dependency {package_name} is available")
|
|
1353
|
+
except ImportError:
|
|
1354
|
+
logger.warning(f"Dependency {dependency} is not installed")
|
|
1355
|
+
|
|
528
1356
|
# --- API Start Method ---
|
|
529
1357
|
def start_api(
|
|
530
1358
|
self,
|
|
@@ -600,20 +1428,31 @@ class Flock(BaseModel, Serializable):
|
|
|
600
1428
|
if not p.exists():
|
|
601
1429
|
raise FileNotFoundError(f"Flock file not found: {file_path}")
|
|
602
1430
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1431
|
+
try:
|
|
1432
|
+
if p.suffix in [".yaml", ".yml"]:
|
|
1433
|
+
return Flock.from_yaml_file(p)
|
|
1434
|
+
elif p.suffix == ".json":
|
|
1435
|
+
return Flock.from_json(p.read_text())
|
|
1436
|
+
elif p.suffix == ".msgpack":
|
|
1437
|
+
return Flock.from_msgpack_file(p)
|
|
1438
|
+
elif p.suffix == ".pkl":
|
|
1439
|
+
if PICKLE_AVAILABLE:
|
|
1440
|
+
return Flock.from_pickle_file(p)
|
|
1441
|
+
else:
|
|
1442
|
+
raise RuntimeError(
|
|
1443
|
+
"Cannot load Pickle file: cloudpickle not installed."
|
|
1444
|
+
)
|
|
612
1445
|
else:
|
|
613
|
-
raise
|
|
614
|
-
"
|
|
1446
|
+
raise ValueError(
|
|
1447
|
+
f"Unsupported file extension: {p.suffix}. Use .yaml, .json, .msgpack, or .pkl."
|
|
615
1448
|
)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1449
|
+
except Exception as e:
|
|
1450
|
+
# Check if it's an exception about missing types
|
|
1451
|
+
if "Could not get registered type name" in str(e):
|
|
1452
|
+
logger.error(
|
|
1453
|
+
f"Failed to load Flock from {file_path}: Missing type definition. "
|
|
1454
|
+
"This may happen if the YAML was created on a system with different types registered. "
|
|
1455
|
+
"Check if the file includes 'types' section with necessary type definitions."
|
|
1456
|
+
)
|
|
1457
|
+
logger.error(f"Error loading Flock from {file_path}: {e}")
|
|
1458
|
+
raise
|