flo-ai 1.0.0.dev3__tar.gz → 1.0.0.dev5__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 (34) hide show
  1. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/PKG-INFO +1 -1
  2. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/__init__.py +3 -1
  3. flo_ai-1.0.0.dev5/flo_ai/arium/arium.py +164 -0
  4. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/builder.py +8 -4
  5. flo_ai-1.0.0.dev5/flo_ai/llm/__init__.py +16 -0
  6. flo_ai-1.0.0.dev5/flo_ai/llm/openai_vllm.py +58 -0
  7. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/models/agent.py +97 -27
  8. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/models/base_agent.py +1 -0
  9. flo_ai-1.0.0.dev5/flo_ai/utils/variable_extractor.py +157 -0
  10. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/pyproject.toml +1 -1
  11. flo_ai-1.0.0.dev3/flo_ai/arium/arium.py +0 -75
  12. flo_ai-1.0.0.dev3/flo_ai/llm/__init__.py +0 -7
  13. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/README.md +0 -0
  14. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/README.md +0 -0
  15. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/__init__.py +0 -0
  16. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/base.py +0 -0
  17. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/examples.py +0 -0
  18. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/memory.py +0 -0
  19. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/arium/models.py +0 -0
  20. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/builder/agent_builder.py +0 -0
  21. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/formatter/yaml_format_parser.py +0 -0
  22. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/llm/anthropic_llm.py +0 -0
  23. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/llm/base_llm.py +0 -0
  24. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/llm/gemini_llm.py +0 -0
  25. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/llm/ollama_llm.py +0 -0
  26. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/llm/openai_llm.py +0 -0
  27. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/models/__init__.py +0 -0
  28. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/models/agent_error.py +0 -0
  29. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/tool/__init__.py +0 -0
  30. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/tool/base_tool.py +0 -0
  31. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/tool/flo_tool.py +0 -0
  32. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/utils/__init__.py +0 -0
  33. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/flo_ai/utils/flo_utils.py +0 -0
  34. {flo_ai-1.0.0.dev3 → flo_ai-1.0.0.dev5}/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.dev3
3
+ Version: 1.0.0.dev5
4
4
  Summary: A easy way to create structured AI agents
5
5
  License: MIT
6
6
  Author: rootflo
@@ -8,7 +8,7 @@ from .models import Agent, AgentError, BaseAgent, AgentType, ReasoningPattern
8
8
  from .builder.agent_builder import AgentBuilder
9
9
 
10
10
  # LLM package - Language model integrations
11
- from .llm import BaseLLM, Anthropic, OpenAI, OllamaLLM, Gemini
11
+ from .llm import BaseLLM, Anthropic, OpenAI, OllamaLLM, Gemini, ImageMessage
12
12
 
13
13
  # Tool package - Tool framework components
14
14
  from .tool import Tool, ToolExecutionError, flo_tool, create_tool_from_function
