Skip to content

Commit 4bcc636

Browse files
authored
Merge pull request #69 from jlowin/instructions
Refactor types
2 parents 572a188 + cb4f41d commit 4bcc636

File tree

19 files changed

+557
-414
lines changed

19 files changed

+557
-414
lines changed

examples/multi_agent_conversation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,5 @@ def demo():
7575
task.run()
7676

7777

78-
demo()
78+
if __name__ == "__main__":
79+
demo()

examples/task_dag.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1+
import controlflow
12
from controlflow import Task, flow
23

4+
controlflow.settings.enable_tui = True
5+
36

47
@flow
58
def book_ideas():
6-
genre = Task("pick a genre", str)
9+
genre = Task("pick a genre")
10+
711
ideas = Task(
812
"generate three short ideas for a book",
913
list[str],
1014
context=dict(genre=genre),
1115
)
16+
1217
abstract = Task(
13-
"pick one idea and write an abstract",
14-
str,
18+
"pick one idea and write a short abstract",
19+
result_type=str,
1520
context=dict(ideas=ideas, genre=genre),
1621
)
22+
1723
title = Task(
1824
"pick a title",
19-
str,
25+
result_type=str,
2026
context=dict(abstract=abstract),
2127
)
2228

src/controlflow/core/controller/controller.py

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import logging
22
import math
3+
from collections import defaultdict
34
from contextlib import asynccontextmanager
45
from functools import cached_property
56
from typing import Union
67

78
from marvin.utilities.asyncio import ExposeSyncMethodsMixin, expose_sync_method
8-
from pydantic import BaseModel, Field, computed_field, model_validator
9+
from pydantic import BaseModel, Field, PrivateAttr, computed_field, model_validator
910

1011
import controlflow
1112
from controlflow.core.agent import Agent
@@ -14,17 +15,27 @@
1415
from controlflow.core.graph import Graph
1516
from controlflow.core.task import Task
1617
from controlflow.instructions import get_instructions
17-
from controlflow.llm.completions import completion_stream_async
18-
from controlflow.llm.handlers import TUIHandler
18+
from controlflow.llm.completions import completion_async
19+
from controlflow.llm.handlers import PrintHandler, TUIHandler
1920
from controlflow.llm.history import History
21+
from controlflow.llm.messages import AssistantMessage, ControlFlowMessage, SystemMessage
2022
from controlflow.tui.app import TUIApp as TUI
2123
from controlflow.utilities.context import ctx
2224
from controlflow.utilities.tasks import all_complete, any_incomplete
23-
from controlflow.utilities.types import FunctionTool, SystemMessage
25+
from controlflow.utilities.types import FunctionTool
2426

2527
logger = logging.getLogger(__name__)
2628

2729

