Skip to content

Commit edc9b1f

Browse files
authored
Make agents and teams inter-composable, remove openai function usage (#48)
* Fix to use same compose graph for agents & teams * Remove dependency of open-ai function bindings * Fix for support email example
1 parent 3ae5c52 commit edc9b1f

File tree

10 files changed

+96
-198
lines changed

10 files changed

+96
-198
lines changed

examples/email_reply_agent.ipynb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
"source": [
134134
"# import\n",
135135
"from langchain_chroma import Chroma\n",
136+
"from langchain_openai import OpenAIEmbeddings\n",
136137
"from langchain_community.document_loaders import TextLoader\n",
137138
"from langchain_community.embeddings.sentence_transformer import (\n",
138139
" SentenceTransformerEmbeddings,\n",
@@ -148,7 +149,7 @@
148149
"docs = text_splitter.split_documents(documents)\n",
149150
"\n",
150151
"# create the open-source embedding function\n",
151-
"embedding_function = SentenceTransformerEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n",
152+
"embedding_function = OpenAIEmbeddings()\n",
152153
"\n",
153154
"# load it into Chroma\n",
154155
"db = Chroma.from_documents(docs, embedding_function)"

examples/hierarchical_blogging_team.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
name: blogging-team
1313
team:
1414
name: BloggingTeam
15-
supervisor:
16-
name: supervisor
15+
router:
16+
name: parent-supervisor
17+
kind: supervisor
1718
subteams:
18-
- name: BloggingTeam
19-
supervisor:
20-
name: supervisor
19+
- name: BlogResearchTeam
20+
router:
21+
name: bgsupervisor
22+
kind: supervisor
2123
agents:
2224
- name: Reasercher
2325
job: 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
@@ -27,9 +29,10 @@
2729
job: 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
2830
tools:
2931
- name: TavilySearchResults
30-
- name: WritingTeam
31-
supervisor:
32-
name: supervisor
32+
- name: BlogWritingTeam
33+
router:
34+
name: bwsupervisor
35+
kind: supervisor
3336
agents:
3437
- name: Figure
3538
job: Do somethinh
@@ -46,4 +49,4 @@
4649
session = FloSession(llm).register_tool(
4750
name="TavilySearchResults", tool=TavilySearchResults()
4851
)
49-
flo: Flo = Flo.build(session, llm, yaml=yaml_data)
52+
flo: Flo = Flo.build(session, yaml=yaml_data)

flo_ai/builders/yaml_builder.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ def build_supervised_team(session: FloSession) -> ExecutableFlo:
2222

2323
def validate_team(name_set: set, team_config: TeamConfig):
2424
validate_names(name_set, team_config.name)
25-
[validate_names(name_set, agent.name) for agent in team_config.agents]
2625

2726
def parse_and_build_subteams(session: FloSession, team_config: TeamConfig, name_set = set()) -> ExecutableFlo:
2827
flo_team = None

flo_ai/models/flo_executable.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class ExecutableType(Enum):
1010
tool = "tool"
1111
reflection = "reflection"
1212
delegator = "delegator"
13+
team = "team"
14+
router = "router"
1315

1416
@staticmethod
1517
def isAgent(type: 'ExecutableType'):
@@ -26,7 +28,7 @@ class ExecutableFlo(FloMember):
2628
def __init__(self,
2729
name: str,
2830
runnable: Runnable,
29-
type: str = "team") -> None:
31+
type: str = ExecutableType.team) -> None:
3032
super().__init__(name, type)
3133
self.runnable = runnable
3234

flo_ai/models/flo_node.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,22 @@ def build_from_team(self, flo_team: FloRoutedTeam) -> 'FloNode':
3131
return FloNode((
3232
FloNode.Builder.__get_last_message | team_chain | FloNode.Builder.__join_graph
3333
), flo_team.name, flo_team.type, flo_team.config)
34+
35+
def build_from_router(self, flo_router) -> 'FloNode':
36+
router_func = functools.partial(FloNode.Builder.__teamflo_router_node, agent=flo_router.executor, name=flo_router.router_name, agent_config=flo_router.config)
37+
return FloNode(router_func, flo_router.router_name, flo_router.type, flo_router.config)
3438

3539
@staticmethod
3640
def __teamflo_agent_node(state: TeamFloAgentState, agent: AgentExecutor, name: str, agent_config: AgentConfig):
3741
result = agent.invoke(state)
38-
# TODO see how to fix this
3942
output = result if isinstance(result, str) else result["output"]
4043
return { STATE_NAME_MESSAGES: [HumanMessage(content=output, name=name)] }
44+
45+
@staticmethod
46+
def __teamflo_router_node(state: TeamFloAgentState, agent: AgentExecutor, name: str, agent_config: AgentConfig):
47+
result = agent.invoke(state)
48+
nextNode = result if isinstance(result, str) else result["next"]
49+
return { "next": nextNode }
4150

4251
@staticmethod
4352
def __get_last_message(state: TeamFloAgentState) -> str:

flo_ai/router/flo_custom_router.py

Lines changed: 16 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ def router_fn(state: TeamFloAgentState):
3333

3434

3535
function_def = {
36-
"name": "route",
37-
"description": "Select the next role.",
38-
"parameters": {
39-
"title": "routeSchema",
40-
"type": "object",
41-
"properties": {
42-
"next": {
43-
"title": "Next",
44-
"anyOf": [
45-
{"enum": members},
46-
],
47-
}
48-
},
49-
"required": ["next"],
50-
}
36+
"name": "route",
37+
"description": "Select the next role.",
38+
"parameters": {
39+
"title": "routeSchema",
40+
"type": "object",
41+
"properties": {
42+
"next": {
43+
"title": "Next",
44+
"anyOf": [
45+
{"enum": members},
46+
],
47+
}
48+
},
49+
"required": ["next"],
50+
}
5151
}
5252

5353
chain = prompt | self.llm.bind_functions(functions=[function_def], function_call="route") | JsonOutputFunctionsParser()
@@ -60,7 +60,7 @@ def router_fn(state: TeamFloAgentState):
6060

6161
return router_fn
6262

63-
def build_agent_graph(self):
63+
def build_graph(self):
6464
flo_agent_nodes = [self.build_node(flo_agent) for flo_agent in self.members]
6565
workflow = StateGraph(TeamFloAgentState)
6666

@@ -90,36 +90,6 @@ def build_agent_graph(self):
9090

9191
return FloRoutedTeam(self.flo_team.name, workflow_graph, self.flo_team.config)
9292

93-
def build_team_graph(self):
94-
flo_team_entry_chains = [self.build_node_for_teams(flo_agent) for flo_agent in self.members]
95-
96-
super_graph = StateGraph(TeamFloAgentState)
97-
98-
for flo_team_chain in flo_team_entry_chains:
99-
agent_name = flo_team_chain.name
100-
super_graph.add_node(agent_name, flo_team_chain.func)
101-
102-
router_config = self.router_config
103-
super_graph.add_edge(START, router_config.start_node)
104-
for edge_config in router_config.edges:
105-
edge = edge_config.edge
106-
if len(edge) > 2:
107-
teams = edge[1:]
108-
router = self.build_router_fn(teams, edge_config.rule)
109-
super_graph.add_conditional_edges(edge[0], router, {item: item for item in teams})
110-
else:
111-
super_graph.add_edge(edge[0], edge[1])
112-
113-
if isinstance(router_config.end_node, list):
114-
for node in router_config.end_node:
115-
super_graph.add_edge(node, END)
116-
else:
117-
super_graph.add_edge(router_config.end_node, END)
118-
119-
workflow_graph = super_graph.compile()
120-
121-
return FloRoutedTeam(self.flo_team.name, workflow_graph, self.flo_team.config)
122-
12393
class Builder():
12494

12595
def __init__(self, session: FloSession, config: TeamConfig, flo_team: FloTeam,) -> None:

flo_ai/router/flo_linear.py

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def __init__(self, session: FloSession, config: TeamConfig, flo_team: FloTeam):
1414
flo_team=flo_team, executor=None, config=config)
1515
self.router_config = config.router
1616

