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.
Files changed (33) hide show
  1. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/PKG-INFO +1 -1
  2. flo_ai-1.0.0.dev4/flo_ai/__init__.py +66 -0
  3. flo_ai-1.0.0.dev4/flo_ai/arium/arium.py +164 -0
  4. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/builder.py +8 -4
  5. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/__init__.py +1 -7
  6. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/agent.py +92 -22
  7. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/base_agent.py +1 -0
  8. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/utils/__init__.py +1 -3
  9. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/utils/flo_utils.py +6 -7
  10. flo_ai-1.0.0.dev4/flo_ai/utils/variable_extractor.py +157 -0
  11. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/pyproject.toml +1 -1
  12. flo_ai-1.0.0.dev2/flo_ai/__init__.py +0 -92
  13. flo_ai-1.0.0.dev2/flo_ai/arium/arium.py +0 -75
  14. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/README.md +0 -0
  15. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/README.md +0 -0
  16. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/__init__.py +0 -0
  17. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/base.py +0 -0
  18. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/examples.py +0 -0
  19. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/memory.py +0 -0
  20. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/arium/models.py +0 -0
  21. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/builder/agent_builder.py +0 -0
  22. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/formatter/yaml_format_parser.py +0 -0
  23. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/__init__.py +0 -0
  24. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/anthropic_llm.py +0 -0
  25. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/base_llm.py +0 -0
  26. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/gemini_llm.py +0 -0
  27. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/ollama_llm.py +0 -0
  28. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/llm/openai_llm.py +0 -0
  29. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/models/agent_error.py +0 -0
  30. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/tool/__init__.py +0 -0
  31. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/tool/base_tool.py +0 -0
  32. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/tool/flo_tool.py +0 -0
  33. {flo_ai-1.0.0.dev2 → flo_ai-1.0.0.dev4}/flo_ai/utils/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flo_ai
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.0.dev4
4
4
  Summary: A easy way to create structured AI agents
5
5
  License: MIT
6
6
  Author: rootflo
@@ -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(self, inputs: List[Union[str, ImageMessage]]) -> List[dict]:
132
- """Build the Arium and run it with the given inputs."""
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(self, inputs: List[str | ImageMessage] | str) -> str:
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
- for input in inputs:
48
- if isinstance(input, ImageMessage):
49
- self.add_to_history('user', self.llm.format_image_in_message(input))
50
- else:
51
- self.add_to_history('user', input)
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(self, retry_count: int) -> str:
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': self._get_cot_prompt()
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(self, retry_count: int = 0) -> str:
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': self._get_react_prompt()
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
- react_prompt = f"""{self.system_prompt}
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
- cot_prompt = f"""{self.system_prompt}
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:
@@ -29,6 +29,7 @@ class BaseAgent(ABC):
29
29
  self.agent_type = agent_type
30
30
  self.llm = llm
31
31
  self.max_retries = max_retries
32
+ self.resolved_variables = False
32
33
  self.conversation_history: List[Dict[str, str]] = []
33
34
 
34
35
  @abstractmethod
@@ -1,5 +1,3 @@
1
1
  from .flo_utils import FloUtils
2
2
 
3
- __all__ = [
4
- 'FloUtils'
5
- ]
3
+ __all__ = ['FloUtils']
@@ -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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "flo_ai"
3
- version = "1.0.0-dev2"
3
+ version = "1.0.0-dev4"
4
4
  description = "A easy way to create structured AI agents"
5
5
  authors = ["rootflo <*@rootflo.ai>"]
6
6
  license = "MIT"
@@ -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