@@ -43,6 +43,8 @@ __all__ = [
43
43
  'OpenAI',
44
44
  'OllamaLLM',
45
45
  'Gemini',
46
+ # LLM DataClass
47
+ 'ImageMessage',
46
48
  # Tools
47
49
  'Tool',
48
50
  'ToolExecutionError',
@@ -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'
@@ -0,0 +1,16 @@
1
+ from .base_llm import BaseLLM, ImageMessage
2
+ from .anthropic_llm import Anthropic
3
+ from .openai_llm import OpenAI
4
+ from .ollama_llm import OllamaLLM
5
+ from .gemini_llm import Gemini
6
+ from .openai_vllm import OpenAIVLLM
7
+
8
+ __all__ = [
9
+ 'BaseLLM',
10
+ 'Anthropic',
11
+ 'OpenAI',
12
+ 'OllamaLLM',
13
+ 'Gemini',
14
+ 'OpenAIVLLM',
15
+ 'ImageMessage',
16
+ ]
@@ -0,0 +1,58 @@
1
+ from typing import Any
2
+ from .openai_llm import OpenAI
3
+
4
+
5
+ class OpenAIVLLM(OpenAI):
6
+ def __init__(
7
+ self,
8
+ base_url: str,
9
+ model: str,
10
+ api_key: str = None,
11
+ temperature: float = 0.7,
12
+ **kwargs,
13
+ ):
14
+ super().__init__(
15
+ model=model,
16
+ api_key=api_key,
17
+ temperature=temperature,
18
+ base_url=base_url,
19
+ **kwargs,
20
+ )
21
+
22
+ # overriden
23
+ async def generate(
24
+ self, messages: list[dict], output_schema: dict = None, **kwargs
25
+ ) -> Any:
26
+ # Convert output_schema to OpenAI format if provided
27
+ if output_schema:
28
+ kwargs['extra_body'] = {'guided_json': output_schema.get('schema')}
29
+
30
+ # Add JSON format instruction to the system prompt
31
+ if messages and messages[0]['role'] == 'system':
32
+ messages[0]['content'] = (
33
+ messages[0]['content']
34
+ + '\n\nPlease provide your response in JSON format according to the specified schema.'
35
+ )
36
+ else:
37
+ messages.insert(
38
+ 0,
39
+ {
40
+ 'role': 'system',
41
+ 'content': 'Please provide your response in JSON format according to the specified schema.',
42
+ },
43
+ )
44
+
45
+ # Prepare OpenAI API parameters
46
+ vllm_openai_kwargs = {
47
+ 'model': self.model,
48
+ 'messages': messages,
49
+ **kwargs,
50
+ **self.kwargs,
51
+ }
52
+
53
+ # Make the API call
54
+ response = await self.client.chat.completions.create(**vllm_openai_kwargs)
55
+ message = response.choices[0].message
56
+
57
+ # Return the full message object instead of just the content
58
+ return message
@@ -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"""
64
- while retry_count < self.max_retries:
107
+ variables = variables or {}
108
+
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
 
@@ -100,7 +150,7 @@ class Agent(BaseAgent):
100
150
 
101
151
  should_retry, analysis = await self.handle_error(e, context)
102
152
 
103
- if should_retry and retry_count < self.max_retries:
153
+ if should_retry and retry_count <= self.max_retries:
104
154
  self.add_to_history(
105
155
  'system', f'Error occurred. Analysis: {analysis}'
106
156
  )
@@ -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"""
116
- while retry_count < self.max_retries:
168
+ variables = variables or {}
169
+
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
 
@@ -190,7 +248,7 @@ class Agent(BaseAgent):
190
248
  'attempt': retry_count,
191
249
  }
192
250
  should_retry, analysis = await self.handle_error(e, context)
193
- if should_retry and retry_count < self.max_retries:
251
+ if should_retry and retry_count <= self.max_retries:
194
252
  self.add_to_history(
195
253
  'system', f'Tool execution error: {analysis}'
196
254
  )
@@ -226,7 +284,7 @@ class Agent(BaseAgent):
226
284
  }
227
285
 
228
286
  should_retry, analysis = await self.handle_error(e, context)
229
- if should_retry and retry_count < self.max_retries:
287
+ if should_retry and retry_count <= self.max_retries:
230
288
  self.add_to_history(
231
289
  'system', f'Error occurred. Analysis: {analysis}'
232
290
  )
@@ -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
@@ -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-dev3"
3
+ version = "1.0.0-dev5"
4
4
  description = "A easy way to create structured AI agents"
5
5
  authors = ["rootflo <*@rootflo.ai>"]
6
6
  license = "MIT"
@@ -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)
@@ -1,7 +0,0 @@
1
- from .base_llm import BaseLLM
2
- from .anthropic_llm import Anthropic
3
- from .openai_llm import OpenAI
4
- from .ollama_llm import OllamaLLM
5
- from .gemini_llm import Gemini
6
-
7
- __all__ = ['BaseLLM', 'Anthropic', 'OpenAI', 'OllamaLLM', 'Gemini']
File without changes