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 +101 -0
- flo_ai-0.0.1/README.md +71 -0
- flo_ai-0.0.1/flo_ai/__init__.py +5 -0
- flo_ai-0.0.1/flo_ai/builders/__init__.py +0 -0
- flo_ai-0.0.1/flo_ai/builders/yaml_builder.py +69 -0
- flo_ai-0.0.1/flo_ai/constants/__init__.py +0 -0
- flo_ai-0.0.1/flo_ai/constants/prompt_constants.py +1 -0
- flo_ai-0.0.1/flo_ai/core.py +40 -0
- flo_ai-0.0.1/flo_ai/helpers/utils.py +10 -0
- flo_ai-0.0.1/flo_ai/models/__init__.py +0 -0
- flo_ai-0.0.1/flo_ai/models/exception.py +7 -0
- flo_ai-0.0.1/flo_ai/models/flo_agent.py +57 -0
- flo_ai-0.0.1/flo_ai/models/flo_executable.py +22 -0
- flo_ai-0.0.1/flo_ai/models/flo_member.py +4 -0
- flo_ai-0.0.1/flo_ai/models/flo_planner.py +157 -0
- flo_ai-0.0.1/flo_ai/models/flo_rag.py +80 -0
- flo_ai-0.0.1/flo_ai/models/flo_supervisor.py +97 -0
- flo_ai-0.0.1/flo_ai/models/flo_team.py +126 -0
- flo_ai-0.0.1/flo_ai/retrievers/__init__.py +0 -0
- flo_ai-0.0.1/flo_ai/retrievers/chromadb_retriever.py +31 -0
- flo_ai-0.0.1/flo_ai/retrievers/flo_retriever.py +6 -0
- flo_ai-0.0.1/flo_ai/retrievers/mongodb_retriver.py +25 -0
- flo_ai-0.0.1/flo_ai/state/__init__.py +0 -0
- flo_ai-0.0.1/flo_ai/state/flo_session.py +38 -0
- flo_ai-0.0.1/flo_ai/state/flo_state.py +19 -0
- flo_ai-0.0.1/flo_ai/tools/__init__.py +0 -0
- flo_ai-0.0.1/flo_ai/tools/big_query_tool.py +43 -0
- flo_ai-0.0.1/flo_ai/tools/retriever_tool.py +14 -0
- flo_ai-0.0.1/flo_ai/yaml/flo_team_builder.py +86 -0
- flo_ai-0.0.1/pyproject.toml +36 -0
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
|
+
```
|
|
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,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,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,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"
|