flo-ai 0.0.1__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-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.1
2
+ Name: flo-ai
3
+ Version: 0.0.1
4
+ Summary: A easy way to create structured AI agents
5
+ License: MIT
6
+ Author: vizsatiz
7
+ Author-email: vishnu@rootfor.xyz
8
+ Requires-Python: >=3.10,<3.13
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Dist: google-cloud-bigquery (>=3.23.1,<4.0.0)
15
+ Requires-Dist: httpx (==0.27.0)
16
+ Requires-Dist: ipython (==8.24.0)
17
+ Requires-Dist: langchain (==0.2.1)
18
+ Requires-Dist: langchain-chroma (>=0.1.1,<0.2.0)
19
+ Requires-Dist: langchain-community (==0.2.0)
20
+ Requires-Dist: langchain-experimental (==0.0.59)
21
+ Requires-Dist: langchain-mistralai (>=0.1.7,<0.2.0)
22
+ Requires-Dist: langchain-mongodb (>=0.1.5,<0.2.0)
23
+ Requires-Dist: langchain-openai (>=0.1.8,<0.2.0)
24
+ Requires-Dist: langgraph (==0.0.55)
25
+ Requires-Dist: openai (==1.30.5)
26
+ Requires-Dist: pillow (>=10.3.0,<11.0.0)
27
+ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Flo: Composable AI Agents
31
+
32
+ Flo gives you a composable framework for creating agentic AI architectures. What we intent to do here is to create an easy framework for AI app developers to compose apps using pre-implemented architectural components, at the same time providing the flexibilty to create their own components.
33
+
34
+ ### What is composibility ?
35
+
36
+ Composility is the ability to use smaller components to build bigger apps. In a composible architecture, you will be given smaller building blocks which you can use to build a bigger applications. It is every similar to how legos work, you are given smaller lego blocks which when put together creates a whole structure.
37
+
38
+ ### What are the building blocks in Flo ?
39
+
40
+ In flo, we tried to put togther a system where we have small micro components like vector store, or simple LLM prompts, and then higher components/arhictectures made of these mico components like RAGs, Agentic Teams etc.
41
+
42
+ # Flo vs langraph/langchain
43
+
44
+ Flo is built with langraph under the stood. So everything that works in langraph still works here, including all tools and architectures. The following makes flo easy to implement:
45
+
46
+ 1. Langraph needs good understanding of underlying graph and states, its more of a raw tool and asks developers to implement the required artitectures. While flo is more usecase friendly and components can be easily used by using the flo classes, which has lot of internal abstraction for ease of use.
47
+
48
+ 2. In every AI component that has gone in production their are lot of nuances that needs to be implemented to get production quality output, flo inherently implements the best architecutures and gives then out of the box, you can enable and disable as you wish. This not only reduces the complexity but also improves the time to iterate solutions
49
+
50
+ 3. Every component in flo is combosable meaning you can easily put them together and flo takes care of routing between the components where as in langraph the developer has to tie these up.
51
+
52
+ # How to use
53
+
54
+ Flo supports two ways to set up and run the components, first is through code. This is much more flexible. This can help you write your own tools and add them to the flo.
55
+
56
+ Second way it to use yaml. You can write an yaml to define your agentic workflow or RAG and it compiles into a application. See examples below
57
+
58
+ ## Building your first agent by structured yaml
59
+ To create a small team of researcher + blogger for writing blogs
60
+
61
+ ```python
62
+ # This yaml defines a team of 2 agents + 1 supervisor
63
+ yaml_data = """
64
+ apiVersion: flo/alpha-v1
65
+ kind: FloRoutedTeam
66
+ name: blogging-team
67
+ team:
68
+ name: BloggingTeam
69
+ router:
70
+ name: TeamLead
71
+ kind: supervisor
72
+ agents:
73
+ - name: Researcher
74
+ prompt: Do a research on the internet and find articles of relevent to the topic asked by the user, always try to find the latest information on the same
75
+ tools:
76
+ - name: TavilySearchResults
77
+ - name: Blogger
78
+ prompt: From the documents provider by the researcher write a blog of 300 words with can be readily published, make in engaging and add reference links to original blogs
79
+ tools:
80
+ - name: TavilySearchResults
81
+ """
82
+
83
+ input_prompt = """
84
+ Question: Write me an interesting blog about latest advancements in agentic AI
85
+ """
86
+
87
+ llm = ChatOpenAI(temperature=0, model_name='gpt-4o')
88
+
89
+ # Register all the tools at some place and use everywhere in the yaml
90
+ session = FloSession(llm).register_tool(
91
+ name="TavilySearchResults",
92
+ tool=TavilySearchResults()
93
+ )
94
+
95
+ # Build the final flow and run it
96
+ flo: Flo = Flo.build_with_yaml(session, yaml=yaml_data)
97
+
98
+ # call invoke or stream
99
+ for s in flo.stream(input_prompt)
100
+ ```
101
+
flo_ai-0.0.1/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Flo: Composable AI Agents
2
+
3
+ Flo gives you a composable framework for creating agentic AI architectures. What we intent to do here is to create an easy framework for AI app developers to compose apps using pre-implemented architectural components, at the same time providing the flexibilty to create their own components.
4
+
5
+ ### What is composibility ?
6
+
7
+ Composility is the ability to use smaller components to build bigger apps. In a composible architecture, you will be given smaller building blocks which you can use to build a bigger applications. It is every similar to how legos work, you are given smaller lego blocks which when put together creates a whole structure.
8
+
9
+ ### What are the building blocks in Flo ?
10
+
11
+ In flo, we tried to put togther a system where we have small micro components like vector store, or simple LLM prompts, and then higher components/arhictectures made of these mico components like RAGs, Agentic Teams etc.
12
+
13
+ # Flo vs langraph/langchain
14
+
15
+ Flo is built with langraph under the stood. So everything that works in langraph still works here, including all tools and architectures. The following makes flo easy to implement:
16
+
17
+ 1. Langraph needs good understanding of underlying graph and states, its more of a raw tool and asks developers to implement the required artitectures. While flo is more usecase friendly and components can be easily used by using the flo classes, which has lot of internal abstraction for ease of use.
18
+
19
+ 2. In every AI component that has gone in production their are lot of nuances that needs to be implemented to get production quality output, flo inherently implements the best architecutures and gives then out of the box, you can enable and disable as you wish. This not only reduces the complexity but also improves the time to iterate solutions
20
+
21
+ 3. Every component in flo is combosable meaning you can easily put them together and flo takes care of routing between the components where as in langraph the developer has to tie these up.
22
+
23
+ # How to use
24
+
25
+ Flo supports two ways to set up and run the components, first is through code. This is much more flexible. This can help you write your own tools and add them to the flo.
26
+
27
+ Second way it to use yaml. You can write an yaml to define your agentic workflow or RAG and it compiles into a application. See examples below
28
+
29
+ ## Building your first agent by structured yaml
30
+ To create a small team of researcher + blogger for writing blogs
31
+
32
+ ```python
33
+ # This yaml defines a team of 2 agents + 1 supervisor
34
+ yaml_data = """
35
+ apiVersion: flo/alpha-v1
36
+ kind: FloRoutedTeam
37
+ name: blogging-team
38
+ team:
39
+ name: BloggingTeam
40
+ router:
41
+ name: TeamLead
42
+ kind: supervisor
43
+ agents:
44
+ - name: Researcher
45
+ prompt: Do a research on the internet and find articles of relevent to the topic asked by the user, always try to find the latest information on the same
46
+ tools:
47
+ - name: TavilySearchResults
48
+ - name: Blogger
49
+ prompt: From the documents provider by the researcher write a blog of 300 words with can be readily published, make in engaging and add reference links to original blogs
50
+ tools:
51
+ - name: TavilySearchResults
52
+ """
53
+
54
+ input_prompt = """
55
+ Question: Write me an interesting blog about latest advancements in agentic AI
56
+ """
57
+
58
+ llm = ChatOpenAI(temperature=0, model_name='gpt-4o')
59
+
60
+ # Register all the tools at some place and use everywhere in the yaml
61
+ session = FloSession(llm).register_tool(
62
+ name="TavilySearchResults",
63
+ tool=TavilySearchResults()
64
+ )
65
+
66
+ # Build the final flow and run it
67
+ flo: Flo = Flo.build_with_yaml(session, yaml=yaml_data)
68
+
69
+ # call invoke or stream
70
+ for s in flo.stream(input_prompt)
71
+ ```
@@ -0,0 +1,5 @@
1
+ from flo.core import Flo
2
+ from flo.models.flo_agent import FloAgentBuilder
3
+ from flo.models.flo_supervisor import FloSupervisorBuilder
4
+ from flo.models.flo_team import FloTeamBuilder
5
+ from flo.state.flo_session import FloSession
File without changes
@@ -0,0 +1,69 @@
1
+ from flo.models.flo_team import FloTeamBuilder
2
+ from flo.models.flo_agent import FloAgentBuilder, FloAgent
3
+ from flo.models.flo_supervisor import FloSupervisorBuilder
4
+ from flo.yaml.flo_team_builder import (FloRoutedTeamConfig, TeamConfig,
5
+ AgentConfig, FloAgentConfig)
6
+ from flo.models.flo_executable import ExecutableFlo
7
+ from flo.models.flo_planner import FloPlannerBuilder
8
+ from flo.state.flo_session import FloSession
9
+ from typing import Union, List
10
+
11
+ def build_supervised_team(
12
+ session: FloSession,
13
+ flo_config: Union[FloRoutedTeamConfig, FloAgentConfig]) -> ExecutableFlo:
14
+ if isinstance(flo_config, FloRoutedTeamConfig):
15
+ team_config: TeamConfig = flo_config.team
16
+ team = parse_and_build_subteams(session, team_config, session.tools)
17
+ return team
18
+ elif isinstance(flo_config, FloAgentConfig):
19
+ agent_config: AgentConfig = flo_config.agent
20
+ agent = create_agent(session, agent_config, session.tools)
21
+ return agent
22
+
23
+ def parse_and_build_subteams(
24
+ session: FloSession,
25
+ team: TeamConfig,
26
+ tool_map) -> ExecutableFlo:
27
+ flo_team = None
28
+ if team.agents:
29
+ agents = []
30
+ for agent in team.agents:
31
+ flo_agent: FloAgent = create_agent(session, agent, tool_map)
32
+ agents.append(flo_agent)
33
+ router = create_router(session, team, agents)
34
+ flo_team = FloTeamBuilder(
35
+ session=session,
36
+ name=team.name,
37
+ supervisor=router
38
+ ).build()
39
+ if team.planner is not None:
40
+ return FloPlannerBuilder(session, team.planner.name, flo_team).build()
41
+ else:
42
+ flo_teams = []
43
+ for subteam in team.subteams:
44
+ flo_subteam = parse_and_build_subteams(session, subteam, tool_map)
45
+ flo_teams.append(flo_subteam)
46
+ router = create_router(session, team, agents)
47
+ flo_team = FloTeamBuilder(
48
+ session=session,
49
+ name=team.name,
50
+ supervisor=router
51
+ ).build()
52
+ return flo_team
53
+
54
+ def create_router(session: FloSession, team_config: TeamConfig, agents: List[FloAgent]):
55
+ if (team_config.router.kind == "supervisor"):
56
+ return FloSupervisorBuilder(session, team_config.router.name, agents).build()
57
+ else:
58
+ raise Exception("Unknown router type")
59
+
60
+ def create_agent(session: FloSession, agent: AgentConfig, tool_map) -> FloAgent:
61
+ tools = [tool_map[tool.name] for tool in agent.tools]
62
+ flo_agent: FloAgent = FloAgentBuilder(
63
+ session,
64
+ agent.name,
65
+ agent.prompt,
66
+ tools
67
+ ).build()
68
+ return flo_agent
69
+
File without changes
@@ -0,0 +1 @@
1
+ FLO_FINISH = "FINISH"
@@ -0,0 +1,40 @@
1
+ from flo.yaml.flo_team_builder import to_supervised_team
2
+ from flo.builders.yaml_builder import build_supervised_team, FloRoutedTeamConfig
3
+ from typing import (
4
+ Any,
5
+ Iterator,
6
+ Union
7
+ )
8
+ from flo.state.flo_session import FloSession
9
+ from flo.models.flo_executable import ExecutableFlo
10
+
11
+ class Flo:
12
+
13
+ def __init__(self,
14
+ session: FloSession,
15
+ config: FloRoutedTeamConfig) -> None:
16
+ self.config = config
17
+ self.runnable: ExecutableFlo = build_supervised_team(session, config)
18
+
19
+ def stream(self, query, config = None) -> Iterator[Union[dict[str, Any], Any]]:
20
+ return self.runnable.stream(query, config)
21
+
22
+ def invoke(self, query, config = None) -> Iterator[Union[dict[str, Any], Any]]:
23
+ return self.runnable.invoke(query, config)
24
+
25
+ @staticmethod
26
+ def build_with_yaml(session: FloSession, yaml: str):
27
+ return Flo(session, to_supervised_team(yaml))
28
+
29
+ def draw(self, xray=True):
30
+ return self.runnable.draw(xray)
31
+
32
+ def draw_to_file(self, filename: str, xray=True):
33
+ from PIL import Image as PILImage
34
+ import io
35
+ byte_image = self.runnable.draw(xray)
36
+ with io.BytesIO(byte_image) as image_io:
37
+ image = PILImage.open(image_io)
38
+ image.save(filename)
39
+
40
+
@@ -0,0 +1,10 @@
1
+ import random
2
+ import string
3
+
4
+ def random_str(length: int = 5):
5
+ letters = string.ascii_letters + string.digits
6
+ result_str = ''.join(random.choice(letters) for i in range(length))
7
+ return result_str
8
+
9
+ def randomize_name(name: str):
10
+ return "{}-{}".format(name, random_str(5))
File without changes
@@ -0,0 +1,7 @@
1
+ class FloValidationException(Exception):
2
+ def __init__(self, *args: object) -> None:
3
+ super().__init__(*args)
4
+
5
+ class FloIllegalStateException(Exception):
6
+ def __init__(self, *args: object) -> None:
7
+ super().__init__(*args)
@@ -0,0 +1,57 @@
1
+ from langchain_core.tools import BaseTool
2
+ from langchain.agents import AgentExecutor
3
+ from langchain.agents import create_openai_tools_agent
4
+ from langchain_core.runnables import Runnable
5
+ from langchain_core.language_models import BaseLanguageModel
6
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
7
+ from flo.helpers.utils import randomize_name
8
+ from flo.models.flo_executable import ExecutableFlo
9
+ from flo.state.flo_session import FloSession
10
+ from typing import Union
11
+
12
+ class FloAgent(ExecutableFlo):
13
+ def __init__(self,
14
+ agent: Runnable,
15
+ executor: AgentExecutor,
16
+ name: str) -> None:
17
+ super().__init__(name, executor, "agent")
18
+ self.agent: Runnable = agent,
19
+ self.executor: AgentExecutor = executor
20
+
21
+ class FloAgentBuilder:
22
+ def __init__(self,
23
+ session: FloSession,
24
+ name: str,
25
+ prompt: Union[ChatPromptTemplate, str],
26
+ tools: list[BaseTool],
27
+ verbose: bool = True,
28
+ llm: Union[BaseLanguageModel, None] = None,
29
+ return_intermediate_steps: bool = False,
30
+ handle_parsing_errors: bool = True) -> None:
31
+ self.name: str = randomize_name(name)
32
+ self.llm = llm if llm is not None else session.llm
33
+ # TODO improve to add more context of what other agents are available
34
+ self.prompt: ChatPromptTemplate = ChatPromptTemplate.from_messages(
35
+ [
36
+ (
37
+ "system",
38
+ prompt,
39
+ ),
40
+ MessagesPlaceholder(variable_name="messages"),
41
+ MessagesPlaceholder(variable_name="agent_scratchpad")
42
+ ]
43
+ ) if isinstance(prompt, str) else prompt
44
+ self.tools: list[BaseTool] = tools
45
+ self.verbose = verbose
46
+ self.return_intermediate_steps = return_intermediate_steps
47
+ self.handle_parsing_errors = handle_parsing_errors
48
+
49
+
50
+ def build(self) -> AgentExecutor:
51
+ agent = create_openai_tools_agent(self.llm, self.tools, self.prompt)
52
+ executor = AgentExecutor(agent=agent,
53
+ tools=self.tools,
54
+ verbose=self.verbose,
55
+ return_intermediate_steps=True,
56
+ handle_parsing_errors=True)
57
+ return FloAgent(agent, executor, self.name)
@@ -0,0 +1,22 @@
1
+ from flo.models.flo_member import FloMember
2
+ from langgraph.graph.graph import CompiledGraph
3
+ from langchain_core.messages import HumanMessage
4
+
5
+ class ExecutableFlo(FloMember):
6
+ def __init__(self, name: str, graph: CompiledGraph, type: str = "team") -> None:
7
+ super().__init__(name, type)
8
+ self.graph = graph
9
+
10
+ def stream(self, work, config = None):
11
+ return self.graph.stream({
12
+ "messages": [
13
+ HumanMessage(content=work)
14
+ ]
15
+ }, config)
16
+
17
+ def invoke(self, work):
18
+ return self.graph.invoke({
19
+ "messages": [
20
+ HumanMessage(content=work)
21
+ ]
22
+ })
@@ -0,0 +1,4 @@
1
+ class FloMember():
2
+ def __init__(self, name: str, type: str) -> None:
3
+ self.name = name
4
+ self.type = type
@@ -0,0 +1,157 @@
1
+ from langchain_core.language_models import BaseLanguageModel
2
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
3
+ from flo.models.flo_executable import ExecutableFlo
4
+ from flo.state.flo_state import TeamFloAgentStateWithPlan
5
+ from langchain_core.pydantic_v1 import BaseModel, Field
6
+ from typing import Union
7
+ from langgraph.graph.graph import CompiledGraph
8
+ from flo.models.flo_team import FloTeam
9
+ from langgraph.graph import StateGraph
10
+ from flo.state.flo_session import FloSession
11
+ from flo.helpers.utils import randomize_name
12
+
13
+
14
+ class Response(BaseModel):
15
+ """Response to user."""
16
+ response: str
17
+
18
+ class Plan(BaseModel):
19
+ """Plan to follow in future"""
20
+
21
+ steps: list[str] = Field(
22
+ description="different steps to follow, should be in sorted order"
23
+ )
24
+
25
+ class Act(BaseModel):
26
+ """Action to perform."""
27
+ action: Union[Response, Plan] = Field(
28
+ description="Action to perform. If you want to respond to user, use Response. "
29
+ "If you need to further use tools to get the answer, use Plan."
30
+ )
31
+
32
+ class FloPlanner(ExecutableFlo):
33
+ def __init__(self, name: str, graph: CompiledGraph) -> None:
34
+ super().__init__(name, graph)
35
+
36
+ def draw(self, xray=True):
37
+ return self.graph.get_graph(xray=xray).draw_mermaid_png()
38
+
39
+ def stream(self, work, config = None):
40
+ return self.graph.stream({
41
+ "input": work
42
+ }, config)
43
+
44
+ def invoke(self, work):
45
+ return self.graph.invoke({
46
+ "input": work
47
+ })
48
+
49
+ class FloPlannerBuilder:
50
+
51
+ def __init__(self,
52
+ session: FloSession,
53
+ name: str,
54
+ team: FloTeam,
55
+ planner_prompt: Union[ChatPromptTemplate, None] = None,
56
+ replanner_prompt: Union[ChatPromptTemplate, None] = None,
57
+ llm: Union[BaseLanguageModel, None] = None) -> None:
58
+ self.planner_name = randomize_name(name)
59
+ self.re_planner_name = randomize_name("{}-{}".format("replan", name))
60
+ self.session = session
61
+ self.llm = llm if llm is not None else session.llm
62
+ self.team = team
63
+ planner_prompt = ChatPromptTemplate.from_messages(
64
+ [
65
+ (
66
+ "system",
67
+ """For the given objective, come up with a simple step by step plan. \
68
+ This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
69
+ The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.""",
70
+ ),
71
+ MessagesPlaceholder(variable_name="messages"),
72
+ ]
73
+ ) if planner_prompt is None else planner_prompt
74
+ self.planner = planner_prompt | self.llm.with_structured_output(Plan)
75
+
76
+ self.replanner_prompt = ChatPromptTemplate.from_template(
77
+ """For the given objective, come up with a simple step by step plan. \
78
+ This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
79
+ The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.
80
+
81
+ Your objective was this:
82
+ {input}
83
+
84
+ Your original plan was this:
85
+ {plan}
86
+
87
+ You have currently done the follow steps:
88
+ {past_steps}
89
+
90
+ Update your plan accordingly. If no more steps are needed and you can return to the user, then do that.
91
+ Otherwise if there are still steps to complete, fill out the plan. Only add steps to the plan that still NEED to be done.
92
+ Do not return previously done steps as part of the plan."""
93
+ ) if replanner_prompt is None else replanner_prompt
94
+ self.replanner = self.replanner_prompt | self.llm.with_structured_output(Act)
95
+
96
+ def execute_step(self, state: TeamFloAgentStateWithPlan):
97
+ plan = state["plan"]
98
+ plan_str = "\n".join(f"{i+1}. {step}" for i, step in enumerate(plan))
99
+ task = plan[0]
100
+ task_formatted = f"""For the following plan:
101
+ {plan_str}\n\nYou are tasked with executing step {1}, {task}."""
102
+ agent_response = self.team.invoke(task_formatted)
103
+ return {
104
+ "past_steps": (task, agent_response["messages"][-1].content),
105
+ }
106
+
107
+ def plan_step(self, state: TeamFloAgentStateWithPlan):
108
+ plan = self.planner.invoke({"messages": [("user", state["input"])]})
109
+ if self.session is not None:
110
+ self.session.append(self.planner_name)
111
+ return {"plan": plan.steps}
112
+
113
+ def replan_step(self, state: TeamFloAgentStateWithPlan):
114
+ output = self.replanner.invoke(state)
115
+ if self.session is not None:
116
+ self.session.append(self.re_planner_name)
117
+ if isinstance(output.action, Response):
118
+ return { "response": output.action.response }
119
+ else:
120
+ return { "plan": output.action.steps }
121
+
122
+ def should_end(self, state: TeamFloAgentStateWithPlan):
123
+ if "response" in state and state["response"]:
124
+ return "__end__"
125
+ else:
126
+ return self.team.name
127
+
128
+ def build(self):
129
+ workflow = StateGraph(TeamFloAgentStateWithPlan)
130
+
131
+ # Add the plan node
132
+ workflow.add_node(self.planner_name, self.plan_step)
133
+
134
+ # Add the execution step
135
+ workflow.add_node(self.team.name, self.execute_step)
136
+
137
+ # Add a replan node
138
+ workflow.add_node(self.re_planner_name, self.replan_step)
139
+
140
+ workflow.set_entry_point(self.planner_name)
141
+
142
+ # From plan we go to agent
143
+ workflow.add_edge(self.planner_name, self.team.name)
144
+
145
+ # From agent, we replan
146
+ workflow.add_edge(self.team.name, self.re_planner_name)
147
+
148
+ workflow.add_conditional_edges(
149
+ self.re_planner_name,
150
+ # Next, we pass in the function that will determine which node is called next.
151
+ self.should_end,
152
+ )
153
+
154
+ # Finally, we compile it!
155
+ # This compiles it into a LangChain Runnable,
156
+ # meaning you can use it as you would any other runnable
157
+ return FloPlanner(self.planner_name, workflow.compile())
@@ -0,0 +1,80 @@
1
+ from typing import Optional
2
+ from langchain_core.language_models import BaseLanguageModel
3
+ from langchain.tools import Tool
4
+ from langgraph.graph import END, StateGraph
5
+ from langgraph.graph.graph import CompiledGraph
6
+ from langgraph.prebuilt import ToolNode
7
+ from flo.state.flo_state import TeamFloAgentState
8
+ from langgraph.prebuilt import tools_condition
9
+ from flo.models.flo_executable import ExecutableFlo
10
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
11
+ from langchain import hub
12
+ from flo.helpers.utils import randomize_name
13
+
14
+ class FloRag(ExecutableFlo):
15
+ def __init__(self,
16
+ name: str,
17
+ graph: CompiledGraph) -> None:
18
+ super().__init__(name, "team")
19
+ self.graph = graph
20
+
21
+ def draw(self, xray=True):
22
+ return self.graph.get_graph(xray=xray).draw_mermaid_png()
23
+
24
+ class FloRagBuilder:
25
+
26
+ def __init__(self,
27
+ name: str,
28
+ tools: list[Tool],
29
+ llm: BaseLanguageModel,
30
+ prompt: Optional[ChatPromptTemplate] = None) -> None:
31
+ self.name = randomize_name(name)
32
+ self.llm = llm
33
+ self.tools = tools
34
+ self.prompt = hub.pull("rlm/rag-prompt") if prompt is None else prompt
35
+
36
+ def retriever_agent(self, state: TeamFloAgentState):
37
+ messages = state["messages"]
38
+ model = self.llm.bind_tools(self.tools)
39
+ response = model.invoke(messages)
40
+ # We return a list, because this will get added to the existing list
41
+ return { "messages": [response] }
42
+
43
+ def generate(self, state: TeamFloAgentState):
44
+ messages = state["messages"]
45
+ question = messages[0].content
46
+ last_message = messages[-1]
47
+
48
+ question = messages[0].content
49
+ docs = last_message.content
50
+
51
+ # Chain
52
+ rag_chain = self.prompt | self.llm
53
+
54
+ # Run
55
+ response = rag_chain.invoke({"context": docs, "question": question})
56
+
57
+ return {"messages": [response] }
58
+
59
+ def build(self) -> FloRag:
60
+ retrieve = ToolNode(self.tools)
61
+
62
+ workflow = StateGraph(TeamFloAgentState)
63
+ workflow.add_node("agent", self.retriever_agent)
64
+ workflow.add_node("retrieve", retrieve) # retrieval
65
+ workflow.add_node("generate", self.generate)
66
+ workflow.add_conditional_edges(
67
+ "agent",
68
+ tools_condition,
69
+ {
70
+ "tools": "retrieve",
71
+ END: END,
72
+ },
73
+ )
74
+
75
+ workflow.add_edge("retrieve", "generate")
76
+ workflow.add_edge("generate", END)
77
+
78
+ workflow.set_entry_point("agent")
79
+ graph = workflow.compile()
80
+ return FloRag(self.name, graph=graph)
@@ -0,0 +1,97 @@
1
+ from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser
2
+ from langchain.chains import LLMChain
3
+ from langchain_core.language_models import BaseLanguageModel
4
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
5
+ from typing import Union
6
+ from flo.models.flo_member import FloMember
7
+ from flo.state.flo_session import FloSession
8
+ from flo.constants.prompt_constants import FLO_FINISH
9
+ from flo.helpers.utils import randomize_name
10
+
11
+
12
+ # TODO, maybe add description about what team members can do
13
+ supervisor_system_message = (
14
+ "You are a supervisor tasked with managing a conversation between the"
15
+ " following {member_type}: {members}. Given the following user request,"
16
+ " respond with the worker to act next. Each worker will perform a"
17
+ " task and respond with their results and status. When the users question is answered or the assigned task is finished,"
18
+ " respond with FINISH. If both your workers has responded atleast once, then strictly return FINISH "
19
+ )
20
+
21
+ class StateUpdateComponent:
22
+ def __init__(self, name: str, session: FloSession) -> None:
23
+ self.name = name
24
+ self.inner_session = session
25
+
26
+ def __call__(self, input):
27
+ self.inner_session.append(self.name)
28
+ return input
29
+
30
+
31
+ class FloSupervisor:
32
+ def __init__(self,
33
+ executor: LLMChain,
34
+ agents: list[FloMember],
35
+ name: str) -> None:
36
+ self.executor = executor
37
+ self.agents = agents
38
+ self.name = name
39
+ self.type = agents[0].type
40
+
41
+ def is_agent_supervisor(self):
42
+ return self.type == "agent"
43
+
44
+ class FloSupervisorBuilder:
45
+ def __init__(self,
46
+ session: FloSession,
47
+ name: str,
48
+ reportees: list[FloMember],
49
+ supervisor_prompt: Union[ChatPromptTemplate, None] = None,
50
+ llm: Union[BaseLanguageModel, None] = None) -> None:
51
+ # TODO add validation for reporteess
52
+ self.name = randomize_name(name)
53
+ self.session = session
54
+ self.llm = llm if llm is not None else session.llm
55
+ self.agents = reportees
56
+ self.members = [agent.name for agent in reportees]
57
+ self.options = self.members + [FLO_FINISH]
58
+ member_type = "workers" if reportees[0].type == "agent" else "team members"
59
+ self.supervisor_prompt = ChatPromptTemplate.from_messages(
60
+ [
61
+ ("system", supervisor_system_message),
62
+ MessagesPlaceholder(variable_name="messages"),
63
+ (
64
+ "system",
65
+ "Given the conversation above, who should act next?"
66
+ " Or should we FINISH if the task is already answered, (do not wait for the entire question to be answered) or there isn't enough information to answer the question, even then return FINISH? Select one of: {options}",
67
+ ),
68
+ ]
69
+ ).partial(options=str(self.options), members=", ".join(self.members), member_type=member_type) if supervisor_prompt is None else supervisor_prompt
70
+
71
+ def build(self):
72
+ function_def = {
73
+ "name": "route",
74
+ "description": "Select the next role.",
75
+ "parameters": {
76
+ "title": "routeSchema",
77
+ "type": "object",
78
+ "properties": {
79
+ "next": {
80
+ "title": "Next",
81
+ "anyOf": [
82
+ {"enum": self.options},
83
+ ],
84
+ }
85
+ },
86
+ "required": ["next"],
87
+ }
88
+ }
89
+
90
+ chain = (
91
+ self.supervisor_prompt
92
+ | self.llm.bind_functions(functions=[function_def], function_call="route")
93
+ | JsonOutputFunctionsParser()
94
+ | StateUpdateComponent(self.name, self.session)
95
+ )
96
+
97
+ return FloSupervisor(chain, self.agents, self.name)
@@ -0,0 +1,126 @@
1
+ import functools
2
+ from langchain_core.messages import HumanMessage
3
+ from langgraph.graph.graph import CompiledGraph
4
+ from langgraph.graph import StateGraph, END
5
+ from langchain.agents import AgentExecutor
6
+
7
+ from flo.models.flo_executable import ExecutableFlo
8
+ from flo.models.flo_agent import FloAgent
9
+ from flo.models.flo_supervisor import FloSupervisor
10
+ from flo.constants.prompt_constants import FLO_FINISH
11
+ from flo.state.flo_state import TeamFloAgentState
12
+ from flo.state.flo_session import FloSession
13
+ from flo.helpers.utils import randomize_name
14
+
15
+ class FloAgentNode:
16
+ def __init__(self,
17
+ agent_node: functools.partial,
18
+ name: str) -> None:
19
+ self.agent_node = agent_node
20
+ self.name = name
21
+
22
+ def teamflo_agent_node(state: TeamFloAgentState, agent: AgentExecutor, name: str):
23
+ result = agent.invoke(state)
24
+ return {"messages": [HumanMessage(content=result["output"], name=name)]}
25
+
26
+ # The following functions interoperate between the top level graph state
27
+ # and the state of the research sub-graph
28
+ # this makes it so that the states of each graph don't get intermixed
29
+ def enter_chain(message: str, members: list[str]):
30
+ results = {
31
+ "messages": [HumanMessage(content=message)],
32
+ "team_members": ", ".join(members),
33
+ }
34
+ return results
35
+
36
+ class FloTeam(ExecutableFlo):
37
+ def __init__(self, name: str, graph: CompiledGraph) -> None:
38
+ super().__init__(name, graph)
39
+
40
+ def draw(self, xray=True):
41
+ return self.graph.get_graph(xray=xray).draw_mermaid_png()
42
+
43
+ class FloTeamChain():
44
+ def __init__(self, name: str, chain) -> None:
45
+ self.chain = chain
46
+ self.name = name
47
+
48
+ class FloTeamBuilder:
49
+ def __init__(self,
50
+ session: FloSession,
51
+ name: str,
52
+ supervisor: FloSupervisor) -> None:
53
+ self.name = randomize_name(name)
54
+ self.session = session
55
+ self.flo_agents = supervisor.agents
56
+ self.flo_supervisor: FloSupervisor = supervisor
57
+ self.members = list(map(lambda x: x.name, self.flo_agents))
58
+
59
+ def build_node(self, flo_agent: FloAgent):
60
+ agent_func = functools.partial(teamflo_agent_node, agent=flo_agent.executor, name=flo_agent.name)
61
+ return FloAgentNode(agent_func, flo_agent.name)
62
+
63
+
64
+ def build(self):
65
+ if self.flo_supervisor.is_agent_supervisor():
66
+ return self.build_agent_supervisor_graph()
67
+ else:
68
+ return self.build_team_supervisor_graph()
69
+
70
+ def router(self, state: TeamFloAgentState):
71
+ next = state["next"]
72
+ conditional_map = {k: k for k in self.members}
73
+ conditional_map[FLO_FINISH] = END
74
+ self.session.append(node=next)
75
+ if self.session.is_looping(node=next):
76
+ return conditional_map[FLO_FINISH]
77
+ return conditional_map[next]
78
+
79
+
80
+ def build_agent_supervisor_graph(self) -> FloTeam:
81
+ flo_agent_nodes = [self.build_node(flo_agent) for flo_agent in self.flo_agents]
82
+ workflow = StateGraph(TeamFloAgentState)
83
+ for flo_agent_node in flo_agent_nodes:
84
+ workflow.add_node(flo_agent_node.name, flo_agent_node.agent_node)
85
+ workflow.add_node(self.flo_supervisor.name, self.flo_supervisor.executor)
86
+ for member in self.members:
87
+ workflow.add_edge(member, self.flo_supervisor.name)
88
+
89
+ workflow.add_conditional_edges(self.flo_supervisor.name, self.router)
90
+
91
+ workflow.set_entry_point(self.flo_supervisor.name)
92
+ workflow_graph = workflow.compile()
93
+ return FloTeam(self.name, workflow_graph)
94
+
95
+ def build_chain_for_teams(self, flo_team: FloTeam):
96
+ # TODO lets see if we can convert to members
97
+ return FloTeamChain(flo_team.name, (
98
+ functools.partial(enter_chain, members=flo_team.graph.nodes)
99
+ | flo_team.graph
100
+ ))
101
+
102
+ def get_last_message(self, state: TeamFloAgentState, second = None) -> str:
103
+ return state["messages"][-1].content
104
+
105
+
106
+ def join_graph(self, response: dict):
107
+ return {"messages": [response["messages"][-1]]}
108
+
109
+ def build_team_supervisor_graph(self) -> FloTeam:
110
+ flo_team_entry_chains = [self.build_chain_for_teams(flo_agent) for flo_agent in self.flo_agents]
111
+ # Define the graph.
112
+ super_graph = StateGraph(TeamFloAgentState)
113
+ # First add the nodes, which will do the work
114
+ for flo_team_chain in flo_team_entry_chains:
115
+ super_graph.add_node(flo_team_chain.name, self.get_last_message | flo_team_chain.chain | self.join_graph)
116
+ super_graph.add_node(self.flo_supervisor.name, self.flo_supervisor.executor)
117
+
118
+ for member in self.members:
119
+ super_graph.add_edge(member, self.flo_supervisor.name)
120
+
121
+ super_graph.add_conditional_edges(self.flo_supervisor.name, self.router)
122
+
123
+ super_graph.set_entry_point(self.flo_supervisor.name)
124
+ super_graph = super_graph.compile()
125
+ return FloTeam(self.name, super_graph)
126
+
File without changes
@@ -0,0 +1,31 @@
1
+ from langchain_community.vectorstores import Chroma
2
+ from langchain_core.embeddings import Embeddings
3
+ from flo.retrievers.flo_retriever import FloRetriever
4
+ from typing import Dict, Optional
5
+
6
+ import chromadb
7
+ from chromadb.config import Settings, DEFAULT_TENANT, DEFAULT_DATABASE
8
+
9
+ class ChromDBRetriever(FloRetriever):
10
+ def __init__(self,
11
+ collection: str,
12
+ embedding: Embeddings,
13
+ type: str = "persistent",
14
+ connection_args: Optional[Dict] = None) -> None:
15
+ if type == "http":
16
+ client = chromadb.HttpClient(**connection_args,
17
+ settings=Settings(),
18
+ tenant=DEFAULT_TENANT,
19
+ database=DEFAULT_DATABASE)
20
+ else:
21
+ client = chromadb.PersistentClient(
22
+ **connection_args,
23
+ settings=Settings(),
24
+ tenant=DEFAULT_TENANT,
25
+ database=DEFAULT_DATABASE)
26
+ chromdb = Chroma(
27
+ client=client,
28
+ collection_name=collection,
29
+ embedding_function=embedding
30
+ )
31
+ super().__init__(chromdb)
@@ -0,0 +1,6 @@
1
+ from langchain_core.vectorstores import VectorStore
2
+
3
+ class FloRetriever():
4
+ def __init__(self, vector_store: VectorStore) -> None:
5
+ self.vector_store = vector_store
6
+ self.retriver = self.vector_store.as_retriever()
@@ -0,0 +1,25 @@
1
+ from langchain_mongodb import MongoDBAtlasVectorSearch
2
+ from langchain_core.embeddings import Embeddings
3
+ from pymongo import MongoClient
4
+
5
+ from flo.retrievers.flo_retriever import FloRetriever
6
+
7
+ class MongoDBRetriever(FloRetriever):
8
+ def __init__(self,
9
+ dburl: str,
10
+ db_name: str,
11
+ collection: str,
12
+ embedding_key: str,
13
+ index_name: str,
14
+ embedding: Embeddings) -> None:
15
+ connection_timeout = 60000
16
+ mongo_client = MongoClient(dburl, connectTimeoutMS=connection_timeout, socketTimeoutMS=connection_timeout)
17
+ mongo_embedding_collection = (mongo_client
18
+ .get_database(db_name)
19
+ .get_collection(collection))
20
+ super().__init__(MongoDBAtlasVectorSearch(
21
+ collection=mongo_embedding_collection,
22
+ embedding_key=embedding_key,
23
+ embedding=embedding,
24
+ index_name=index_name,
25
+ ))
File without changes
@@ -0,0 +1,38 @@
1
+ from langchain_core.language_models import BaseLanguageModel
2
+ from langchain_core.tools import BaseTool
3
+
4
+ class FloSession:
5
+ def __init__(self, llm: BaseLanguageModel, loop_size: int = 2, max_loop: int = 3) -> None:
6
+ self.llm = llm
7
+ self.tools = dict()
8
+ self.counter = dict()
9
+ self.navigation: list[str] = list()
10
+ self.pattern_series = dict()
11
+ self.loop_size: int = loop_size
12
+ self.max_loop: int = max_loop
13
+
14
+ def register_tool(self, name: str, tool: BaseTool):
15
+ self.tools[name] = tool
16
+ return self
17
+
18
+ def append(self, node: str) -> int:
19
+ self.counter[node] = self.counter.get(node, 0) + 1
20
+ if node in self.navigation:
21
+ last_known_index = len(self.navigation) - 1 - self.navigation[::-1].index(node)
22
+ pattern_array = self.navigation[last_known_index: len(self.navigation)]
23
+ if len(pattern_array) + 1 >= self.loop_size:
24
+ pattern = "|".join(pattern_array) + "|" + node
25
+ if node in self.pattern_series:
26
+ self.pattern_series[node].append(pattern)
27
+ else:
28
+ self.pattern_series[node] = [pattern]
29
+ self.navigation.append(node)
30
+
31
+ def is_looping(self, node) -> bool:
32
+ patterns = self.pattern_series[node] if node in self.pattern_series else []
33
+ if len(patterns) < self.max_loop:
34
+ return False
35
+ return patterns[-(self.max_loop):] == [patterns[-1]] * self.max_loop
36
+
37
+ def stringify(self):
38
+ return str(self.counter)
@@ -0,0 +1,19 @@
1
+ from typing import Annotated, List, Sequence, TypedDict
2
+ from langchain_core.messages import BaseMessage
3
+ from typing import List, Tuple, Annotated, TypedDict, Dict
4
+
5
+ import operator
6
+
7
+ # The agent state is the input to each node in the graph
8
+ class TeamFloAgentState(TypedDict):
9
+ # The annotation tells the graph that new messages will always
10
+ # be added to the current states
11
+ messages: Annotated[Sequence[BaseMessage], operator.add]
12
+ # The 'next' field indicates where to route to next
13
+ next: str
14
+
15
+ class TeamFloAgentStateWithPlan(TypedDict):
16
+ input: str
17
+ plan: List[str]
18
+ past_steps: Annotated[List[Tuple], operator.add]
19
+ response: str
File without changes
@@ -0,0 +1,43 @@
1
+ from typing import Type, Optional
2
+ from google.cloud import bigquery
3
+ from langchain.tools import BaseTool
4
+ from langchain_core.tools import CallbackManagerForToolRun, AsyncCallbackManagerForToolRun
5
+ from langchain.pydantic_v1 import Field, BaseModel
6
+
7
+
8
+ class BigQueryToolInput(BaseModel):
9
+ query: str = Field(description="""
10
+ The sql query to be run on the BigQuery dataset as string and nothing else
11
+ """)
12
+
13
+
14
+ class BigQueryExecutorTool(BaseTool):
15
+ name = "BigQueryReaderTool"
16
+ description = "A tool that can run SQL queries on BigQuery Tables, just pass query to be run as input and nothing else"
17
+ args_schema: Type[BaseModel] = BigQueryToolInput
18
+
19
+ def _run(
20
+ self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None
21
+ ) -> str:
22
+ """Use the tool."""
23
+ return self.bigquery_executor(query)
24
+
25
+ async def _arun(
26
+ self, query: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
27
+ ) -> str:
28
+ """Use the tool asynchronously."""
29
+ raise NotImplementedError("custom_search does not support async")
30
+
31
+ def bigquery_executor(self, sql_query_string: str) -> str:
32
+ """Executes a BigQuery SQL query and returns formatted results."""
33
+ try:
34
+ query = sql_query_string.replace("```sql", "").replace("```", "")
35
+ client = bigquery.Client()
36
+ query_job = client.query(query)
37
+ results = query_job.result()
38
+ result_str = "Query Results:\n"
39
+ for row in results:
40
+ result_str += f" - {row}\n"
41
+ return result_str
42
+ except Exception as e:
43
+ return f"Failed to run query: {str(e)}"
@@ -0,0 +1,14 @@
1
+ from langchain.tools.retriever import create_retriever_tool
2
+ from langchain_core.retrievers import BaseRetriever
3
+
4
+ MONGODB_RETRIVER_TOOL = "MongoDBRetreiverTool"
5
+ CHROMADB_RETRIVER_TOOL = "ChromaDBRetreiverTool"
6
+
7
+ def fetch_retriver_tool_by_name(tool_name: str,
8
+ tool_description: str,
9
+ retriever: BaseRetriever):
10
+ return create_retriever_tool(
11
+ retriever,
12
+ tool_name,
13
+ tool_description
14
+ )
@@ -0,0 +1,86 @@
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+ import yaml
4
+ import re
5
+ from typing import Optional
6
+ from flo.models.exception import FloValidationException
7
+
8
+
9
+ KIND_SUPERVISED_TEAM = "FloRoutedTeam"
10
+ KIND_FLO_AGENT = "FloAgent"
11
+
12
+ yaml_kinds = [
13
+ KIND_SUPERVISED_TEAM,
14
+ KIND_FLO_AGENT
15
+ ]
16
+
17
+ class KeyValueArgs(BaseModel):
18
+ name: str
19
+ value: str
20
+
21
+ class FilterArgs(BaseModel):
22
+ name: str
23
+ description: str
24
+ type: str
25
+
26
+ class ToolConfig(BaseModel):
27
+ name: str
28
+ args: Optional[List[KeyValueArgs]] = None
29
+ properties: Optional[List[KeyValueArgs]] = None
30
+ filters: Optional[List[FilterArgs]] = None
31
+
32
+ class AgentConfig(BaseModel):
33
+ name: str
34
+ kind: Optional[str] = None
35
+ prompt: str
36
+ tools: List[ToolConfig] = []
37
+
38
+ class RouterConfig(BaseModel):
39
+ name: str
40
+ kind: str
41
+
42
+ class PlannerConfig(BaseModel):
43
+ name: str
44
+
45
+ class TeamConfig(BaseModel):
46
+ name: str
47
+ agents: Optional[List[AgentConfig]] = None
48
+ subteams: Optional[List['TeamConfig']] = None
49
+ router: Optional[RouterConfig] = None
50
+ planner: Optional[PlannerConfig] = None
51
+
52
+ class FloRoutedTeamConfig(BaseModel):
53
+ apiVersion: str
54
+ kind: str
55
+ name: str
56
+ team: TeamConfig
57
+
58
+ class FloAgentConfig(BaseModel):
59
+ apiVersion: str
60
+ kind: str
61
+ name: str
62
+ agent: AgentConfig
63
+
64
+ def to_supervised_team(yaml_str: str) -> FloRoutedTeamConfig:
65
+ parsed_data = yaml.safe_load(yaml_str)
66
+ kind = parsed_data["kind"]
67
+ if kind == KIND_SUPERVISED_TEAM:
68
+ flo_supervised_team = FloRoutedTeamConfig(**parsed_data)
69
+ validate_sup_team_config(flo_supervised_team)
70
+ return flo_supervised_team
71
+ elif kind == KIND_FLO_AGENT:
72
+ flo_agent = FloAgentConfig(**parsed_data)
73
+ validate_sup_team_config(flo_agent)
74
+ return flo_agent
75
+ else:
76
+ raise FloValidationException("Unknown kind: {}".format(kind))
77
+
78
+ def validate_sup_team_config(flo: FloRoutedTeamConfig):
79
+ if flo.kind == KIND_FLO_AGENT:
80
+ return
81
+ if flo.name is None or not is_valid_name(flo.name):
82
+ raise FloValidationException("Invalid agent name while creating the flow, expected: [^[a-z][a-z0-9_-]*$]")
83
+
84
+ def is_valid_name(s: str) -> bool:
85
+ pattern = r'^[a-z][a-z0-9_-]*$'
86
+ return bool(re.match(pattern, s))
@@ -0,0 +1,36 @@
1
+ [tool.poetry]
2
+ name = "flo-ai"
3
+ version = "0.0.1"
4
+ description = "A easy way to create structured AI agents"
5
+ authors = ["vizsatiz <vishnu@rootfor.xyz>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = ">=3.10, <3.13"
11
+ langchain = "0.2.1"
12
+ langgraph = "0.0.55"
13
+ ipython = "8.24.0"
14
+ httpx = "0.27.0"
15
+ langchain-community = "0.2.0"
16
+ langchain-experimental = "0.0.59"
17
+ google-cloud-bigquery = "^3.23.1"
18
+ openai = "1.30.5"
19
+ langchain-openai = "^0.1.8"
20
+ pillow = "^10.3.0"
21
+ langchain-mongodb = "^0.1.5"
22
+ langchain-mistralai = "^0.1.7"
23
+ langchain-chroma = "^0.1.1"
24
+ python-dotenv = "^1.0.1"
25
+
26
+
27
+ [tool.poetry.group.dev.dependencies]
28
+ langchainhub = "^0.1.17"
29
+ torch = "2.0.0"
30
+ sentence-transformers = "^3.0.0"
31
+ pypdf = "^4.2.0"
32
+ lark = "^1.1.9"
33
+
34
+ [build-system]
35
+ requires = ["poetry-core"]
36
+ build-backend = "poetry.core.masonry.api"