flo-ai 1.0.0.dev2__tar.gz → 1.0.0.dev4__tar.gz
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.
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/PKG-INFO +1 -1
- flo_ai-1.0.0.dev4/flo_ai/__init__.py +66 -0
- flo_ai-1.0.0.dev4/flo_ai/arium/arium.py +164 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/builder.py +8 -4
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/__init__.py +1 -7
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/agent.py +92 -22
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/base_agent.py +1 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/utils/__init__.py +1 -3
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/utils/flo_utils.py +6 -7
- flo_ai-1.0.0.dev4/flo_ai/utils/variable_extractor.py +157 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/pyproject.toml +1 -1
- flo_ai-1.0.0.dev2/flo_ai/__init__.py +0 -92
- flo_ai-1.0.0.dev2/flo_ai/arium/arium.py +0 -75
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/README.md +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/README.md +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/__init__.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/base.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/examples.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/memory.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/models.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/builder/agent_builder.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/formatter/yaml_format_parser.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/__init__.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/anthropic_llm.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/base_llm.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/gemini_llm.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/ollama_llm.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/openai_llm.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/agent_error.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/tool/__init__.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/tool/base_tool.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/tool/flo_tool.py +0 -0
- {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/utils/logger.py +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flo_ai - A flexible agent framework for LLM-powered applications
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Models package - Agent framework components
|
|
6
|
+
from .models import Agent, AgentError, BaseAgent, AgentType, ReasoningPattern
|
|
7
|
+
|
|
8
|
+
from .builder.agent_builder import AgentBuilder
|
|
9
|
+
|
|
10
|
+
# LLM package - Language model integrations
|
|
11
|
+
from .llm import BaseLLM, Anthropic, OpenAI, OllamaLLM, Gemini
|
|
12
|
+
|
|
13
|
+
# Tool package - Tool framework components
|
|
14
|
+
from .tool import Tool, ToolExecutionError, flo_tool, create_tool_from_function
|
|
15
|
+
|
|
16
|
+
# Arium package - Workflow and memory components
|
|
17
|
+
from .arium import (
|
|
18
|
+
Arium,
|
|
19
|
+
BaseArium,
|
|
20
|
+
create_arium,
|
|
21
|
+
MessageMemory,
|
|
22
|
+
BaseMemory,
|
|
23
|
+
StartNode,
|
|
24
|
+
EndNode,
|
|
25
|
+
Edge,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Utils package - Utility functions
|
|
29
|
+
from .utils import FloUtils
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
# Models
|
|
33
|
+
'Agent',
|
|
34
|
+
'AgentError',
|
|
35
|
+
'BaseAgent',
|
|
36
|
+
'AgentType',
|
|
37
|
+
'ReasoningPattern',
|
|
38
|
+
# Utils
|
|
39
|
+
'FloUtils',
|
|
40
|
+
# LLM
|
|
41
|
+
'BaseLLM',
|
|
42
|
+
'Anthropic',
|
|
43
|
+
'OpenAI',
|
|
44
|
+
'OllamaLLM',
|
|
45
|
+
'Gemini',
|
|
46
|
+
# Tools
|
|
47
|
+
'Tool',
|
|
48
|
+
'ToolExecutionError',
|
|
49
|
+
'flo_tool',
|
|
50
|
+
'create_tool_from_function',
|
|
51
|
+
# Arium
|
|
52
|
+
'Arium',
|
|
53
|
+
'BaseArium',
|
|
54
|
+
'AriumBuilder',
|
|
55
|
+
'create_arium',
|
|
56
|
+
'MessageMemory',
|
|
57
|
+
'BaseMemory',
|
|
58
|
+
'StartNode',
|
|
59
|
+
'EndNode',
|
|
60
|
+
'Edge',
|
|
61
|
+
# Builder
|
|
62
|
+
'AgentBuilder',
|
|
63
|
+
'AriumBuilder',
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
__version__ = '1.0.0'
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from flo_ai.arium.base import BaseArium
|
|
2
|
+
from flo_ai.arium.memory import MessageMemory, BaseMemory
|
|
3
|
+
from flo_ai.llm.base_llm import ImageMessage
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
from flo_ai.models.agent import Agent
|
|
6
|
+
from flo_ai.tool.base_tool import Tool
|
|
7
|
+
from flo_ai.arium.models import StartNode, EndNode
|
|
8
|
+
from flo_ai.utils.logger import logger
|
|
9
|
+
from flo_ai.utils.variable_extractor import (
|
|
10
|
+
extract_variables_from_inputs,
|
|
11
|
+
extract_agent_variables,
|
|
12
|
+
validate_multi_agent_variables,
|
|
13
|
+
resolve_variables,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Arium(BaseArium):
|
|
18
|
+
def __init__(self, memory: BaseMemory):
|
|
19
|
+
super().__init__()
|
|
20
|
+
self.is_compiled = False
|
|
21
|
+
self.memory = memory if memory else MessageMemory()
|
|
22
|
+
|
|
23
|
+
def compile(self):
|
|
24
|
+
self.validate_graph()
|
|
25
|
+
self.is_compiled = True
|
|
26
|
+
|
|
27
|
+
async def run(
|
|
28
|
+
self,
|
|
29
|
+
inputs: List[str | ImageMessage],
|
|
30
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
31
|
+
):
|
|
32
|
+
if not self.is_compiled:
|
|
33
|
+
raise ValueError('Arium is not compiled')
|
|
34
|
+
|
|
35
|
+
if not self.memory:
|
|
36
|
+
raise ValueError('Arium has no memory')
|
|
37
|
+
|
|
38
|
+
if not self.nodes:
|
|
39
|
+
raise ValueError('Arium has no nodes')
|
|
40
|
+
|
|
41
|
+
# Extract and validate variables from inputs and all agents
|
|
42
|
+
self._extract_and_validate_variables(inputs, variables)
|
|
43
|
+
|
|
44
|
+
# Resolve variables in inputs and agent prompts
|
|
45
|
+
resolved_inputs = self._resolve_inputs(inputs, variables)
|
|
46
|
+
self._resolve_agent_prompts(variables)
|
|
47
|
+
|
|
48
|
+
return await self._execute_graph(resolved_inputs)
|
|
49
|
+
|
|
50
|
+
async def _execute_graph(self, inputs: List[str | ImageMessage]):
|
|
51
|
+
[self.memory.add(msg) for msg in inputs]
|
|
52
|
+
|
|
53
|
+
current_node = self.nodes[self.start_node_name]
|
|
54
|
+
current_edge = self.edges[self.start_node_name]
|
|
55
|
+
|
|
56
|
+
logger.info(f'Executing graph from {current_node.name}')
|
|
57
|
+
while current_node.name != self.end_node_name:
|
|
58
|
+
# execute current node
|
|
59
|
+
result = await self._execute_node(current_node)
|
|
60
|
+
|
|
61
|
+
# update results to memory
|
|
62
|
+
self._add_to_memory(result)
|
|
63
|
+
|
|
64
|
+
# find next node post current node
|
|
65
|
+
next_node_name = current_edge.router_fn(memory=self.memory)
|
|
66
|
+
|
|
67
|
+
# find next edge
|
|
68
|
+
# TODO: next_node_name might not be in self.edges if it's the end node. Handle this case
|
|
69
|
+
next_edge = (
|
|
70
|
+
self.edges[next_node_name] if next_node_name in self.edges else None
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# update current node
|
|
74
|
+
current_node = self.nodes[next_node_name]
|
|
75
|
+
current_edge = next_edge
|
|
76
|
+
|
|
77
|
+
return self.memory.get()
|
|
78
|
+
|
|
79
|
+
def _extract_and_validate_variables(
|
|
80
|
+
self, inputs: List[str | ImageMessage], variables: Dict[str, Any]
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Extract variables from inputs and agents, then validate them.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
inputs: List of input messages
|
|
86
|
+
variables: Dictionary of variable name to value mappings
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
ValueError: If any required variables are missing
|
|
90
|
+
"""
|
|
91
|
+
# Extract variables from inputs
|
|
92
|
+
input_variables = extract_variables_from_inputs(inputs)
|
|
93
|
+
|
|
94
|
+
# Extract variables from all agents in the workflow
|
|
95
|
+
agents_variables = {}
|
|
96
|
+
for node in self.nodes.values():
|
|
97
|
+
if isinstance(node, Agent):
|
|
98
|
+
agent_vars = extract_agent_variables(node)
|
|
99
|
+
if agent_vars:
|
|
100
|
+
agents_variables[node.name] = agent_vars
|
|
101
|
+
|
|
102
|
+
# Validate input variables separately with cleaner error message
|
|
103
|
+
if input_variables:
|
|
104
|
+
missing_input_vars = input_variables - set(variables.keys())
|
|
105
|
+
if missing_input_vars:
|
|
106
|
+
provided_keys = sorted(variables.keys())
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f'Input contains missing variables: {sorted(missing_input_vars)}. '
|
|
109
|
+
f'Provided variables: {provided_keys}'
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Validate agent variables with detailed agent breakdown
|
|
113
|
+
if agents_variables:
|
|
114
|
+
validate_multi_agent_variables(agents_variables, variables)
|
|
115
|
+
|
|
116
|
+
def _resolve_inputs(
|
|
117
|
+
self, inputs: List[str | ImageMessage], variables: Dict[str, Any]
|
|
118
|
+
) -> List[str | ImageMessage]:
|
|
119
|
+
"""Resolve variables in input messages.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
inputs: List of input messages
|
|
123
|
+
variables: Dictionary of variable name to value mappings
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
List of inputs with variables resolved
|
|
127
|
+
"""
|
|
128
|
+
resolved_inputs = []
|
|
129
|
+
for input_item in inputs:
|
|
130
|
+
if isinstance(input_item, str):
|
|
131
|
+
# Resolve variables in text input
|
|
132
|
+
resolved_input = resolve_variables(input_item, variables)
|
|
133
|
+
resolved_inputs.append(resolved_input)
|
|
134
|
+
else:
|
|
135
|
+
# ImageMessage objects don't need variable resolution
|
|
136
|
+
resolved_inputs.append(input_item)
|
|
137
|
+
return resolved_inputs
|
|
138
|
+
|
|
139
|
+
def _resolve_agent_prompts(self, variables: Dict[str, Any]) -> None:
|
|
140
|
+
"""Resolve variables in all agent system prompts and mark them as resolved.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
variables: Dictionary of variable name to value mappings
|
|
144
|
+
"""
|
|
145
|
+
for node in self.nodes.values():
|
|
146
|
+
if isinstance(node, Agent):
|
|
147
|
+
node.system_prompt = resolve_variables(node.system_prompt, variables)
|
|
148
|
+
node.resolved_variables = True
|
|
149
|
+
|
|
150
|
+
async def _execute_node(self, node: Agent | Tool | StartNode | EndNode):
|
|
151
|
+
if isinstance(node, Agent):
|
|
152
|
+
# Variables are already resolved, pass empty dict to avoid re-processing
|
|
153
|
+
return await node.run(self.memory.get(), variables={})
|
|
154
|
+
elif isinstance(node, Tool):
|
|
155
|
+
return await node.execute(self.memory.get())
|
|
156
|
+
elif isinstance(node, StartNode):
|
|
157
|
+
return None
|
|
158
|
+
elif isinstance(node, EndNode):
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _add_to_memory(self, result: str):
|
|
162
|
+
# TODO result will be None for start and end nodes
|
|
163
|
+
if result:
|
|
164
|
+
self.memory.add(result)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Optional, Callable, Union
|
|
1
|
+
from typing import List, Optional, Callable, Union, Dict, Any
|
|
2
2
|
from flo_ai.arium.arium import Arium
|
|
3
3
|
from flo_ai.arium.memory import MessageMemory, BaseMemory
|
|
4
4
|
from flo_ai.models.agent import Agent
|
|
@@ -128,10 +128,14 @@ class AriumBuilder:
|
|
|
128
128
|
self._arium = arium
|
|
129
129
|
return arium
|
|
130
130
|
|
|
131
|
-
async def build_and_run(
|
|
132
|
-
|
|
131
|
+
async def build_and_run(
|
|
132
|
+
self,
|
|
133
|
+
inputs: List[Union[str, ImageMessage]],
|
|
134
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
135
|
+
) -> List[dict]:
|
|
136
|
+
"""Build the Arium and run it with the given inputs and optional runtime variables."""
|
|
133
137
|
arium = self.build()
|
|
134
|
-
return await arium.run(inputs)
|
|
138
|
+
return await arium.run(inputs, variables=variables)
|
|
135
139
|
|
|
136
140
|
def visualize(
|
|
137
141
|
self, output_path: str = 'arium_graph.png', title: str = 'Arium Workflow'
|
|
@@ -6,10 +6,4 @@ from .agent import Agent
|
|
|
6
6
|
from .agent_error import AgentError
|
|
7
7
|
from .base_agent import BaseAgent, AgentType, ReasoningPattern
|
|
8
8
|
|
|
9
|
-
__all__ = [
|
|
10
|
-
"Agent",
|
|
11
|
-
"AgentError",
|
|
12
|
-
"BaseAgent",
|
|
13
|
-
"AgentType",
|
|
14
|
-
"ReasoningPattern"
|
|
15
|
-
]
|
|
9
|
+
__all__ = ['Agent', 'AgentError', 'BaseAgent', 'AgentType', 'ReasoningPattern']
|
|
@@ -5,6 +5,12 @@ from flo_ai.llm.base_llm import BaseLLM, ImageMessage
|
|
|
5
5
|
from flo_ai.tool.base_tool import Tool, ToolExecutionError
|
|
6
6
|
from flo_ai.models.agent_error import AgentError
|
|
7
7
|
from flo_ai.utils.logger import logger
|
|
8
|
+
from flo_ai.utils.variable_extractor import (
|
|
9
|
+
extract_variables_from_inputs,
|
|
10
|
+
extract_agent_variables,
|
|
11
|
+
validate_multi_agent_variables,
|
|
12
|
+
resolve_variables,
|
|
13
|
+
)
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
class Agent(BaseAgent):
|
|
@@ -40,35 +46,79 @@ class Agent(BaseAgent):
|
|
|
40
46
|
self.output_schema = output_schema
|
|
41
47
|
self.role = role
|
|
42
48
|
|
|
43
|
-
async def run(
|
|
49
|
+
async def run(
|
|
50
|
+
self,
|
|
51
|
+
inputs: List[str | ImageMessage] | str,
|
|
52
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
53
|
+
) -> str:
|
|
54
|
+
variables = variables or {}
|
|
55
|
+
|
|
44
56
|
if isinstance(inputs, str):
|
|
45
57
|
inputs = [inputs]
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
# Perform runtime variable validation if not already resolved (single agent usage)
|
|
60
|
+
if not self.resolved_variables:
|
|
61
|
+
# Extract variables from inputs and system prompt
|
|
62
|
+
input_variables = extract_variables_from_inputs(inputs)
|
|
63
|
+
agent_variables = extract_agent_variables(self)
|
|
64
|
+
all_required_variables = input_variables.union(agent_variables)
|
|
65
|
+
|
|
66
|
+
# Validate that all required variables are provided
|
|
67
|
+
if all_required_variables:
|
|
68
|
+
agents_variables = {self.name: all_required_variables}
|
|
69
|
+
validate_multi_agent_variables(agents_variables, variables)
|
|
70
|
+
|
|
71
|
+
# Resolve variables and mark as resolved
|
|
72
|
+
self.system_prompt = resolve_variables(self.system_prompt, variables)
|
|
73
|
+
|
|
74
|
+
# Process inputs and resolve variables in string inputs
|
|
75
|
+
for input in inputs:
|
|
76
|
+
if isinstance(input, ImageMessage):
|
|
77
|
+
self.add_to_history('user', self.llm.format_image_in_message(input))
|
|
78
|
+
else:
|
|
79
|
+
# Resolve variables in text input
|
|
80
|
+
resolved_input = resolve_variables(input, variables)
|
|
81
|
+
self.add_to_history('user', resolved_input)
|
|
82
|
+
|
|
83
|
+
# after resolving agent system prompts and inputs, mark variables as resolved
|
|
84
|
+
self.resolved_variables = True
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
# Variables already resolved, process inputs without variable resolution
|
|
88
|
+
for input in inputs:
|
|
89
|
+
if isinstance(input, ImageMessage):
|
|
90
|
+
self.add_to_history('user', self.llm.format_image_in_message(input))
|
|
91
|
+
else:
|
|
92
|
+
self.add_to_history('user', input)
|
|
52
93
|
|
|
53
94
|
retry_count = 0
|
|
54
95
|
|
|
55
96
|
# If no tools, act as conversational agent
|
|
56
97
|
if not self.tools:
|
|
57
|
-
return await self._run_conversational(retry_count)
|
|
98
|
+
return await self._run_conversational(retry_count, variables)
|
|
58
99
|
|
|
59
100
|
# Otherwise, run as tool agent
|
|
60
|
-
return await self._run_with_tools(retry_count)
|
|
101
|
+
return await self._run_with_tools(retry_count, variables)
|
|
61
102
|
|
|
62
|
-
async def _run_conversational(
|
|
103
|
+
async def _run_conversational(
|
|
104
|
+
self, retry_count: int, variables: Optional[Dict[str, Any]] = None
|
|
105
|
+
) -> str:
|
|
63
106
|
"""Run as a conversational agent when no tools are provided"""
|
|
107
|
+
variables = variables or {}
|
|
108
|
+
|
|
64
109
|
while retry_count < self.max_retries:
|
|
65
110
|
try:
|
|
111
|
+
# Resolve variables in system prompt
|
|
112
|
+
system_content = (
|
|
113
|
+
self._get_cot_prompt(variables)
|
|
114
|
+
if self.reasoning_pattern == ReasoningPattern.COT
|
|
115
|
+
else resolve_variables(self.system_prompt, variables)
|
|
116
|
+
)
|
|
117
|
+
|
|
66
118
|
messages = [
|
|
67
119
|
{
|
|
68
120
|
'role': 'system',
|
|
69
|
-
'content':
|
|
70
|
-
if self.reasoning_pattern == ReasoningPattern.COT
|
|
71
|
-
else self.system_prompt,
|
|
121
|
+
'content': system_content,
|
|
72
122
|
}
|
|
73
123
|
] + self.conversation_history
|
|
74
124
|
|
|
@@ -111,18 +161,26 @@ class Agent(BaseAgent):
|
|
|
111
161
|
original_error=e,
|
|
112
162
|
)
|
|
113
163
|
|
|
114
|
-
async def _run_with_tools(
|
|
164
|
+
async def _run_with_tools(
|
|
165
|
+
self, retry_count: int = 0, variables: Optional[Dict[str, Any]] = None
|
|
166
|
+
) -> str:
|
|
115
167
|
"""Run as a tool-using agent when tools are provided"""
|
|
168
|
+
variables = variables or {}
|
|
169
|
+
|
|
116
170
|
while retry_count < self.max_retries:
|
|
117
171
|
try:
|
|
172
|
+
# Resolve variables in system prompt based on reasoning pattern
|
|
173
|
+
if self.reasoning_pattern == ReasoningPattern.REACT:
|
|
174
|
+
system_content = self._get_react_prompt(variables)
|
|
175
|
+
elif self.reasoning_pattern == ReasoningPattern.COT:
|
|
176
|
+
system_content = self._get_cot_prompt(variables)
|
|
177
|
+
else:
|
|
178
|
+
system_content = resolve_variables(self.system_prompt, variables)
|
|
179
|
+
|
|
118
180
|
messages = [
|
|
119
181
|
{
|
|
120
182
|
'role': 'system',
|
|
121
|
-
'content':
|
|
122
|
-
if self.reasoning_pattern == ReasoningPattern.REACT
|
|
123
|
-
else self._get_cot_prompt()
|
|
124
|
-
if self.reasoning_pattern == ReasoningPattern.COT
|
|
125
|
-
else self.system_prompt,
|
|
183
|
+
'content': system_content,
|
|
126
184
|
}
|
|
127
185
|
] + self.conversation_history
|
|
128
186
|
|
|
@@ -239,12 +297,18 @@ class Agent(BaseAgent):
|
|
|
239
297
|
|
|
240
298
|
raise AgentError(f'Failed after maximum {self.max_retries} attempts.')
|
|
241
299
|
|
|
242
|
-
def _get_react_prompt(self) -> str:
|
|
300
|
+
def _get_react_prompt(self, variables: Optional[Dict[str, Any]] = None) -> str:
|
|
243
301
|
"""Get system prompt modified for ReACT pattern"""
|
|
302
|
+
variables = variables or {}
|
|
303
|
+
|
|
244
304
|
tools_desc = '\n'.join(
|
|
245
305
|
[f'- {tool.name}: {tool.description}' for tool in self.tools]
|
|
246
306
|
)
|
|
247
|
-
|
|
307
|
+
|
|
308
|
+
# Resolve variables in the base system prompt
|
|
309
|
+
resolved_system_prompt = resolve_variables(self.system_prompt, variables)
|
|
310
|
+
|
|
311
|
+
react_prompt = f"""{resolved_system_prompt}
|
|
248
312
|
When solving tasks, follow this format:
|
|
249
313
|
|
|
250
314
|
Thought: Analyze the situation and think about what to do
|
|
@@ -263,12 +327,18 @@ class Agent(BaseAgent):
|
|
|
263
327
|
|
|
264
328
|
return react_prompt
|
|
265
329
|
|
|
266
|
-
def _get_cot_prompt(self) -> str:
|
|
330
|
+
def _get_cot_prompt(self, variables: Optional[Dict[str, Any]] = None) -> str:
|
|
267
331
|
"""Get system prompt modified for Chain of Thought pattern"""
|
|
332
|
+
variables = variables or {}
|
|
333
|
+
|
|
268
334
|
tools_desc = '\n'.join(
|
|
269
335
|
[f'- {tool.name}: {tool.description}' for tool in self.tools]
|
|
270
336
|
)
|
|
271
|
-
|
|
337
|
+
|
|
338
|
+
# Resolve variables in the base system prompt
|
|
339
|
+
resolved_system_prompt = resolve_variables(self.system_prompt, variables)
|
|
340
|
+
|
|
341
|
+
cot_prompt = f"""{resolved_system_prompt}
|
|
272
342
|
When solving tasks, follow this Chain of Thought reasoning format:
|
|
273
343
|
|
|
274
344
|
Let me think through this step by step:
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from typing import Dict, Any
|
|
2
|
-
import re
|
|
3
2
|
import json
|
|
4
3
|
from flo_ai.utils.logger import logger
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class FloUtils:
|
|
8
|
-
|
|
9
7
|
@staticmethod
|
|
10
8
|
def extract_jsons_from_string(data: str, strict: bool = False) -> Dict[str, Any]:
|
|
11
9
|
"""
|
|
@@ -14,6 +12,7 @@ class FloUtils:
|
|
|
14
12
|
3) Merge into one dict (later keys override earlier)
|
|
15
13
|
4) On strict mode, raise FloException if no JSON found
|
|
16
14
|
"""
|
|
15
|
+
|
|
17
16
|
# Custom function to find balanced braces since (?R) is not supported in Python re
|
|
18
17
|
def find_balanced_braces(text):
|
|
19
18
|
matches = []
|
|
@@ -25,10 +24,10 @@ class FloUtils:
|
|
|
25
24
|
j = i + 1
|
|
26
25
|
in_string = False
|
|
27
26
|
escape_next = False
|
|
28
|
-
|
|
27
|
+
|
|
29
28
|
while j < len(text) and brace_count > 0:
|
|
30
29
|
char = text[j]
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
if escape_next:
|
|
33
32
|
escape_next = False
|
|
34
33
|
elif char == '\\' and in_string:
|
|
@@ -40,9 +39,9 @@ class FloUtils:
|
|
|
40
39
|
brace_count += 1
|
|
41
40
|
elif char == '}':
|
|
42
41
|
brace_count -= 1
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
j += 1
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
if brace_count == 0:
|
|
47
46
|
candidate = text[start:j]
|
|
48
47
|
# Try to validate it's actually JSON by attempting to parse
|
|
@@ -77,7 +76,7 @@ class FloUtils:
|
|
|
77
76
|
raise ValueError(f'No JSON found in strict mode: {data}')
|
|
78
77
|
|
|
79
78
|
return merged
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
@staticmethod
|
|
82
81
|
def strip_comments_from_string(data: str) -> str:
|
|
83
82
|
"""Remove JS-style comments (// and /*…*/) so json.loads() will succeed."""
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Variable extraction utilities for the Flo AI framework.
|
|
3
|
+
|
|
4
|
+
This module provides functions to extract variable placeholders from text,
|
|
5
|
+
inputs, and agent configurations for runtime variable validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import List, Set, Dict, Any
|
|
10
|
+
from flo_ai.llm.base_llm import ImageMessage
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def extract_variables_from_text(text: str) -> Set[str]:
|
|
14
|
+
"""Extract variable placeholders from text using <variable_name> pattern.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
text: Text containing variable placeholders like <variable_name>
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Set of variable names found in the text
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> extract_variables_from_text("Hello <name>, your <role> is important")
|
|
24
|
+
{'name', 'role'}
|
|
25
|
+
>>> extract_variables_from_text("No variables here")
|
|
26
|
+
set()
|
|
27
|
+
"""
|
|
28
|
+
if not text:
|
|
29
|
+
return set()
|
|
30
|
+
|
|
31
|
+
# Use regex to find all <variable_name> patterns
|
|
32
|
+
# \w+ matches word characters (letters, digits, underscore)
|
|
33
|
+
variable_pattern = r'<(\w+)>'
|
|
34
|
+
matches = re.findall(variable_pattern, text)
|
|
35
|
+
return set(matches)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def extract_variables_from_inputs(inputs: List[str | ImageMessage]) -> Set[str]:
|
|
39
|
+
"""Extract variable placeholders from a list of input messages.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
inputs: List of input strings or ImageMessages
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Set of variable names found across all string inputs
|
|
46
|
+
|
|
47
|
+
Note:
|
|
48
|
+
ImageMessage objects are skipped as they don't contain variable placeholders
|
|
49
|
+
"""
|
|
50
|
+
all_variables = set()
|
|
51
|
+
|
|
52
|
+
for input_item in inputs:
|
|
53
|
+
if isinstance(input_item, str):
|
|
54
|
+
variables = extract_variables_from_text(input_item)
|
|
55
|
+
all_variables.update(variables)
|
|
56
|
+
# Skip ImageMessage objects as they don't contain text variables
|
|
57
|
+
|
|
58
|
+
return all_variables
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def extract_agent_variables(agent) -> Set[str]:
|
|
62
|
+
"""Extract variable placeholders from an agent's system prompt.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
agent: Agent instance with a system_prompt attribute
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Set of variable names found in the agent's system prompt
|
|
69
|
+
|
|
70
|
+
Note:
|
|
71
|
+
This function avoids importing Agent to prevent circular imports
|
|
72
|
+
"""
|
|
73
|
+
if not hasattr(agent, 'system_prompt'):
|
|
74
|
+
return set()
|
|
75
|
+
|
|
76
|
+
return extract_variables_from_text(agent.system_prompt)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def validate_variables(
|
|
80
|
+
required_variables: Set[str], provided_variables: Dict[str, Any], context: str = ''
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Validate that all required variables are provided.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
required_variables: Set of variable names that are required
|
|
86
|
+
provided_variables: Dictionary of variable name to value mappings
|
|
87
|
+
context: Optional context string for error messages (e.g., agent name)
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If any required variables are missing
|
|
91
|
+
"""
|
|
92
|
+
missing = required_variables - set(provided_variables.keys())
|
|
93
|
+
if missing:
|
|
94
|
+
available = list(provided_variables.keys()) if provided_variables else []
|
|
95
|
+
context_str = f' for {context}' if context else ''
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f'Missing required variables{context_str}: {sorted(missing)}. '
|
|
98
|
+
f'Available variables: {available}'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_variables(text: str, variables: Dict[str, Any]) -> str:
|
|
103
|
+
"""Replace <variable_name> patterns with actual values
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
text: Text containing variable placeholders like <variable_name>
|
|
107
|
+
variables: Dictionary of variable name to value mappings
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Text with variables resolved
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: If a variable placeholder is found but not provided in variables
|
|
114
|
+
"""
|
|
115
|
+
if not text or not variables:
|
|
116
|
+
return text
|
|
117
|
+
|
|
118
|
+
def replace_var(match):
|
|
119
|
+
var_name = match.group(1)
|
|
120
|
+
if var_name in variables:
|
|
121
|
+
return str(variables[var_name])
|
|
122
|
+
else:
|
|
123
|
+
available = list(variables.keys())
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Variable '{var_name}' referenced in text but not provided. "
|
|
126
|
+
f'Available variables: {available}'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return re.sub(r'<(\w+)>', replace_var, text)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_multi_agent_variables(
|
|
133
|
+
agents_variables: Dict[str, Set[str]], provided_variables: Dict[str, Any]
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Validate variables for multiple agents and provide detailed error messages.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
agents_variables: Dictionary mapping agent names to their required variables
|
|
139
|
+
provided_variables: Dictionary of variable name to value mappings
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If any agents are missing required variables, with detailed breakdown
|
|
143
|
+
"""
|
|
144
|
+
missing_by_agent = {}
|
|
145
|
+
provided_keys = set(provided_variables.keys())
|
|
146
|
+
|
|
147
|
+
for agent_name, required_vars in agents_variables.items():
|
|
148
|
+
missing = required_vars - provided_keys
|
|
149
|
+
if missing:
|
|
150
|
+
missing_by_agent[agent_name] = sorted(missing)
|
|
151
|
+
|
|
152
|
+
if missing_by_agent:
|
|
153
|
+
error_msg = 'Missing required variables for agents:\n'
|
|
154
|
+
for agent_name, missing_vars in missing_by_agent.items():
|
|
155
|
+
error_msg += f" - Agent '{agent_name}': {missing_vars}\n"
|
|
156
|
+
error_msg += f'Provided variables: {sorted(provided_keys)}'
|
|
157
|
+
raise ValueError(error_msg)
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
flo_ai - A flexible agent framework for LLM-powered applications
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
# Models package - Agent framework components
|
|
6
|
-
from .models import (
|
|
7
|
-
Agent,
|
|
8
|
-
AgentError,
|
|
9
|
-
BaseAgent,
|
|
10
|
-
AgentType,
|
|
11
|
-
ReasoningPattern
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
from .builder.agent_builder import (
|
|
15
|
-
AgentBuilder
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
# LLM package - Language model integrations
|
|
19
|
-
from .llm import (
|
|
20
|
-
BaseLLM,
|
|
21
|
-
Anthropic,
|
|
22
|
-
OpenAI,
|
|
23
|
-
OllamaLLM,
|
|
24
|
-
Gemini
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
# Tool package - Tool framework components
|
|
28
|
-
from .tool import (
|
|
29
|
-
Tool,
|
|
30
|
-
ToolExecutionError,
|
|
31
|
-
flo_tool,
|
|
32
|
-
create_tool_from_function
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
# Arium package - Workflow and memory components
|
|
36
|
-
from .arium import (
|
|
37
|
-
Arium,
|
|
38
|
-
BaseArium,
|
|
39
|
-
create_arium,
|
|
40
|
-
MessageMemory,
|
|
41
|
-
BaseMemory,
|
|
42
|
-
StartNode,
|
|
43
|
-
EndNode,
|
|
44
|
-
Edge
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# Utils package - Utility functions
|
|
48
|
-
from .utils import (
|
|
49
|
-
FloUtils
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
__all__ = [
|
|
53
|
-
# Models
|
|
54
|
-
"Agent",
|
|
55
|
-
"AgentError",
|
|
56
|
-
"BaseAgent",
|
|
57
|
-
"AgentType",
|
|
58
|
-
"ReasoningPattern",
|
|
59
|
-
|
|
60
|
-
# Utils
|
|
61
|
-
"FloUtils",
|
|
62
|
-
|
|
63
|
-
# LLM
|
|
64
|
-
"BaseLLM",
|
|
65
|
-
"Anthropic",
|
|
66
|
-
"OpenAI",
|
|
67
|
-
"OllamaLLM",
|
|
68
|
-
"Gemini",
|
|
69
|
-
|
|
70
|
-
# Tools
|
|
71
|
-
"Tool",
|
|
72
|
-
"ToolExecutionError",
|
|
73
|
-
"flo_tool",
|
|
74
|
-
"create_tool_from_function",
|
|
75
|
-
|
|
76
|
-
# Arium
|
|
77
|
-
"Arium",
|
|
78
|
-
"BaseArium",
|
|
79
|
-
"AriumBuilder",
|
|
80
|
-
"create_arium",
|
|
81
|
-
"MessageMemory",
|
|
82
|
-
"BaseMemory",
|
|
83
|
-
"StartNode",
|
|
84
|
-
"EndNode",
|
|
85
|
-
"Edge"
|
|
86
|
-
|
|
87
|
-
# Builder
|
|
88
|
-
"AgentBuilder",
|
|
89
|
-
"AriumBuilder"
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
__version__ = "1.0.0"
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from flo_ai.arium.base import BaseArium
|
|
2
|
-
from flo_ai.arium.memory import MessageMemory, BaseMemory
|
|
3
|
-
from flo_ai.llm.base_llm import ImageMessage
|
|
4
|
-
from typing import List
|
|
5
|
-
from flo_ai.models.agent import Agent
|
|
6
|
-
from flo_ai.tool.base_tool import Tool
|
|
7
|
-
from flo_ai.arium.models import StartNode, EndNode
|
|
8
|
-
from flo_ai.utils.logger import logger
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Arium(BaseArium):
|
|
12
|
-
def __init__(self, memory: BaseMemory):
|
|
13
|
-
super().__init__()
|
|
14
|
-
self.is_compiled = False
|
|
15
|
-
self.memory = memory if memory else MessageMemory()
|
|
16
|
-
|
|
17
|
-
def compile(self):
|
|
18
|
-
self.validate_graph()
|
|
19
|
-
self.is_compiled = True
|
|
20
|
-
|
|
21
|
-
async def run(self, inputs: List[str | ImageMessage]):
|
|
22
|
-
if not self.is_compiled:
|
|
23
|
-
raise ValueError('Arium is not compiled')
|
|
24
|
-
|
|
25
|
-
if not self.memory:
|
|
26
|
-
raise ValueError('Arium has no memory')
|
|
27
|
-
|
|
28
|
-
if not self.nodes:
|
|
29
|
-
raise ValueError('Arium has no nodes')
|
|
30
|
-
|
|
31
|
-
return await self._execute_graph(inputs)
|
|
32
|
-
|
|
33
|
-
async def _execute_graph(self, inputs: List[str | ImageMessage]):
|
|
34
|
-
[self.memory.add(msg) for msg in inputs]
|
|
35
|
-
|
|
36
|
-
current_node = self.nodes[self.start_node_name]
|
|
37
|
-
current_edge = self.edges[self.start_node_name]
|
|
38
|
-
|
|
39
|
-
logger.info(f'Executing graph from {current_node.name}')
|
|
40
|
-
while current_node.name != self.end_node_name:
|
|
41
|
-
# execute current node
|
|
42
|
-
result = await self._execute_node(current_node)
|
|
43
|
-
|
|
44
|
-
# update results to memory
|
|
45
|
-
self._add_to_memory(result)
|
|
46
|
-
|
|
47
|
-
# find next node post current node
|
|
48
|
-
next_node_name = current_edge.router_fn(memory=self.memory)
|
|
49
|
-
|
|
50
|
-
# find next edge
|
|
51
|
-
# TODO: next_node_name might not be in self.edges if it's the end node. Handle this case
|
|
52
|
-
next_edge = (
|
|
53
|
-
self.edges[next_node_name] if next_node_name in self.edges else None
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# update current node
|
|
57
|
-
current_node = self.nodes[next_node_name]
|
|
58
|
-
current_edge = next_edge
|
|
59
|
-
|
|
60
|
-
return self.memory.get()
|
|
61
|
-
|
|
62
|
-
async def _execute_node(self, node: Agent | Tool | StartNode | EndNode):
|
|
63
|
-
if isinstance(node, Agent):
|
|
64
|
-
return await node.run(self.memory.get())
|
|
65
|
-
elif isinstance(node, Tool):
|
|
66
|
-
return await node.execute(self.memory.get())
|
|
67
|
-
elif isinstance(node, StartNode):
|
|
68
|
-
return None
|
|
69
|
-
elif isinstance(node, EndNode):
|
|
70
|
-
return None
|
|
71
|
-
|
|
72
|
-
def _add_to_memory(self, result: str):
|
|
73
|
-
# TODO result will be None for start and end nodes
|
|
74
|
-
if result:
|
|
75
|
-
self.memory.add(result)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|