17-
def build_agent_graph(self):
17+
def build_graph(self):
1818
flo_agent_nodes = [self.build_node(member) for member in self.members]
1919

2020
workflow = StateGraph(TeamFloAgentState)
@@ -31,7 +31,6 @@ def build_agent_graph(self):
3131
parent_node = flo_agent_nodes[i]
3232
child_node = flo_agent_nodes[i+1]
3333
next_node = flo_agent_nodes[i+2] if (i+2) < len(flo_agent_nodes) else END
34-
3534
if (parent_node.kind == ExecutableType.reflection):
3635
self.add_reflection_edge(workflow, parent_node, child_node)
3736
continue
@@ -56,33 +55,6 @@ def build_agent_graph(self):
5655

5756
return FloRoutedTeam(self.flo_team.name, workflow_graph, self.flo_team.config)
5857

59-
def build_team_graph(self):
60-
flo_team_entry_chains = [self.build_node_for_teams(flo_agent) for flo_agent in self.members]
61-
# Define the graph.
62-
super_graph = StateGraph(TeamFloAgentState)
63-
# First add the nodes, which will do the work
64-
for flo_team_chain in flo_team_entry_chains:
65-
agent_name = flo_team_chain.name
66-
super_graph.add_node(agent_name, flo_team_chain.func)
67-
68-
if self.router_config.edges is None:
69-
start_node_name = flo_team_entry_chains[0].name
70-
end_node_name = flo_team_entry_chains[-1].name
71-
super_graph.add_edge(START, start_node_name)
72-
for i in range(len(flo_team_entry_chains) - 1):
73-
agent1_name = flo_team_entry_chains[i].name
74-
agent2_name = flo_team_entry_chains[i+1].name
75-
super_graph.add_edge(agent1_name, agent2_name)
76-
super_graph.add_edge(end_node_name, END)
77-
else:
78-
super_graph.add_edge(START, self.router_config.start_node)
79-
for edge in self.router_config.edges:
80-
super_graph.add_edge(edge[0], edge[1])
81-
super_graph.add_edge(self.router_config.end_node, END)
82-
83-
super_graph = super_graph.compile()
84-
return FloRoutedTeam(self.flo_team.name, super_graph, self.flo_team.config)
85-
8658
class Builder():
8759

