flock-core 0.4.528__py3-none-any.whl → 0.5.0b0__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/cli/execute_flock.py +1 -1
- flock/cli/manage_agents.py +6 -6
- flock/components/__init__.py +30 -0
- flock/components/evaluation/__init__.py +9 -0
- flock/components/evaluation/declarative_evaluation_component.py +222 -0
- flock/components/routing/__init__.py +15 -0
- flock/{routers/conditional/conditional_router.py → components/routing/conditional_routing_component.py} +61 -53
- flock/components/routing/default_routing_component.py +103 -0
- flock/components/routing/llm_routing_component.py +206 -0
- flock/components/utility/__init__.py +15 -0
- flock/{modules/enterprise_memory/enterprise_memory_module.py → components/utility/memory_utility_component.py} +195 -173
- flock/{modules/performance/metrics_module.py → components/utility/metrics_utility_component.py} +110 -95
- flock/{modules/output/output_module.py → components/utility/output_utility_component.py} +47 -45
- flock/core/__init__.py +26 -18
- flock/core/agent/__init__.py +16 -0
- flock/core/agent/flock_agent_components.py +104 -0
- flock/core/agent/flock_agent_execution.py +101 -0
- flock/core/agent/flock_agent_integration.py +206 -0
- flock/core/agent/flock_agent_lifecycle.py +177 -0
- flock/core/agent/flock_agent_serialization.py +381 -0
- flock/core/api/endpoints.py +2 -2
- flock/core/api/service.py +2 -2
- flock/core/component/__init__.py +15 -0
- flock/core/{flock_module.py → component/agent_component_base.py} +136 -34
- flock/core/component/evaluation_component.py +56 -0
- flock/core/component/routing_component.py +74 -0
- flock/core/component/utility_component.py +69 -0
- flock/core/config/flock_agent_config.py +49 -2
- flock/core/evaluation/utils.py +3 -2
- flock/core/execution/batch_executor.py +1 -1
- flock/core/execution/evaluation_executor.py +2 -2
- flock/core/execution/opik_executor.py +1 -1
- flock/core/flock.py +147 -493
- flock/core/flock_agent.py +195 -1032
- flock/core/flock_factory.py +114 -90
- flock/core/flock_scheduler.py +1 -1
- flock/core/flock_server_manager.py +8 -8
- flock/core/logging/logging.py +1 -0
- flock/core/mcp/flock_mcp_server.py +53 -48
- flock/core/mcp/{flock_mcp_tool_base.py → flock_mcp_tool.py} +2 -2
- flock/core/mcp/mcp_client.py +9 -9
- flock/core/mcp/mcp_client_manager.py +9 -9
- flock/core/mcp/mcp_config.py +24 -24
- flock/core/mixin/dspy_integration.py +5 -5
- flock/core/orchestration/__init__.py +18 -0
- flock/core/orchestration/flock_batch_processor.py +94 -0
- flock/core/orchestration/flock_evaluator.py +113 -0
- flock/core/orchestration/flock_execution.py +288 -0
- flock/core/orchestration/flock_initialization.py +125 -0
- flock/core/orchestration/flock_server_manager.py +67 -0
- flock/core/orchestration/flock_web_server.py +117 -0
- flock/core/registry/__init__.py +45 -0
- flock/core/registry/agent_registry.py +69 -0
- flock/core/registry/callable_registry.py +139 -0
- flock/core/registry/component_discovery.py +142 -0
- flock/core/registry/component_registry.py +64 -0
- flock/core/registry/config_mapping.py +64 -0
- flock/core/registry/decorators.py +137 -0
- flock/core/registry/registry_hub.py +205 -0
- flock/core/registry/server_registry.py +57 -0
- flock/core/registry/type_registry.py +86 -0
- flock/core/serialization/flock_serializer.py +36 -32
- flock/core/serialization/serialization_utils.py +28 -25
- flock/core/util/hydrator.py +1 -1
- flock/core/util/input_resolver.py +29 -2
- flock/mcp/servers/sse/flock_sse_server.py +10 -10
- flock/mcp/servers/stdio/flock_stdio_server.py +10 -10
- flock/mcp/servers/streamable_http/flock_streamable_http_server.py +10 -10
- flock/mcp/servers/websockets/flock_websocket_server.py +10 -10
- flock/platform/docker_tools.py +3 -3
- flock/webapp/app/chat.py +1 -1
- flock/webapp/app/main.py +9 -5
- flock/webapp/app/services/flock_service.py +1 -1
- flock/webapp/app/services/sharing_store.py +1 -0
- flock/workflow/activities.py +67 -92
- flock/workflow/agent_execution_activity.py +6 -6
- flock/workflow/flock_workflow.py +1 -1
- flock_core-0.5.0b0.dist-info/METADATA +272 -0
- {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/RECORD +82 -95
- flock/core/flock_evaluator.py +0 -60
- flock/core/flock_registry.py +0 -702
- flock/core/flock_router.py +0 -83
- flock/evaluators/__init__.py +0 -1
- flock/evaluators/declarative/__init__.py +0 -1
- flock/evaluators/declarative/declarative_evaluator.py +0 -217
- flock/evaluators/memory/memory_evaluator.py +0 -90
- flock/evaluators/test/test_case_evaluator.py +0 -38
- flock/evaluators/zep/zep_evaluator.py +0 -59
- flock/modules/__init__.py +0 -1
- flock/modules/assertion/__init__.py +0 -1
- flock/modules/assertion/assertion_module.py +0 -286
- flock/modules/callback/__init__.py +0 -1
- flock/modules/callback/callback_module.py +0 -91
- flock/modules/enterprise_memory/README.md +0 -99
- flock/modules/mem0/__init__.py +0 -1
- flock/modules/mem0/mem0_module.py +0 -126
- flock/modules/mem0_async/__init__.py +0 -1
- flock/modules/mem0_async/async_mem0_module.py +0 -126
- flock/modules/memory/__init__.py +0 -1
- flock/modules/memory/memory_module.py +0 -429
- flock/modules/memory/memory_parser.py +0 -125
- flock/modules/memory/memory_storage.py +0 -736
- flock/modules/output/__init__.py +0 -1
- flock/modules/performance/__init__.py +0 -1
- flock/modules/zep/__init__.py +0 -1
- flock/modules/zep/zep_module.py +0 -192
- flock/routers/__init__.py +0 -1
- flock/routers/agent/__init__.py +0 -1
- flock/routers/agent/agent_router.py +0 -236
- flock/routers/agent/handoff_agent.py +0 -58
- flock/routers/default/__init__.py +0 -1
- flock/routers/default/default_router.py +0 -80
- flock/routers/feedback/feedback_router.py +0 -114
- flock/routers/list_generator/list_generator_router.py +0 -166
- flock/routers/llm/__init__.py +0 -1
- flock/routers/llm/llm_router.py +0 -365
- flock/tools/__init__.py +0 -0
- flock/tools/azure_tools.py +0 -781
- flock/tools/code_tools.py +0 -167
- flock/tools/file_tools.py +0 -149
- flock/tools/github_tools.py +0 -157
- flock/tools/markdown_tools.py +0 -205
- flock/tools/system_tools.py +0 -9
- flock/tools/text_tools.py +0 -810
- flock/tools/web_tools.py +0 -90
- flock/tools/zendesk_tools.py +0 -147
- flock_core-0.4.528.dist-info/METADATA +0 -675
- {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/WHEEL +0 -0
- {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.528.dist-info → flock_core-0.5.0b0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# src/flock/routers/correction/correction_router.py (New File)
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from pydantic import Field
|
|
6
|
-
|
|
7
|
-
from flock.core.context.context import FlockContext
|
|
8
|
-
from flock.core.flock_agent import FlockAgent
|
|
9
|
-
from flock.core.flock_registry import flock_component
|
|
10
|
-
from flock.core.flock_router import (
|
|
11
|
-
FlockRouter,
|
|
12
|
-
FlockRouterConfig,
|
|
13
|
-
HandOffRequest,
|
|
14
|
-
)
|
|
15
|
-
from flock.core.logging.logging import get_logger
|
|
16
|
-
|
|
17
|
-
logger = get_logger("router.correction")
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class FeedbackRetryRouterConfig(FlockRouterConfig):
|
|
21
|
-
max_retries: int = Field(
|
|
22
|
-
default=1,
|
|
23
|
-
description="Maximum number of times to retry the same agent on failure.",
|
|
24
|
-
)
|
|
25
|
-
feedback_context_key: str = Field(
|
|
26
|
-
default="flock.assertion_feedback",
|
|
27
|
-
description="Context key containing feedback from AssertionCheckerModule.",
|
|
28
|
-
)
|
|
29
|
-
retry_count_context_key_prefix: str = Field(
|
|
30
|
-
default="flock.retry_count_",
|
|
31
|
-
description="Prefix for context key storing retry attempts per agent.",
|
|
32
|
-
)
|
|
33
|
-
fallback_agent: str | None = Field(
|
|
34
|
-
None, description="Agent to route to if max_retries is exceeded."
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@flock_component(config_class=FeedbackRetryRouterConfig)
|
|
39
|
-
class FeedbackRetryRouter(FlockRouter):
|
|
40
|
-
"""Routes based on assertion feedback in the context.
|
|
41
|
-
|
|
42
|
-
If feedback exists for the current agent and retries are not exhausted,
|
|
43
|
-
it routes back to the same agent, adding the feedback to its input.
|
|
44
|
-
Otherwise, it can route to a fallback agent or stop the chain.
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
name: str = "feedback_retry_router"
|
|
48
|
-
config: FeedbackRetryRouterConfig = Field(
|
|
49
|
-
default_factory=FeedbackRetryRouterConfig
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
async def route(
|
|
53
|
-
self,
|
|
54
|
-
current_agent: FlockAgent,
|
|
55
|
-
result: dict[str, Any],
|
|
56
|
-
context: FlockContext,
|
|
57
|
-
) -> HandOffRequest:
|
|
58
|
-
feedback = context.get_variable(self.config.feedback_context_key)
|
|
59
|
-
|
|
60
|
-
if feedback:
|
|
61
|
-
logger.warning(
|
|
62
|
-
f"Assertion feedback detected for agent '{current_agent.name}'. Attempting retry."
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
retry_key = f"{self.config.retry_count_context_key_prefix}{current_agent.name}"
|
|
66
|
-
retry_count = context.get_variable(retry_key, 0)
|
|
67
|
-
logger.warning(f"Feedback: {feedback} - Retry Count {retry_count}")
|
|
68
|
-
|
|
69
|
-
if retry_count < self.config.max_retries:
|
|
70
|
-
logger.info(
|
|
71
|
-
f"Routing back to agent '{current_agent.name}' for retry #{retry_count + 1}"
|
|
72
|
-
)
|
|
73
|
-
context.set_variable(retry_key, retry_count + 1)
|
|
74
|
-
context.set_variable(
|
|
75
|
-
f"{current_agent.name}_prev_result", result
|
|
76
|
-
)
|
|
77
|
-
# Add feedback to the *next* agent's input (which is the same agent)
|
|
78
|
-
# Requires the agent's signature to potentially accept a 'feedback' input field.
|
|
79
|
-
return HandOffRequest(
|
|
80
|
-
next_agent=current_agent.name,
|
|
81
|
-
output_to_input_merge_strategy="match", # Add feedback to existing context/previous results
|
|
82
|
-
add_input_fields=[
|
|
83
|
-
f"{self.config.feedback_context_key} | Feedback for prev result",
|
|
84
|
-
f"{current_agent.name}_prev_result | Previous Result",
|
|
85
|
-
],
|
|
86
|
-
add_description=f"Try to fix the previous result based on the feedback.",
|
|
87
|
-
override_context=None, # Context already updated with feedback and retry count
|
|
88
|
-
)
|
|
89
|
-
else:
|
|
90
|
-
logger.error(
|
|
91
|
-
f"Max retries ({self.config.max_retries}) exceeded for agent '{current_agent.name}'."
|
|
92
|
-
)
|
|
93
|
-
# Max retries exceeded, route to fallback or stop
|
|
94
|
-
if self.config.fallback_agent:
|
|
95
|
-
logger.info(
|
|
96
|
-
f"Routing to fallback agent '{self.config.fallback_agent}'"
|
|
97
|
-
)
|
|
98
|
-
# Clear feedback before going to fallback? Optional.
|
|
99
|
-
if self.config.feedback_context_key in context.state:
|
|
100
|
-
del context.state[self.config.feedback_context_key]
|
|
101
|
-
return HandOffRequest(next_agent=self.config.fallback_agent)
|
|
102
|
-
else:
|
|
103
|
-
logger.info("No fallback agent defined. Stopping workflow.")
|
|
104
|
-
return HandOffRequest(next_agent="") # Stop the chain
|
|
105
|
-
|
|
106
|
-
else:
|
|
107
|
-
# No feedback, assertions passed or module not configured for feedback
|
|
108
|
-
logger.debug(
|
|
109
|
-
f"No assertion feedback for agent '{current_agent.name}'. Proceeding normally."
|
|
110
|
-
)
|
|
111
|
-
# Default behavior: Stop the chain if no other routing is defined
|
|
112
|
-
# In a real system, you might chain this with another router (e.g., LLMRouter)
|
|
113
|
-
# to decide the *next different* agent if assertions passed.
|
|
114
|
-
return HandOffRequest(next_agent="") # Stop or pass to next router
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
# src/flock/routers/list_generator/iterative_list_router.py (New File)
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from pydantic import Field
|
|
6
|
-
|
|
7
|
-
from flock.core.context.context import FlockContext
|
|
8
|
-
from flock.core.flock_agent import FlockAgent
|
|
9
|
-
from flock.core.flock_registry import flock_component
|
|
10
|
-
from flock.core.flock_router import (
|
|
11
|
-
FlockRouter,
|
|
12
|
-
FlockRouterConfig,
|
|
13
|
-
HandOffRequest,
|
|
14
|
-
)
|
|
15
|
-
from flock.core.logging.logging import get_logger
|
|
16
|
-
|
|
17
|
-
# Need signature utils
|
|
18
|
-
|
|
19
|
-
logger = get_logger("router.list_generator")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class IterativeListGeneratorRouterConfig(FlockRouterConfig):
|
|
23
|
-
target_list_field: str = Field(
|
|
24
|
-
...,
|
|
25
|
-
description="Name of the final list output field (e.g., 'chapters').",
|
|
26
|
-
)
|
|
27
|
-
item_output_field: str = Field(
|
|
28
|
-
...,
|
|
29
|
-
description="Name of the single item output field for each iteration (e.g., 'chapter').",
|
|
30
|
-
)
|
|
31
|
-
context_input_field: str = Field(
|
|
32
|
-
default="previous_items",
|
|
33
|
-
description="Input field name for passing back generated items (e.g., 'existing_chapters').",
|
|
34
|
-
)
|
|
35
|
-
max_iterations: int = Field(
|
|
36
|
-
default=10, description="Maximum number of items to generate."
|
|
37
|
-
)
|
|
38
|
-
# More advanced: termination_condition: Optional[Callable] = None
|
|
39
|
-
# Store iteration state in context under this prefix
|
|
40
|
-
context_state_prefix: str = Field(
|
|
41
|
-
default="flock.iterator_state_",
|
|
42
|
-
description="Prefix for context keys storing iteration state.",
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
# Field to extract item type from target_list_field signature
|
|
46
|
-
# This might require parsing the original agent's output signature
|
|
47
|
-
# item_type_str: Optional[str] = None # e.g., 'dict[str, str]' or 'MyChapterType'
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@flock_component(config_class=IterativeListGeneratorRouterConfig)
|
|
51
|
-
class IterativeListGeneratorRouter(FlockRouter):
|
|
52
|
-
name: str = "iterative_list_generator"
|
|
53
|
-
config: IterativeListGeneratorRouterConfig = Field(
|
|
54
|
-
default_factory=IterativeListGeneratorRouterConfig
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
# Helper to get state keys
|
|
58
|
-
def _get_state_keys(self, agent_name: str) -> tuple[str, str]:
|
|
59
|
-
prefix = self.config.context_state_prefix
|
|
60
|
-
list_key = f"{prefix}{agent_name}_{self.config.target_list_field}"
|
|
61
|
-
count_key = f"{prefix}{agent_name}_iteration_count"
|
|
62
|
-
return list_key, count_key
|
|
63
|
-
|
|
64
|
-
async def route(
|
|
65
|
-
self,
|
|
66
|
-
current_agent: FlockAgent,
|
|
67
|
-
result: dict[str, Any],
|
|
68
|
-
context: FlockContext,
|
|
69
|
-
) -> HandOffRequest:
|
|
70
|
-
list_key, count_key = self._get_state_keys(current_agent.name)
|
|
71
|
-
|
|
72
|
-
# --- State Initialization (First Run) ---
|
|
73
|
-
if count_key not in context.state:
|
|
74
|
-
logger.debug(
|
|
75
|
-
f"Initializing iterative list generation for '{self.config.target_list_field}' in agent '{current_agent.name}'."
|
|
76
|
-
)
|
|
77
|
-
context.set_variable(count_key, 0)
|
|
78
|
-
context.set_variable(list_key, [])
|
|
79
|
-
# Modify agent signature for the *first* iteration (remove context_input_field, use item_output_field)
|
|
80
|
-
# This requires modifying the agent's internal state or creating a temporary one.
|
|
81
|
-
# Let's try modifying the context passed to the *next* run instead.
|
|
82
|
-
context.set_variable(
|
|
83
|
-
f"{current_agent.name}.next_run_output_field",
|
|
84
|
-
self.config.item_output_field,
|
|
85
|
-
)
|
|
86
|
-
context.set_variable(
|
|
87
|
-
f"{current_agent.name}.next_run_input_fields_to_exclude",
|
|
88
|
-
{self.config.context_input_field},
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
# --- Process Result of Previous Iteration ---
|
|
92
|
-
iteration_count = context.get_variable(count_key, 0)
|
|
93
|
-
generated_items = context.get_variable(list_key, [])
|
|
94
|
-
|
|
95
|
-
# Get the single item generated in the *last* run
|
|
96
|
-
# The result dict should contain the 'item_output_field' if it wasn't the very first run
|
|
97
|
-
new_item = result.get(self.config.item_output_field)
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
new_item is not None and iteration_count > 0
|
|
101
|
-
): # Add item from previous run (not the init run)
|
|
102
|
-
generated_items.append(new_item)
|
|
103
|
-
context.set_variable(list_key, generated_items) # Update context
|
|
104
|
-
logger.info(
|
|
105
|
-
f"Added item #{iteration_count} to list '{self.config.target_list_field}' for agent '{current_agent.name}'."
|
|
106
|
-
)
|
|
107
|
-
elif iteration_count > 0:
|
|
108
|
-
logger.warning(
|
|
109
|
-
f"Iteration {iteration_count} for agent '{current_agent.name}' did not produce expected output field '{self.config.item_output_field}'."
|
|
110
|
-
)
|
|
111
|
-
# Decide how to handle: stop, retry, continue? Let's continue for now.
|
|
112
|
-
|
|
113
|
-
# Increment iteration count *after* processing the result of the previous one
|
|
114
|
-
current_iteration = iteration_count + 1
|
|
115
|
-
context.set_variable(count_key, current_iteration)
|
|
116
|
-
|
|
117
|
-
# --- Termination Check ---
|
|
118
|
-
if current_iteration > self.config.max_iterations:
|
|
119
|
-
logger.info(
|
|
120
|
-
f"Max iterations ({self.config.max_iterations}) reached for '{self.config.target_list_field}' in agent '{current_agent.name}'. Finalizing."
|
|
121
|
-
)
|
|
122
|
-
# Clean up state
|
|
123
|
-
del context.state[count_key]
|
|
124
|
-
# Final result should be the list itself under the target_list_field key
|
|
125
|
-
final_result = {self.config.target_list_field: generated_items}
|
|
126
|
-
# Handoff with empty next_agent to stop, but potentially override the *result*
|
|
127
|
-
# This is tricky. Routers usually decide the *next agent*, not the *final output*.
|
|
128
|
-
# Maybe the router should just signal termination, and the Flock run loop handles assembling the final output?
|
|
129
|
-
# Let's assume the router signals termination by returning next_agent=""
|
|
130
|
-
# The final list is already in the context under list_key.
|
|
131
|
-
# A final "AssemblerAgent" could read this context variable.
|
|
132
|
-
# OR we modify the HandOffRequest:
|
|
133
|
-
return HandOffRequest(
|
|
134
|
-
next_agent="", final_output_override=final_result
|
|
135
|
-
) # Needs HandOffRequest modification
|
|
136
|
-
|
|
137
|
-
# --- Prepare for Next Iteration ---
|
|
138
|
-
logger.info(
|
|
139
|
-
f"Routing back to agent '{current_agent.name}' for item #{current_iteration} of '{self.config.target_list_field}'."
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
# The agent needs the context (previously generated items) and the original inputs again.
|
|
143
|
-
# We will pass the generated items via the context_input_field.
|
|
144
|
-
# The original inputs (like story_outline) should still be in the context.
|
|
145
|
-
next_input_override = {
|
|
146
|
-
self.config.context_input_field: generated_items # Pass the list back
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
# Modify agent signature for the *next* iteration (add context_input_field, use item_output_field)
|
|
150
|
-
# This is the trickiest part - how to modify the agent's perceived signature for the next run?
|
|
151
|
-
# Option 1: Pass overrides via HandOffRequest (cleanest)
|
|
152
|
-
next_signature_input = f"{current_agent.input}, {self.config.context_input_field}: list | Previously generated items" # Needs smarter joining
|
|
153
|
-
next_signature_output = (
|
|
154
|
-
self.config.item_output_field
|
|
155
|
-
) # Only ask for one item
|
|
156
|
-
|
|
157
|
-
# This requires HandOffRequest and Flock execution loop to support signature overrides
|
|
158
|
-
return HandOffRequest(
|
|
159
|
-
next_agent=current_agent.name,
|
|
160
|
-
output_to_input_merge_strategy="add", # Add the context_input_field to existing context
|
|
161
|
-
input_override=next_input_override, # Provide the actual list data
|
|
162
|
-
# --- Hypothetical Overrides ---
|
|
163
|
-
next_run_input_signature_override=next_signature_input,
|
|
164
|
-
next_run_output_signature_override=next_signature_output,
|
|
165
|
-
# -----------------------------
|
|
166
|
-
)
|
flock/routers/llm/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""LLM-based router implementation for the Flock framework."""
|
flock/routers/llm/llm_router.py
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
"""LLM-based router implementation for the Flock framework."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
import litellm
|
|
7
|
-
|
|
8
|
-
from flock.core.context.context import FlockContext
|
|
9
|
-
from flock.core.flock_agent import FlockAgent
|
|
10
|
-
from flock.core.flock_registry import flock_component
|
|
11
|
-
from flock.core.flock_router import (
|
|
12
|
-
FlockRouter,
|
|
13
|
-
FlockRouterConfig,
|
|
14
|
-
HandOffRequest,
|
|
15
|
-
)
|
|
16
|
-
from flock.core.logging.logging import get_logger
|
|
17
|
-
|
|
18
|
-
logger = get_logger("llm_router")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class LLMRouterConfig(FlockRouterConfig):
|
|
22
|
-
"""Configuration for the LLM router.
|
|
23
|
-
|
|
24
|
-
This class extends FlockRouterConfig with parameters specific to the LLM router.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
temperature: float = 0.2
|
|
28
|
-
max_tokens: int = 500
|
|
29
|
-
confidence_threshold: float = 0.5
|
|
30
|
-
prompt: str = ""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@flock_component(config_class=LLMRouterConfig)
|
|
34
|
-
class LLMRouter(FlockRouter):
|
|
35
|
-
"""Router that uses an LLM to determine the next agent in a workflow.
|
|
36
|
-
|
|
37
|
-
This class is responsible for:
|
|
38
|
-
1. Analyzing available agents in the registry
|
|
39
|
-
2. Using an LLM to score each agent's suitability as the next step
|
|
40
|
-
3. Selecting the highest-scoring agent
|
|
41
|
-
4. Creating a HandOff object with the selected agent
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
def __init__(
|
|
45
|
-
self,
|
|
46
|
-
name: str = "llm_router",
|
|
47
|
-
config: LLMRouterConfig | None = None,
|
|
48
|
-
):
|
|
49
|
-
"""Initialize the LLMRouter.
|
|
50
|
-
|
|
51
|
-
Args:
|
|
52
|
-
registry: The agent registry containing all available agents
|
|
53
|
-
name: The name of the router
|
|
54
|
-
config: The router configuration
|
|
55
|
-
"""
|
|
56
|
-
logger.info(f"Initializing LLM Router '{name}'")
|
|
57
|
-
super().__init__(name=name, config=config or LLMRouterConfig(name=name))
|
|
58
|
-
logger.debug(
|
|
59
|
-
"LLM Router configuration",
|
|
60
|
-
temperature=self.config.temperature,
|
|
61
|
-
max_tokens=self.config.max_tokens,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
async def route(
|
|
65
|
-
self,
|
|
66
|
-
current_agent: FlockAgent,
|
|
67
|
-
result: dict[str, Any],
|
|
68
|
-
context: FlockContext,
|
|
69
|
-
) -> HandOffRequest:
|
|
70
|
-
"""Determine the next agent to hand off to based on the current agent's output.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
current_agent: The agent that just completed execution
|
|
74
|
-
result: The output from the current agent
|
|
75
|
-
context: The global execution context
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
A HandOff object containing the next agent and input data
|
|
79
|
-
"""
|
|
80
|
-
logger.info(
|
|
81
|
-
f"Routing from agent '{current_agent.name}'",
|
|
82
|
-
current_agent=current_agent.name,
|
|
83
|
-
)
|
|
84
|
-
logger.debug("Current agent result", result=result)
|
|
85
|
-
|
|
86
|
-
agent_definitions = context.agent_definitions
|
|
87
|
-
# Get all available agents from the registry
|
|
88
|
-
available_agents = self._get_available_agents(
|
|
89
|
-
agent_definitions, current_agent.name
|
|
90
|
-
)
|
|
91
|
-
logger.debug(
|
|
92
|
-
"Available agents for routing",
|
|
93
|
-
count=len(available_agents),
|
|
94
|
-
agents=[a.agent_data["name"] for a in available_agents],
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
if not available_agents:
|
|
98
|
-
logger.warning(
|
|
99
|
-
"No available agents for routing",
|
|
100
|
-
current_agent=current_agent.name,
|
|
101
|
-
)
|
|
102
|
-
return HandOffRequest(
|
|
103
|
-
next_agent="", override_next_agent={}, override_context=None
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Use LLM to determine the best next agent
|
|
107
|
-
next_agent_name, score = await self._select_next_agent(
|
|
108
|
-
current_agent, result, available_agents
|
|
109
|
-
)
|
|
110
|
-
logger.info(
|
|
111
|
-
"Agent selection result",
|
|
112
|
-
next_agent=next_agent_name,
|
|
113
|
-
score=score,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
if not next_agent_name or score < self.config.confidence_threshold:
|
|
117
|
-
logger.warning(
|
|
118
|
-
"No suitable next agent found",
|
|
119
|
-
best_score=score,
|
|
120
|
-
)
|
|
121
|
-
return HandOffRequest(
|
|
122
|
-
next_agent="", override_next_agent={}, override_context=None
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
# Get the next agent from the registry
|
|
126
|
-
next_agent = agent_definitions.get(next_agent_name)
|
|
127
|
-
if not next_agent:
|
|
128
|
-
logger.error(
|
|
129
|
-
"Selected agent not found in registry",
|
|
130
|
-
agent_name=next_agent_name,
|
|
131
|
-
)
|
|
132
|
-
return HandOffRequest(
|
|
133
|
-
next_agent="", override_next_agent={}, override_context=None
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
# Create input for the next agent
|
|
137
|
-
|
|
138
|
-
logger.success(
|
|
139
|
-
f"Successfully routed to agent '{next_agent_name}'",
|
|
140
|
-
score=score,
|
|
141
|
-
from_agent=current_agent.name,
|
|
142
|
-
)
|
|
143
|
-
return HandOffRequest(
|
|
144
|
-
next_agent=next_agent_name,
|
|
145
|
-
output_to_input_merge_strategy="add",
|
|
146
|
-
override_next_agent=None,
|
|
147
|
-
override_context=None,
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
def _get_available_agents(
|
|
151
|
-
self, agent_definitions: dict[str, Any], current_agent_name: str
|
|
152
|
-
) -> list[FlockAgent]:
|
|
153
|
-
"""Get all available agents except the current one.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
current_agent_name: Name of the current agent to exclude
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
List of available agents
|
|
160
|
-
"""
|
|
161
|
-
logger.debug(
|
|
162
|
-
"Getting available agents",
|
|
163
|
-
total_agents=len(agent_definitions),
|
|
164
|
-
current_agent=current_agent_name,
|
|
165
|
-
)
|
|
166
|
-
agents = []
|
|
167
|
-
for agent in agent_definitions:
|
|
168
|
-
if agent != current_agent_name:
|
|
169
|
-
agents.append(agent_definitions.get(agent))
|
|
170
|
-
return agents
|
|
171
|
-
|
|
172
|
-
async def _select_next_agent(
|
|
173
|
-
self,
|
|
174
|
-
current_agent: FlockAgent,
|
|
175
|
-
result: dict[str, Any],
|
|
176
|
-
available_agents: list[FlockAgent],
|
|
177
|
-
) -> tuple[str, float]:
|
|
178
|
-
"""Use an LLM to select the best next agent.
|
|
179
|
-
|
|
180
|
-
Args:
|
|
181
|
-
current_agent: The agent that just completed execution
|
|
182
|
-
result: The output from the current agent
|
|
183
|
-
available_agents: List of available agents to choose from
|
|
184
|
-
|
|
185
|
-
Returns:
|
|
186
|
-
Tuple of (selected_agent_name, confidence_score)
|
|
187
|
-
"""
|
|
188
|
-
logger.debug(
|
|
189
|
-
"Selecting next agent",
|
|
190
|
-
current_agent=current_agent.name,
|
|
191
|
-
available_count=len(available_agents),
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
# Prepare the prompt for the LLM
|
|
195
|
-
prompt = self._create_selection_prompt(
|
|
196
|
-
current_agent, result, available_agents
|
|
197
|
-
)
|
|
198
|
-
logger.debug("Generated selection prompt", prompt_length=len(prompt))
|
|
199
|
-
|
|
200
|
-
try:
|
|
201
|
-
logger.info(
|
|
202
|
-
"Calling LLM for agent selection",
|
|
203
|
-
model=current_agent.model,
|
|
204
|
-
temperature=self.config.temperature,
|
|
205
|
-
)
|
|
206
|
-
# Call the LLM to get the next agent
|
|
207
|
-
response = await litellm.acompletion(
|
|
208
|
-
model=current_agent.model,
|
|
209
|
-
messages=[{"role": "user", "content": prompt}],
|
|
210
|
-
temperature=self.config.temperature
|
|
211
|
-
if isinstance(self.config, LLMRouterConfig)
|
|
212
|
-
else 0.2,
|
|
213
|
-
max_tokens=self.config.max_tokens
|
|
214
|
-
if isinstance(self.config, LLMRouterConfig)
|
|
215
|
-
else 500,
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
content = response.choices[0].message.content
|
|
219
|
-
# Parse the response to get the agent name and score
|
|
220
|
-
try:
|
|
221
|
-
# extract the json object from the response
|
|
222
|
-
content = content.split("```json")[1].split("```")[0]
|
|
223
|
-
data = json.loads(content)
|
|
224
|
-
next_agent = data.get("next_agent", "")
|
|
225
|
-
score = float(data.get("score", 0))
|
|
226
|
-
reasoning = data.get("reasoning", "")
|
|
227
|
-
logger.info(
|
|
228
|
-
"Successfully parsed LLM response",
|
|
229
|
-
next_agent=next_agent,
|
|
230
|
-
score=score,
|
|
231
|
-
reasoning=reasoning,
|
|
232
|
-
)
|
|
233
|
-
return next_agent, score
|
|
234
|
-
except (json.JSONDecodeError, ValueError) as e:
|
|
235
|
-
logger.error(
|
|
236
|
-
"Failed to parse LLM response",
|
|
237
|
-
error=str(e),
|
|
238
|
-
raw_response=content,
|
|
239
|
-
)
|
|
240
|
-
logger.debug("Attempting fallback parsing")
|
|
241
|
-
|
|
242
|
-
# Fallback: try to extract the agent name from the text
|
|
243
|
-
for agent in available_agents:
|
|
244
|
-
if agent.agent_data["name"] in content:
|
|
245
|
-
logger.info(
|
|
246
|
-
"Found agent name in response using fallback",
|
|
247
|
-
agent=agent.agent_data["name"],
|
|
248
|
-
)
|
|
249
|
-
return agent.agent_data[
|
|
250
|
-
"name"
|
|
251
|
-
], 0.6 # Default score for fallback
|
|
252
|
-
|
|
253
|
-
return "", 0.0
|
|
254
|
-
|
|
255
|
-
except Exception as e:
|
|
256
|
-
logger.error(
|
|
257
|
-
"Error calling LLM for agent selection",
|
|
258
|
-
error=str(e),
|
|
259
|
-
current_agent=current_agent.name,
|
|
260
|
-
)
|
|
261
|
-
return "", 0.0
|
|
262
|
-
|
|
263
|
-
def _create_selection_prompt(
|
|
264
|
-
self,
|
|
265
|
-
current_agent: FlockAgent,
|
|
266
|
-
result: dict[str, Any],
|
|
267
|
-
available_agents: list[FlockAgent],
|
|
268
|
-
) -> str:
|
|
269
|
-
"""Create a prompt for the LLM to select the next agent.
|
|
270
|
-
|
|
271
|
-
Args:
|
|
272
|
-
current_agent: The agent that just completed execution
|
|
273
|
-
result: The output from the current agent
|
|
274
|
-
available_agents: List of available agents to choose from
|
|
275
|
-
|
|
276
|
-
Returns:
|
|
277
|
-
Prompt string for the LLM
|
|
278
|
-
"""
|
|
279
|
-
# Format the current agent's output
|
|
280
|
-
result_str = json.dumps(result, indent=2)
|
|
281
|
-
|
|
282
|
-
# Format the available agents' information
|
|
283
|
-
agents_info = []
|
|
284
|
-
for agent in available_agents:
|
|
285
|
-
agent_info = {
|
|
286
|
-
"name": agent.agent_data["name"],
|
|
287
|
-
"description": agent.agent_data["description"]
|
|
288
|
-
if agent.agent_data["description"]
|
|
289
|
-
else "",
|
|
290
|
-
"input": agent.agent_data["input"],
|
|
291
|
-
"output": agent.agent_data["output"],
|
|
292
|
-
}
|
|
293
|
-
agents_info.append(agent_info)
|
|
294
|
-
|
|
295
|
-
agents_str = json.dumps(agents_info, indent=2)
|
|
296
|
-
|
|
297
|
-
# Create the prompt
|
|
298
|
-
if self.config.prompt:
|
|
299
|
-
prompt = self.config.prompt
|
|
300
|
-
else:
|
|
301
|
-
prompt = f"""
|
|
302
|
-
You are a workflow router that determines the next agent to execute in a multi-agent system.
|
|
303
|
-
|
|
304
|
-
CURRENT AGENT:
|
|
305
|
-
Name: {current_agent.name}
|
|
306
|
-
Description: {current_agent.description}
|
|
307
|
-
Input: {current_agent.input}
|
|
308
|
-
Output: {current_agent.output}
|
|
309
|
-
|
|
310
|
-
CURRENT AGENT'S OUTPUT:
|
|
311
|
-
{result_str}
|
|
312
|
-
|
|
313
|
-
AVAILABLE AGENTS:
|
|
314
|
-
{agents_str}
|
|
315
|
-
|
|
316
|
-
Based on the current agent's output and the available agents, determine which agent should be executed next.
|
|
317
|
-
Consider the following:
|
|
318
|
-
1. Which agent's input requirements best match the current agent's output?
|
|
319
|
-
2. Which agent's purpose and description make it the most logical next step?
|
|
320
|
-
3. Which agent would provide the most value in continuing the workflow?
|
|
321
|
-
|
|
322
|
-
Respond with a JSON object containing:
|
|
323
|
-
1. "next_agent": The name of the selected agent
|
|
324
|
-
2. "score": A confidence score between 0 and 1 indicating how suitable this agent is
|
|
325
|
-
3. "reasoning": A brief explanation of why this agent was selected
|
|
326
|
-
|
|
327
|
-
If no agent is suitable, set "next_agent" to an empty string and "score" to 0.
|
|
328
|
-
|
|
329
|
-
JSON Response:
|
|
330
|
-
"""
|
|
331
|
-
return prompt
|
|
332
|
-
|
|
333
|
-
def _create_next_input(
|
|
334
|
-
self,
|
|
335
|
-
current_agent: FlockAgent,
|
|
336
|
-
result: dict[str, Any],
|
|
337
|
-
next_agent: FlockAgent,
|
|
338
|
-
) -> dict[str, Any]:
|
|
339
|
-
"""Create the input for the next agent, including the previous agent's output.
|
|
340
|
-
|
|
341
|
-
Args:
|
|
342
|
-
current_agent: The agent that just completed execution
|
|
343
|
-
result: The output from the current agent
|
|
344
|
-
next_agent: The next agent to execute
|
|
345
|
-
|
|
346
|
-
Returns:
|
|
347
|
-
Input dictionary for the next agent
|
|
348
|
-
"""
|
|
349
|
-
# Start with an empty input
|
|
350
|
-
next_input = {}
|
|
351
|
-
|
|
352
|
-
# Add a special field for the previous agent's output
|
|
353
|
-
next_input["previous_agent_output"] = {
|
|
354
|
-
"agent_name": current_agent.name,
|
|
355
|
-
"result": result,
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
# Try to map the current agent's output to the next agent's input
|
|
359
|
-
# This is a simple implementation that could be enhanced with more sophisticated mapping
|
|
360
|
-
for key in result:
|
|
361
|
-
# If the next agent expects this key, add it directly
|
|
362
|
-
if key in next_agent.input:
|
|
363
|
-
next_input[key] = result[key]
|
|
364
|
-
|
|
365
|
-
return next_input
|
flock/tools/__init__.py
DELETED
|
File without changes
|