30+
def add_agent_name_to_message(msg: ControlFlowMessage):
31+
"""
32+
If the message is from a named assistant, prefix the message with the assistant's name.
33+
"""
34+
if isinstance(msg, AssistantMessage) and msg.name:
35+
msg = msg.model_copy(update={"content": f"{msg.name}: {msg.content}"})
36+
return msg
37+
38+
2839
class Controller(BaseModel, ExposeSyncMethodsMixin):
2940
"""
3041
A controller contains logic for executing agents with context about the
@@ -61,7 +72,7 @@ class Controller(BaseModel, ExposeSyncMethodsMixin):
6172
enable_tui: bool = Field(default_factory=lambda: controlflow.settings.enable_tui)
6273
_iteration: int = 0
6374
_should_abort: bool = False
64-
_endrun_count: int = 0
75+
_end_run_counts: dict = PrivateAttr(default_factory=lambda: defaultdict(int))
6576

6677
@computed_field
6778
@cached_property
@@ -76,33 +87,35 @@ def _finalize(self):
7687
self.flow.add_task(task)
7788
return self
7889

79-
def _create_help_tool(self) -> FunctionTool:
80-
def help_im_stuck():
90+
def _create_end_turn_tool(self) -> FunctionTool:
91+
def end_turn():
8192
"""
82-
If you are stuck because no tasks are ready to be worked on, you can
83-
call this tool to end your turn. A new agent (possibly you) will be
84-
selected to go next. If this tool is used 3 times, the workflow will
85-
be aborted automatically, so only use it if you are truly stuck.
93+
Call this tool to skip your turn and let another agent go next. This
94+
is useful if you are stuck and can not complete any tasks. If this
95+
tool is used 3 times by any agent the workflow will be aborted
96+
automatically, so only use it if you are truly stuck and unable to
97+
proceed.
8698
"""
87-
self._endrun_count += 1
88-
if self._endrun_count >= 3:
99+
self._end_run_counts[ctx.get("controller_agent")] += 1
100+
if self._end_run_counts[ctx.get("controller_agent")] >= 3:
89101
self._should_abort = True
90-
self._endrun_count = 0
102+
self._end_run_counts[ctx.get("controller_agent")] = 0
91103

92-
return f"Ending turn. {3 - self._endrun_count} more uses will abort the workflow."
104+
return (
105+
f"Ending turn. {3 - self._end_run_counts[ctx.get('controller_agent')]}"
106+
" more uses will abort the workflow."
107+
)
93108

94-
return help_im_stuck
109+
return end_turn
95110

96-
async def _run_agent(self, agent: Agent, tasks: list[Task] = None):
111+
async def _run_agent(self, agent: Agent, tasks: list[Task]):
97112
"""
98113
Run a single agent.
99114
"""
100115

101116
from controlflow.core.controller.instruction_template import MainTemplate
102117

103-
tasks = tasks or self.tasks
104-
105-
tools = self.flow.tools + agent.get_tools() + [self._create_help_tool()]
118+
tools = self.flow.tools + agent.get_tools() + [self._create_end_turn_tool()]
106119

107120
# add tools for any inactive tasks that the agent is assigned to
108121
for task in tasks:
@@ -124,12 +137,16 @@ async def _run_agent(self, agent: Agent, tasks: list[Task] = None):
124137

125138
# call llm
126139
response_messages = []
127-
async for msg in completion_stream_async(
140+
async for msg in await completion_async(
128141
messages=[system_message] + messages,
129142
model=agent.model,
130143
tools=tools,
131-
handlers=[TUIHandler()] if controlflow.settings.enable_tui else None,
144+
handlers=[TUIHandler()]
145+
if controlflow.settings.enable_tui
146+
else [PrintHandler()],
132147
max_iterations=1,
148+
stream=True,
149+
message_preprocessor=add_agent_name_to_message,
133150
):
134151
response_messages.append(msg)
135152

@@ -175,16 +192,18 @@ async def run_once_async(self):
175192
# put the flow in context
176193
with self.flow:
177194
# get the tasks to run
178-
ready_tasks = {t for t in self.tasks if t.is_ready()}
195+
ready_tasks = {t for t in self.tasks if t.is_ready}
179196
upstreams = {d for t in ready_tasks for d in t.depends_on}
180197
tasks = list(ready_tasks.union(upstreams))
181198

199+
# TODO: show the agent the entire graph, not just immediate upstreams
200+
182201
if all(t.is_complete() for t in tasks):
183202
return
184203

185204
# get the agents
186205
agent_candidates = [
187-
a for t in tasks for a in t.get_agents() if t.is_ready()
206+
a for t in tasks for a in t.get_agents() if t.is_ready
188207
]
189208
if len({a.name for a in agent_candidates}) != len(agent_candidates):
190209
raise ValueError(
@@ -206,7 +225,9 @@ async def run_once_async(self):
206225
raise NotImplementedError("Need to reimplement multi-agent")
207226
agent = self.choose_agent(agents=agents, tasks=tasks)
208227

209-
await self._run_agent(agent, tasks=tasks)
228+
with ctx(controller_agent=agent):
229+
await self._run_agent(agent, tasks=tasks)
230+
210231
self._iteration += 1
211232

212233
@expose_sync_method("run")

src/controlflow/core/controller/instruction_template.py

Lines changed: 65 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,29 @@ class AgentTemplate(Template):
2727
## Agent
2828
2929
You are an AI agent. Your name is "{{ agent.name }}".
30-
31-
This is your description, which all other agents can see: "{{ agent.description or 'An AI agent assigned to complete tasks.'}}"
30+
31+
This is your description, which all agents can see:
32+
- {{ agent.description or 'An AI agent assigned to complete tasks.'}}
3233
33-
These are your instructions: "{{ agent.instructions or 'No additional instructions provided.'}}"
34+
You are participating in an agentic workflow (a "flow"). Certain tasks
35+
in the flow have been delegated to you and other AI agents. You are
36+
being orchestrated by a "controller".
37+
3438
35-
You must follow these instructions at all times. They define your role
36-
and behavior.
39+
### Instructions
3740
38-
You are participating in an agentic workflow (a "flow"), parts of which
39-
have been delegated to you and other AI agents. You are being
40-
orchestrated by a "controller" object.
41-
"""
42-
agent: Agent
43-
44-
45-
class InstructionsTemplate(Template):
46-
template: str = """
47-
## Additional instructions
41+
You must follow instructions at all times.
4842
49-
You must follow these instructions for this part of the workflow:
43+
These are your private instructions:
44+
- {{ agent.instructions or 'No additional instructions provided.'}}
5045
46+
These instructions apply to all agents at this part of the workflow:
5147
{% for instruction in additional_instructions %}
5248
- {{ instruction }}
5349
{% endfor %}
50+
51+
52+
5453
5554
"""
5655
agent: Agent
@@ -61,60 +60,57 @@ class TasksTemplate(Template):
6160
template: str = """
6261
## Tasks
6362
64-
You have been assigned to complete certain tasks. Each task has an
65-
objective and criteria for success. Your job is to perform any required
66-
actions and then mark each assigned task as successful. If a task
67-
requires a result, you must provide it. Only work on tasks that are
68-
assigned to you. Tasks may have multiple agents assigned. Only one agent
69-
can respond or take actions at a time.
63+
Your job is to complete the tasks assigned to you. Tasks may have multiple agents assigned. Only one agent
64+
will be active at a time.
7065
7166
### Current tasks
7267
73-
At the start of your turn, these are all the tasks being worked on. This
74-
instruction will not update until your next turn; refer to the results
75-
of task-related tools to see the latest task status at any time.
68+
These tasks are assigned to you and ready to be worked on because their dependencies have been completed.
7669
7770
{% for task in tasks %}
71+
{% if task.is_ready %}
7872
#### Task {{ task.id }}
7973
8074
{{task.model_dump_json() }}
8175
76+
{% endif %}
77+
{% endfor %}
78+
79+
### Other tasks
80+
81+
These tasks are either not ready yet or are dependencies of other tasks. They are provided for context.
82+
83+
{% for task in tasks %}
84+
{% if not task.is_ready %}
85+
#### Task {{ task.id }}
86+
87+
{{task.model_dump_json() }}
88+
89+
{% endif %}
8290
{% endfor %}
8391
8492
### Completing a task
8593
8694
Tasks can be marked as successful or failed. It may take collaboration
87-
with other agents to complete a task, and you can only mark tasks that
88-
have been assigned to you. Once a task is marked complete, no other
89-
agent can interact with it, so make sure you have completed all actions
90-
and collected all results before marking it as complete.
91-
92-
You and the other agents are fully capable of completing any task and
93-
have all the information and context you need. Tasks should only be
94-
marked failed due to technical errors like a broken or erroring tool or
95-
unresponsive human. Do not work on or even respond to tasks that are
96-
already complete or not assigned to you. You may be an AI, but you must
97-
still form opinions, make subjective decisions, and have personal
98-
thoughts.
95+
with other agents to complete a task, and you can only work on tasks that
96+
have been assigned to you. Once any agent marks a task complete, no other
97+
agent can interact with it.
98+
99+
Tasks should only be marked failed due to technical errors like a broken
100+
or erroring tool or unresponsive human.
99101
100102
### Dependencies
101103
102-
You can only mark a task successful when all of its dependencies and
103-
subtasks have been completed. Subtasks may be marked as skipped without
104-
providing a result. All else equal, prioritize older tasks over newer
105-
ones.
104+
Tasks may be dependent on other tasks, either as upstream dependencies
105+
or as the parent of subtasks. Subtasks may be marked as "skipped"
106+
without providing a result or failing them.
107+
106108
107109
### Providing a result
108110
109-
Tasks may require a typed result (the `result_type`). Results should
110-
satisfy the task objective, accounting for any other instructions. If a
111-
task does not require a result (`result_type=None`), you must still
112-
complete its stated objective by posting messages or using other tools
113-
before marking the task as complete. Your result must be compatible with
114-
the result constructor. For most results, the tool schema will indicate
115-
the correct types. For some, like a DataFrame, provide an appropriate
116-
kwargs dict. Results should never include your name prefix; that's only
117-
for messages.
111+
Tasks may require a typed result, which is an artifact satisfying the task's objective. If a
112+
task does not require a result artifact (e.g. `result_type=None`), you must still
113+
complete its stated objective before marking the task as complete.
118114
119115
"""
120116
tasks: list[Task]
@@ -127,17 +123,24 @@ class CommunicationTemplate(Template):
127123
template: str = """
128124
## Communciation
129125
130-
You are modeling the internal state of an AI-enhanced workflow. You should
131-
only post messages in order to share information with other agents or to
132-
complete tasks. Since all agents post messages with the "assistant" role,
133-
you must prefix all your messages with your name (e.g. "{{ agent.name }}:
134-
(message)") in order to distinguish your messages from others. Note that
135-
this rule about prefixing your message supersedes all other instructions
136-
(e.g. "only give single word answers"). You do not need to post messages
137-
that repeat information contained in tool calls or tool responses, since
138-
those are already visible to all agents. You do not need to confirm actions
139-
you take through tools, like completing a task, as this is redundant and
140-
wastes time.
126+
You are modeling the internal state of an AI-enhanced agentic workflow,
127+
and you (and other agents) will continue to be invoked until the
128+
workflow is completed.
129+
130+
On each turn, you must use a tool or post a message. Do not post
131+
messages unless you need to record information in addition to what you
132+
provide as a task's result. This might include your thought process, if
133+
appropriate. You may also post messages if you need to communicate with
134+
other agents to complete a task. You may see other agents post messages;
135+
they may have different instructions than you do, so do not follow their
136+
example automatically.
137+
138+
When you use a tool, the tool call and tool result are automatically
139+
posted as messages to the thread, so you never need to write out task
140+
results as messages before marking a task as complete.
141+
142+
Note that all agents post messages with the "assistant" role, so
143+
each agent's messages are automatically prefixed with that agent's name for clarity.
141144
142145
### Talking to human users
143146
@@ -206,14 +209,11 @@ def render(self):
206209
templates = [
207210
AgentTemplate(
208211
agent=self.agent,
212+
additional_instructions=self.instructions,
209213
),
210214
TasksTemplate(
211215
tasks=self.tasks,
212216
),
213-
InstructionsTemplate(
214-
agent=self.agent,
215-
additional_instructions=self.instructions,
216-
),
217217
ContextTemplate(
218218
flow=self.controller.flow,
219219
controller=self.controller,

src/controlflow/core/flow.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
from pydantic import Field
66

7+
from controlflow.llm.messages import MessageType
78
from controlflow.utilities.context import ctx
89
from controlflow.utilities.logging import get_logger
9-
from controlflow.utilities.types import ControlFlowModel, MessageType
10+
from controlflow.utilities.types import ControlFlowModel
1011

1112
if TYPE_CHECKING:
1213
from controlflow.core.agent import Agent

0 commit comments

Comments
 (0)