8860
def __init__(self, session: FloSession, config: TeamConfig, flo_team: FloTeam,) -> None:

flo_ai/router/flo_llm_router.py

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
from langchain_core.output_parsers.openai_functions import JsonOutputFunctionsParser
1+
from typing import Union
22
from langchain_core.language_models import BaseLanguageModel
33
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
4-
from typing import Union
54
from langchain_core.runnables import Runnable
65
from flo_ai.state.flo_session import FloSession
76
from flo_ai.constants.prompt_constants import FLO_FINISH
@@ -11,6 +10,11 @@
1110
from langgraph.graph import StateGraph
1211
from flo_ai.state.flo_state import TeamFloAgentState
1312
from flo_ai.yaml.config import TeamConfig
13+
from langchain_core.output_parsers import JsonOutputParser
14+
from langchain_core.pydantic_v1 import BaseModel, Field
15+
16+
class NextAgent(BaseModel):
17+
next: str = Field(description="Name of the next member to be called")
1418

1519
class StateUpdateComponent:
1620
def __init__(self, name: str, session: FloSession) -> None:
@@ -35,37 +39,19 @@ def __init__(self,
3539
executor = executor
3640
)
3741

38-
def build_agent_graph(self):
42+
def build_graph(self):
3943
flo_agent_nodes = [self.build_node(flo_agent) for flo_agent in self.members]
4044
workflow = StateGraph(TeamFloAgentState)
4145
for flo_agent_node in flo_agent_nodes:
4246
workflow.add_node(flo_agent_node.name, flo_agent_node.func)
43-
workflow.add_node(self.router_name, self.executor)
47+
workflow.add_node(self.router_name, self.build_node(self).func)
4448
for member in self.member_names:
4549
workflow.add_edge(member, self.router_name)
4650
workflow.add_conditional_edges(self.router_name, self.router_fn)
4751
workflow.set_entry_point(self.router_name)
4852
workflow_graph = workflow.compile()
4953
return FloRoutedTeam(self.flo_team.name, workflow_graph, self.flo_team.config)
50-
51-
def build_team_graph(self):
52-
flo_team_entry_chains = [self.build_node_for_teams(flo_agent) for flo_agent in self.members]
53-
# Define the graph.
54-
super_graph = StateGraph(TeamFloAgentState)
55-
# First add the nodes, which will do the work
56-
for flo_team_chain in flo_team_entry_chains:
57-
super_graph.add_node(flo_team_chain.name, flo_team_chain.func)
58-
super_graph.add_node(self.router_name, self.executor)
59-
60-
for member in self.member_names:
61-
super_graph.add_edge(member, self.router_name)
62-
63-
super_graph.add_conditional_edges(self.router_name, self.router_fn)
64-
65-
super_graph.set_entry_point(self.router_name)
66-
super_graph = super_graph.compile()
67-
return FloRoutedTeam(self.flo_team.name, super_graph, self.flo_team.config)
68-
54+
6955
class Builder:
7056
def __init__(self,
7157
session: FloSession,
@@ -89,6 +75,7 @@ def __init__(self,
8975
" respond with the worker to act next "
9076
)
9177

78+
self.parser = JsonOutputParser(pydantic_object=NextAgent)
9279
self.llm_router_prompt = ChatPromptTemplate.from_messages(
9380
[
9481
("system", router_base_system_message),
@@ -97,38 +84,25 @@ def __init__(self,
9784
(
9885
"system",
9986
"Given the conversation above, who should act next?"
100-
" Or should we FINISH if the task is already answered ? Select one of: {options}",
87+
" Or should we FINISH if the task is already answered ? Select one of: {options} \n {format_instructions}",
10188
),
10289
]
103-
).partial(options=str(self.options), members=", ".join(self.members), member_type=member_type, router_prompt=router_prompt)
90+
).partial(
91+
options=str(self.options),
92+
members=", ".join(self.members),
93+
member_type=member_type,
94+
router_prompt=router_prompt,
95+
format_instructions=self.parser.get_format_instructions()
96+
)
10497

105-
def build(self):
106-
function_def = {
107-
"name": "route",
108-
"description": "Select the next role.",
109-
"parameters": {
110-
"title": "routeSchema",
111-
"type": "object",
112-
"properties": {
113-
"next": {
114-
"title": "Next",
115-
"anyOf": [
116-
{"enum": self.options},
117-
],
118-
}
119-
},
120-
"required": ["next"],
121-
}
122-
}
123-
98+
def build(self):
12499
chain = (
125100
self.llm_router_prompt
126-
| self.llm.bind_functions(functions=[function_def], function_call="route")
127-
| JsonOutputFunctionsParser()
128-
| StateUpdateComponent(self.name, self.session)
101+
| self.llm
102+
| self.parser
129103
)
130104

131-
return FloLLMRouter(executor = chain,
105+
return FloLLMRouter(executor=chain,
132106
flo_team=self.flo_team,
133107
name=self.name,
134108
session=self.session)

0 commit comments

Comments
 (0)