flock-core 0.4.0b26__py3-none-any.whl → 0.4.0b27__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/context/context.py +10 -1
- flock/core/execution/temporal_executor.py +129 -20
- flock/core/flock.py +46 -2
- flock/core/flock_agent.py +145 -142
- flock/core/flock_factory.py +3 -0
- flock/workflow/agent_execution_activity.py +228 -0
- flock/workflow/flock_workflow.py +195 -28
- flock/workflow/temporal_config.py +96 -0
- flock/workflow/temporal_setup.py +23 -26
- {flock_core-0.4.0b26.dist-info → flock_core-0.4.0b27.dist-info}/METADATA +31 -5
- {flock_core-0.4.0b26.dist-info → flock_core-0.4.0b27.dist-info}/RECORD +14 -12
- {flock_core-0.4.0b26.dist-info → flock_core-0.4.0b27.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b26.dist-info → flock_core-0.4.0b27.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b26.dist-info → flock_core-0.4.0b27.dist-info}/licenses/LICENSE +0 -0
flock/core/flock_agent.py
CHANGED
|
@@ -10,6 +10,7 @@ from datetime import datetime
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
11
11
|
|
|
12
12
|
from flock.core.serialization.json_encoder import FlockJSONEncoder
|
|
13
|
+
from flock.workflow.temporal_config import TemporalActivityConfig
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from flock.core.context.context import FlockContext
|
|
@@ -110,6 +111,12 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
110
111
|
description="Dictionary of FlockModules attached to this agent.",
|
|
111
112
|
)
|
|
112
113
|
|
|
114
|
+
# --- Temporal Configuration (Optional) ---
|
|
115
|
+
temporal_activity_config: TemporalActivityConfig | None = Field(
|
|
116
|
+
default=None,
|
|
117
|
+
description="Optional Temporal settings specific to this agent's activity execution.",
|
|
118
|
+
)
|
|
119
|
+
|
|
113
120
|
# --- Runtime State (Excluded from Serialization) ---
|
|
114
121
|
context: FlockContext | None = Field(
|
|
115
122
|
default=None,
|
|
@@ -130,6 +137,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
130
137
|
modules: dict[str, "FlockModule"] | None = None, # Use dict for modules
|
|
131
138
|
write_to_file: bool = False,
|
|
132
139
|
wait_for_input: bool = False,
|
|
140
|
+
temporal_activity_config: TemporalActivityConfig | None = None,
|
|
133
141
|
**kwargs,
|
|
134
142
|
):
|
|
135
143
|
super().__init__(
|
|
@@ -146,6 +154,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
146
154
|
modules=modules
|
|
147
155
|
if modules is not None
|
|
148
156
|
else {}, # Ensure modules is a dict
|
|
157
|
+
temporal_activity_config=temporal_activity_config,
|
|
149
158
|
**kwargs,
|
|
150
159
|
)
|
|
151
160
|
|
|
@@ -723,202 +732,196 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
|
|
|
723
732
|
|
|
724
733
|
@classmethod
|
|
725
734
|
def from_dict(cls: type[T], data: dict[str, Any]) -> T:
|
|
726
|
-
"""
|
|
727
|
-
from flock.core.flock_registry import
|
|
728
|
-
|
|
729
|
-
logger.debug(
|
|
730
|
-
f"Deserializing agent from dict. Provided keys: {list(data.keys())}"
|
|
735
|
+
"""Deserialize the agent from a dictionary, including components, tools, and callables."""
|
|
736
|
+
from flock.core.flock_registry import (
|
|
737
|
+
get_registry, # Import registry locally
|
|
731
738
|
)
|
|
732
|
-
if "name" not in data:
|
|
733
|
-
raise ValueError("Agent data must include a 'name' field.")
|
|
734
|
-
FlockRegistry = get_registry()
|
|
735
|
-
agent_name = data["name"] # For logging context
|
|
736
|
-
logger.info(f"Deserializing agent '{agent_name}'")
|
|
737
|
-
|
|
738
|
-
# Pop complex components to handle them after basic agent instantiation
|
|
739
|
-
evaluator_data = data.pop("evaluator", None)
|
|
740
|
-
router_data = data.pop("handoff_router", None)
|
|
741
|
-
modules_data = data.pop("modules", {})
|
|
742
|
-
tools_data = data.pop("tools", [])
|
|
743
|
-
description_callable = data.pop("description_callable", None)
|
|
744
|
-
input_callable = data.pop("input_callable", None)
|
|
745
|
-
output_callable = data.pop("output_callable", None)
|
|
746
739
|
|
|
740
|
+
registry = get_registry()
|
|
747
741
|
logger.debug(
|
|
748
|
-
f"
|
|
742
|
+
f"Deserializing agent from dict. Keys: {list(data.keys())}"
|
|
749
743
|
)
|
|
750
744
|
|
|
751
|
-
#
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
745
|
+
# --- Separate Data ---
|
|
746
|
+
component_configs = {}
|
|
747
|
+
callable_configs = {}
|
|
748
|
+
tool_config = []
|
|
749
|
+
agent_data = {}
|
|
750
|
+
|
|
751
|
+
component_keys = [
|
|
752
|
+
"evaluator",
|
|
753
|
+
"handoff_router",
|
|
754
|
+
"modules",
|
|
755
|
+
"temporal_activity_config",
|
|
756
|
+
]
|
|
757
|
+
callable_keys = [
|
|
758
|
+
"description_callable",
|
|
759
|
+
"input_callable",
|
|
760
|
+
"output_callable",
|
|
761
|
+
]
|
|
762
|
+
tool_key = "tools"
|
|
763
|
+
|
|
764
|
+
for key, value in data.items():
|
|
765
|
+
if key in component_keys and value is not None:
|
|
766
|
+
component_configs[key] = value
|
|
767
|
+
elif key in callable_keys and value is not None:
|
|
768
|
+
callable_configs[key] = value
|
|
769
|
+
elif key == tool_key and value is not None:
|
|
770
|
+
tool_config = value # Expecting a list of names
|
|
771
|
+
elif key not in component_keys + callable_keys + [
|
|
772
|
+
tool_key
|
|
773
|
+
]: # Avoid double adding
|
|
774
|
+
agent_data[key] = value
|
|
775
|
+
# else: ignore keys that are None or already handled
|
|
776
|
+
|
|
777
|
+
# --- Deserialize Base Agent ---
|
|
778
|
+
# Ensure required fields like 'name' are present if needed by __init__
|
|
779
|
+
if "name" not in agent_data:
|
|
768
780
|
raise ValueError(
|
|
769
|
-
|
|
770
|
-
)
|
|
781
|
+
"Agent data must include a 'name' field for deserialization."
|
|
782
|
+
)
|
|
783
|
+
agent_name_log = agent_data["name"] # For logging
|
|
784
|
+
logger.info(f"Deserializing base agent data for '{agent_name_log}'")
|
|
785
|
+
|
|
786
|
+
# Pydantic should handle base fields based on type hints in __init__
|
|
787
|
+
agent = cls(**agent_data)
|
|
788
|
+
logger.debug(f"Base agent '{agent.name}' instantiated.")
|
|
771
789
|
|
|
772
|
-
# --- Deserialize
|
|
790
|
+
# --- Deserialize Components ---
|
|
791
|
+
logger.debug(f"Deserializing components for '{agent.name}'")
|
|
773
792
|
# Evaluator
|
|
774
|
-
if
|
|
793
|
+
if "evaluator" in component_configs:
|
|
775
794
|
try:
|
|
776
|
-
logger.debug(
|
|
777
|
-
f"Deserializing evaluator for agent '{agent_name}'"
|
|
778
|
-
)
|
|
779
795
|
agent.evaluator = deserialize_component(
|
|
780
|
-
|
|
781
|
-
)
|
|
782
|
-
if agent.evaluator is None:
|
|
783
|
-
raise ValueError("deserialize_component returned None")
|
|
784
|
-
logger.debug(
|
|
785
|
-
f"Deserialized evaluator '{agent.evaluator.name}' of type '{evaluator_data.get('type')}' for agent '{agent_name}'"
|
|
796
|
+
component_configs["evaluator"], FlockEvaluator
|
|
786
797
|
)
|
|
798
|
+
logger.debug(f"Deserialized evaluator for '{agent.name}'")
|
|
787
799
|
except Exception as e:
|
|
788
800
|
logger.error(
|
|
789
|
-
f"Failed to deserialize evaluator for
|
|
801
|
+
f"Failed to deserialize evaluator for '{agent.name}': {e}",
|
|
790
802
|
exc_info=True,
|
|
791
803
|
)
|
|
792
|
-
# Decide: raise error or continue without evaluator?
|
|
793
|
-
# raise ValueError(f"Failed to deserialize evaluator for agent '{agent_name}': {e}") from e
|
|
794
804
|
|
|
795
|
-
# Router
|
|
796
|
-
if
|
|
805
|
+
# Handoff Router
|
|
806
|
+
if "handoff_router" in component_configs:
|
|
797
807
|
try:
|
|
798
|
-
logger.debug(f"Deserializing router for agent '{agent_name}'")
|
|
799
808
|
agent.handoff_router = deserialize_component(
|
|
800
|
-
|
|
801
|
-
)
|
|
802
|
-
if agent.handoff_router is None:
|
|
803
|
-
raise ValueError("deserialize_component returned None")
|
|
804
|
-
logger.debug(
|
|
805
|
-
f"Deserialized router '{agent.handoff_router.name}' of type '{router_data.get('type')}' for agent '{agent_name}'"
|
|
809
|
+
component_configs["handoff_router"], FlockRouter
|
|
806
810
|
)
|
|
811
|
+
logger.debug(f"Deserialized handoff_router for '{agent.name}'")
|
|
807
812
|
except Exception as e:
|
|
808
813
|
logger.error(
|
|
809
|
-
f"Failed to deserialize
|
|
814
|
+
f"Failed to deserialize handoff_router for '{agent.name}': {e}",
|
|
810
815
|
exc_info=True,
|
|
811
816
|
)
|
|
812
|
-
# Decide: raise error or continue without router?
|
|
813
817
|
|
|
814
818
|
# Modules
|
|
815
|
-
if
|
|
816
|
-
agent.modules = {} #
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
)
|
|
820
|
-
for name, module_data in modules_data.items():
|
|
819
|
+
if "modules" in component_configs:
|
|
820
|
+
agent.modules = {} # Initialize
|
|
821
|
+
for module_name, module_data in component_configs[
|
|
822
|
+
"modules"
|
|
823
|
+
].items():
|
|
821
824
|
try:
|
|
822
|
-
logger.debug(
|
|
823
|
-
f"Deserializing module '{name}' of type '{module_data.get('type')}' for agent '{agent_name}'"
|
|
824
|
-
)
|
|
825
825
|
module_instance = deserialize_component(
|
|
826
826
|
module_data, FlockModule
|
|
827
827
|
)
|
|
828
828
|
if module_instance:
|
|
829
|
-
#
|
|
830
|
-
|
|
831
|
-
agent.add_module(
|
|
832
|
-
module_instance
|
|
833
|
-
) # Use add_module for consistency
|
|
829
|
+
# Use add_module for potential logic within it
|
|
830
|
+
agent.add_module(module_instance)
|
|
834
831
|
logger.debug(
|
|
835
|
-
f"
|
|
832
|
+
f"Deserialized and added module '{module_name}' for '{agent.name}'"
|
|
836
833
|
)
|
|
837
|
-
else:
|
|
838
|
-
raise ValueError("deserialize_component returned None")
|
|
839
834
|
except Exception as e:
|
|
840
835
|
logger.error(
|
|
841
|
-
f"Failed to deserialize module '{
|
|
836
|
+
f"Failed to deserialize module '{module_name}' for '{agent.name}': {e}",
|
|
842
837
|
exc_info=True,
|
|
843
838
|
)
|
|
844
|
-
|
|
839
|
+
|
|
840
|
+
# Temporal Activity Config
|
|
841
|
+
if "temporal_activity_config" in component_configs:
|
|
842
|
+
try:
|
|
843
|
+
agent.temporal_activity_config = TemporalActivityConfig(
|
|
844
|
+
**component_configs["temporal_activity_config"]
|
|
845
|
+
)
|
|
846
|
+
logger.debug(
|
|
847
|
+
f"Deserialized temporal_activity_config for '{agent.name}'"
|
|
848
|
+
)
|
|
849
|
+
except Exception as e:
|
|
850
|
+
logger.error(
|
|
851
|
+
f"Failed to deserialize temporal_activity_config for '{agent.name}': {e}",
|
|
852
|
+
exc_info=True,
|
|
853
|
+
)
|
|
854
|
+
agent.temporal_activity_config = None
|
|
845
855
|
|
|
846
856
|
# --- Deserialize Tools ---
|
|
847
857
|
agent.tools = [] # Initialize tools list
|
|
848
|
-
if
|
|
849
|
-
# Get component registry to look up function imports
|
|
850
|
-
registry = get_registry()
|
|
851
|
-
components = getattr(registry, "_callables", {})
|
|
852
|
-
logger.debug(
|
|
853
|
-
f"Deserializing {len(tools_data)} tools for agent '{agent_name}'"
|
|
854
|
-
)
|
|
858
|
+
if tool_config:
|
|
855
859
|
logger.debug(
|
|
856
|
-
f"
|
|
860
|
+
f"Deserializing {len(tool_config)} tools for '{agent.name}'"
|
|
857
861
|
)
|
|
858
|
-
|
|
859
|
-
for
|
|
862
|
+
# Use get_callable to find each tool
|
|
863
|
+
for tool_name_or_path in tool_config:
|
|
860
864
|
try:
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
for path_str, func in components.items():
|
|
865
|
-
if (
|
|
866
|
-
path_str.endswith("." + tool_name)
|
|
867
|
-
or path_str == tool_name
|
|
868
|
-
):
|
|
869
|
-
agent.tools.append(func)
|
|
870
|
-
found = True
|
|
871
|
-
logger.info(
|
|
872
|
-
f"Found tool '{tool_name}' via path '{path_str}' for agent '{agent_name}'"
|
|
873
|
-
)
|
|
874
|
-
break
|
|
875
|
-
|
|
876
|
-
# If not found by simple name, try manual import
|
|
877
|
-
if not found:
|
|
865
|
+
found_tool = registry.get_callable(tool_name_or_path)
|
|
866
|
+
if found_tool and callable(found_tool):
|
|
867
|
+
agent.tools.append(found_tool)
|
|
878
868
|
logger.debug(
|
|
879
|
-
f"
|
|
869
|
+
f"Resolved and added tool '{tool_name_or_path}' for agent '{agent.name}'"
|
|
880
870
|
)
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if hasattr(__main__, tool_name):
|
|
885
|
-
agent.tools.append(getattr(__main__, tool_name))
|
|
886
|
-
found = True
|
|
887
|
-
logger.info(
|
|
888
|
-
f"Found tool '{tool_name}' in __main__ module for agent '{agent_name}'"
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
if not found:
|
|
871
|
+
else:
|
|
872
|
+
# Should not happen if get_callable returns successfully but just in case
|
|
892
873
|
logger.warning(
|
|
893
|
-
f"
|
|
874
|
+
f"Registry returned non-callable for tool '{tool_name_or_path}' for agent '{agent.name}'. Skipping."
|
|
894
875
|
)
|
|
876
|
+
except (
|
|
877
|
+
ValueError
|
|
878
|
+
) as e: # get_callable raises ValueError if not found/ambiguous
|
|
879
|
+
logger.warning(
|
|
880
|
+
f"Could not resolve tool '{tool_name_or_path}' for agent '{agent.name}': {e}. Skipping."
|
|
881
|
+
)
|
|
895
882
|
except Exception as e:
|
|
896
883
|
logger.error(
|
|
897
|
-
f"
|
|
884
|
+
f"Unexpected error resolving tool '{tool_name_or_path}' for agent '{agent.name}': {e}. Skipping.",
|
|
898
885
|
exc_info=True,
|
|
899
886
|
)
|
|
900
887
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
)
|
|
905
|
-
agent.description = components[description_callable]
|
|
888
|
+
# --- Deserialize Callables ---
|
|
889
|
+
logger.debug(f"Deserializing callable fields for '{agent.name}'")
|
|
890
|
+
# available_callables = registry.get_all_callables() # Incorrect
|
|
906
891
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
892
|
+
def resolve_and_assign(field_name: str, callable_key: str):
|
|
893
|
+
if callable_key in callable_configs:
|
|
894
|
+
callable_name = callable_configs[callable_key]
|
|
895
|
+
try:
|
|
896
|
+
# Use get_callable to find the signature function
|
|
897
|
+
found_callable = registry.get_callable(callable_name)
|
|
898
|
+
if found_callable and callable(found_callable):
|
|
899
|
+
setattr(agent, field_name, found_callable)
|
|
900
|
+
logger.debug(
|
|
901
|
+
f"Resolved callable '{callable_name}' for field '{field_name}' on agent '{agent.name}'"
|
|
902
|
+
)
|
|
903
|
+
else:
|
|
904
|
+
logger.warning(
|
|
905
|
+
f"Registry returned non-callable for name '{callable_name}' for field '{field_name}' on agent '{agent.name}'. Field remains default."
|
|
906
|
+
)
|
|
907
|
+
except (
|
|
908
|
+
ValueError
|
|
909
|
+
) as e: # get_callable raises ValueError if not found/ambiguous
|
|
910
|
+
logger.warning(
|
|
911
|
+
f"Could not resolve callable '{callable_name}' in registry for field '{field_name}' on agent '{agent.name}': {e}. Field remains default."
|
|
912
|
+
)
|
|
913
|
+
except Exception as e:
|
|
914
|
+
logger.error(
|
|
915
|
+
f"Unexpected error resolving callable '{callable_name}' for field '{field_name}' on agent '{agent.name}': {e}. Field remains default.",
|
|
916
|
+
exc_info=True,
|
|
917
|
+
)
|
|
918
|
+
# Else: key not present, field retains its default value from __init__
|
|
912
919
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
)
|
|
917
|
-
agent.output = components[output_callable]
|
|
920
|
+
resolve_and_assign("description", "description_callable")
|
|
921
|
+
resolve_and_assign("input", "input_callable")
|
|
922
|
+
resolve_and_assign("output", "output_callable")
|
|
918
923
|
|
|
919
|
-
logger.info(
|
|
920
|
-
f"Successfully deserialized agent '{agent_name}' with {len(agent.modules)} modules and {len(agent.tools)} tools"
|
|
921
|
-
)
|
|
924
|
+
logger.info(f"Successfully deserialized agent '{agent.name}'.")
|
|
922
925
|
return agent
|
|
923
926
|
|
|
924
927
|
# --- Pydantic v2 Configuration ---
|
flock/core/flock_factory.py
CHANGED
|
@@ -14,6 +14,7 @@ from flock.modules.performance.metrics_module import (
|
|
|
14
14
|
MetricsModule,
|
|
15
15
|
MetricsModuleConfig,
|
|
16
16
|
)
|
|
17
|
+
from flock.workflow.temporal_config import TemporalActivityConfig
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class FlockFactory:
|
|
@@ -39,6 +40,7 @@ class FlockFactory:
|
|
|
39
40
|
write_to_file: bool = False,
|
|
40
41
|
stream: bool = False,
|
|
41
42
|
include_thought_process: bool = False,
|
|
43
|
+
temporal_activity_config: TemporalActivityConfig | None = None,
|
|
42
44
|
) -> FlockAgent:
|
|
43
45
|
"""Creates a default FlockAgent.
|
|
44
46
|
|
|
@@ -69,6 +71,7 @@ class FlockFactory:
|
|
|
69
71
|
evaluator=evaluator,
|
|
70
72
|
write_to_file=write_to_file,
|
|
71
73
|
wait_for_input=wait_for_input,
|
|
74
|
+
temporal_activity_config=temporal_activity_config,
|
|
72
75
|
)
|
|
73
76
|
output_config = OutputModuleConfig(
|
|
74
77
|
render_table=enable_rich_tables,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Defines granular Temporal activities for executing a single agent
|
|
2
|
+
and determining the next agent in a Flock workflow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from opentelemetry import trace
|
|
8
|
+
from temporalio import activity
|
|
9
|
+
|
|
10
|
+
# Third-party imports only within activity functions if needed, or pass context
|
|
11
|
+
# For core flock types, import directly
|
|
12
|
+
from flock.core.context.context import FlockContext
|
|
13
|
+
from flock.core.context.context_vars import FLOCK_MODEL
|
|
14
|
+
from flock.core.flock_agent import FlockAgent # Import concrete class if needed
|
|
15
|
+
from flock.core.flock_registry import get_registry
|
|
16
|
+
from flock.core.flock_router import HandOffRequest
|
|
17
|
+
from flock.core.logging.logging import get_logger
|
|
18
|
+
from flock.core.util.input_resolver import resolve_inputs
|
|
19
|
+
|
|
20
|
+
logger = get_logger("agent_activity") # Using a distinct logger category
|
|
21
|
+
tracer = trace.get_tracer(__name__)
|
|
22
|
+
registry = get_registry() # Get registry instance once
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@activity.defn
|
|
26
|
+
async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
|
|
27
|
+
"""Executes a single specified agent and returns its result.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
agent_name: The name of the agent to execute.
|
|
31
|
+
context: The current FlockContext (passed from the workflow).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The raw result dictionary from the agent's execution.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If the agent is not found in the registry.
|
|
38
|
+
Exception: Propagates exceptions from agent execution for Temporal retries.
|
|
39
|
+
"""
|
|
40
|
+
with tracer.start_as_current_span("execute_single_agent") as span:
|
|
41
|
+
span.set_attribute("agent.name", agent_name)
|
|
42
|
+
logger.info("Executing single agent", agent=agent_name)
|
|
43
|
+
|
|
44
|
+
agent = registry.get_agent(agent_name)
|
|
45
|
+
if not agent:
|
|
46
|
+
logger.error("Agent not found in registry", agent=agent_name)
|
|
47
|
+
# Raise error for Temporal to potentially retry/fail the activity
|
|
48
|
+
raise ValueError(f"Agent '{agent_name}' not found in registry.")
|
|
49
|
+
|
|
50
|
+
# Set agent's context reference (transient, for this execution)
|
|
51
|
+
agent.context = context
|
|
52
|
+
|
|
53
|
+
# Ensure model is set (using context value if needed)
|
|
54
|
+
# Consider if this should be done once when agent is added or workflow starts
|
|
55
|
+
if agent.model is None:
|
|
56
|
+
agent_model = context.get_variable(FLOCK_MODEL)
|
|
57
|
+
if agent_model:
|
|
58
|
+
agent.set_model(agent_model)
|
|
59
|
+
logger.debug(
|
|
60
|
+
f"Set model for agent '{agent_name}' from context: {agent_model}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Resolve agent-specific callables if necessary
|
|
64
|
+
# This might be better handled in the workflow before the loop starts
|
|
65
|
+
# or when agents are initially loaded. Assuming it's handled elsewhere for now.
|
|
66
|
+
# agent.resolve_callables(context=context)
|
|
67
|
+
|
|
68
|
+
# Resolve inputs for this specific agent run
|
|
69
|
+
previous_agent_name = (
|
|
70
|
+
context.get_last_agent_name()
|
|
71
|
+
) # Relies on context method
|
|
72
|
+
logger.debug(
|
|
73
|
+
f"Resolving inputs for {agent_name} with previous agent {previous_agent_name}"
|
|
74
|
+
)
|
|
75
|
+
agent_inputs = resolve_inputs(agent.input, context, previous_agent_name)
|
|
76
|
+
span.add_event(
|
|
77
|
+
"resolved inputs", attributes={"inputs": str(agent_inputs)}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
# Execute just this agent
|
|
82
|
+
result = await agent.run_async(agent_inputs)
|
|
83
|
+
# Avoid logging potentially large results directly to span attributes
|
|
84
|
+
result_str = str(result)
|
|
85
|
+
span.set_attribute("result.type", type(result).__name__)
|
|
86
|
+
span.set_attribute(
|
|
87
|
+
"result.preview",
|
|
88
|
+
result_str[:500] + ("..." if len(result_str) > 500 else ""),
|
|
89
|
+
)
|
|
90
|
+
logger.info("Single agent execution completed", agent=agent_name)
|
|
91
|
+
return result
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(
|
|
94
|
+
"Single agent execution failed",
|
|
95
|
+
agent=agent_name,
|
|
96
|
+
error=str(e),
|
|
97
|
+
exc_info=True,
|
|
98
|
+
)
|
|
99
|
+
span.record_exception(e)
|
|
100
|
+
# Re-raise the exception for Temporal to handle based on retry policy
|
|
101
|
+
raise
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@activity.defn
|
|
105
|
+
async def determine_next_agent(
|
|
106
|
+
current_agent_name: str, result: dict, context: FlockContext
|
|
107
|
+
) -> dict | None:
|
|
108
|
+
"""Determines the next agent using the current agent's handoff router.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
current_agent_name: The name of the agent that just ran.
|
|
112
|
+
result: The result produced by the current agent.
|
|
113
|
+
context: The current FlockContext.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
A dictionary representing the HandOffRequest (serialized via model_dump),
|
|
117
|
+
or None if no handoff occurs or router doesn't specify a next agent.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If the current agent cannot be found.
|
|
121
|
+
Exception: Propagates exceptions from router execution for Temporal retries.
|
|
122
|
+
"""
|
|
123
|
+
with tracer.start_as_current_span("determine_next_agent") as span:
|
|
124
|
+
span.set_attribute("agent.name", current_agent_name)
|
|
125
|
+
logger.info("Determining next agent after", agent=current_agent_name)
|
|
126
|
+
|
|
127
|
+
agent = registry.get_agent(current_agent_name)
|
|
128
|
+
if not agent:
|
|
129
|
+
logger.error(
|
|
130
|
+
"Agent not found for routing", agent=current_agent_name
|
|
131
|
+
)
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"Agent '{current_agent_name}' not found for routing."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not agent.handoff_router:
|
|
137
|
+
logger.info(
|
|
138
|
+
"No handoff router defined for agent", agent=current_agent_name
|
|
139
|
+
)
|
|
140
|
+
span.add_event("no_router")
|
|
141
|
+
return None # Indicate no handoff
|
|
142
|
+
|
|
143
|
+
logger.debug(
|
|
144
|
+
f"Using router {agent.handoff_router.__class__.__name__}",
|
|
145
|
+
agent=agent.name,
|
|
146
|
+
)
|
|
147
|
+
try:
|
|
148
|
+
# Execute the routing logic
|
|
149
|
+
handoff_data: (
|
|
150
|
+
HandOffRequest | Callable
|
|
151
|
+
) = await agent.handoff_router.route(agent, result, context)
|
|
152
|
+
|
|
153
|
+
# Handle callable handoff functions - This is complex in distributed systems.
|
|
154
|
+
# Consider if this pattern should be supported or if routing should always
|
|
155
|
+
# return serializable data directly. Executing arbitrary code from context
|
|
156
|
+
# within an activity can have side effects and security implications.
|
|
157
|
+
# Assuming for now it MUST return HandOffRequest or structure convertible to it.
|
|
158
|
+
if callable(handoff_data):
|
|
159
|
+
logger.warning(
|
|
160
|
+
"Callable handoff detected - executing function.",
|
|
161
|
+
agent=agent.name,
|
|
162
|
+
)
|
|
163
|
+
# Ensure context is available if the callable needs it
|
|
164
|
+
try:
|
|
165
|
+
handoff_data = handoff_data(
|
|
166
|
+
context, result
|
|
167
|
+
) # Potential side effects
|
|
168
|
+
if not isinstance(handoff_data, HandOffRequest):
|
|
169
|
+
logger.error(
|
|
170
|
+
"Handoff function did not return a HandOffRequest object.",
|
|
171
|
+
agent=agent.name,
|
|
172
|
+
)
|
|
173
|
+
raise TypeError(
|
|
174
|
+
"Handoff function must return a HandOffRequest object."
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(
|
|
178
|
+
"Handoff function execution failed",
|
|
179
|
+
agent=agent.name,
|
|
180
|
+
error=str(e),
|
|
181
|
+
exc_info=True,
|
|
182
|
+
)
|
|
183
|
+
span.record_exception(e)
|
|
184
|
+
raise # Propagate error
|
|
185
|
+
|
|
186
|
+
# Ensure we have a HandOffRequest object after potentially calling function
|
|
187
|
+
if not isinstance(handoff_data, HandOffRequest):
|
|
188
|
+
logger.error(
|
|
189
|
+
"Router returned unexpected type",
|
|
190
|
+
type=type(handoff_data).__name__,
|
|
191
|
+
agent=agent.name,
|
|
192
|
+
)
|
|
193
|
+
raise TypeError(
|
|
194
|
+
f"Router for agent '{agent.name}' did not return a HandOffRequest object."
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Ensure agent instance is converted to name for serialization across boundaries
|
|
198
|
+
if isinstance(handoff_data.next_agent, FlockAgent):
|
|
199
|
+
handoff_data.next_agent = handoff_data.next_agent.name
|
|
200
|
+
|
|
201
|
+
# If router logic determines no further agent, return None
|
|
202
|
+
if not handoff_data.next_agent:
|
|
203
|
+
logger.info("Router determined no next agent", agent=agent.name)
|
|
204
|
+
span.add_event("no_next_agent_from_router")
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
logger.info(
|
|
208
|
+
"Handoff determined",
|
|
209
|
+
next_agent=handoff_data.next_agent,
|
|
210
|
+
agent=agent.name,
|
|
211
|
+
)
|
|
212
|
+
span.set_attribute("next_agent", handoff_data.next_agent)
|
|
213
|
+
# Return the serializable HandOffRequest data using Pydantic's export method
|
|
214
|
+
return handoff_data.model_dump(
|
|
215
|
+
mode="json"
|
|
216
|
+
) # Ensure JSON-serializable
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
# Catch potential errors during routing execution
|
|
220
|
+
logger.error(
|
|
221
|
+
"Router execution failed",
|
|
222
|
+
agent=agent.name,
|
|
223
|
+
error=str(e),
|
|
224
|
+
exc_info=True,
|
|
225
|
+
)
|
|
226
|
+
span.record_exception(e)
|
|
227
|
+
# Let Temporal handle the activity failure based on retry policy
|
|
228
|
+
raise
|