Skip to content

Commit

Permalink
Add task relationship APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanholmes committed Jun 4, 2024
1 parent 385cb62 commit f061d1e
Show file tree
Hide file tree
Showing 9 changed files with 550 additions and 137 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed
- `Workflow.insert_task()` no longer inserts duplicate tasks when given multiple parent tasks.

### Added
- `AzureOpenAiStructureConfig` for providing Structures with all Azure OpenAI Driver configuration.
- `AzureOpenAiVisionImageQueryDriver` to support queries on images using Azure's OpenAI Vision models.
- Parameter `env` to `BaseStructureRunDriver` to set environment variables for a Structure Run.
- `BaseTask.add_child()` to add a child task to a parent task.
- `BaseTask.add_children()` to add multiple child tasks to a parent task.
- `BaseTask.add_parent()` to add a parent task to a child task.
- `BaseTask.add_parents()` to add multiple parent tasks to a child task.
- `Structure.resolve_relationships()` to resolve asymmetrically defined parent/child relationships. In other words, if a parent declares a child, but the child does not declare the parent, the parent will automatically be added as a parent of the child when running this method. The method is invoked automatically by `Structure.before_run()`.

### Changed
- **BREAKING**: Updated OpenAI-based image query drivers to remove Vision from the name.
Expand All @@ -18,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Field `azure_ad_token` on all Azure Drivers is no longer serializable.
- Default standard OpenAI and Azure OpenAI image query model to `gpt-4o`.
- Error message to be more helpful when importing optional dependencies.
- **BREAKING**: `Workflow` no longer modifies task relationships when adding tasks via `tasks` init param, `add_tasks()` or `add_task()`. Previously, adding a task would automatically add the previously added task as its parent. Existing code that relies on this behavior will need to be updated to explicitly add parent/child relationships using the API offered by `BaseTask`.
- `Structure.before_run()` now automatically resolves asymmetrically defined parent/child relationships using the new `Structure.resolve_relationships()`.

## [0.25.1] - 2024-05-15

Expand Down
44 changes: 21 additions & 23 deletions docs/examples/multi-agent-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,35 +155,33 @@ if __name__ == "__main__":
),
),
)
end_task = team.add_task(
PromptTask(
'State "All Done!"',
)
)
team.insert_tasks(
research_task,
[
StructureRunTask(
(
"""Using insights provided, develop an engaging blog
writer_tasks = team.add_tasks(*[
StructureRunTask(
(
"""Using insights provided, develop an engaging blog
post that highlights the most significant AI advancements.
Your post should be informative yet accessible, catering to a tech-savvy audience.
Make it sound cool, avoid complex words so it doesn't sound like AI.
Insights:
{{ parent_outputs["research"] }}""",
),
driver=LocalStructureRunDriver(
structure_factory_fn=lambda: build_writer(
role=writer["role"],
goal=writer["goal"],
backstory=writer["backstory"],
)
),
)
for writer in WRITERS
],
end_task,
),
driver=LocalStructureRunDriver(
structure_factory_fn=lambda: build_writer(
role=writer["role"],
goal=writer["goal"],
backstory=writer["backstory"],
)
),
parent_ids=[research_task.id],
)
for writer in WRITERS
])
end_task = team.add_task(
PromptTask(
'State "All Done!"',
parent_ids=[writer_task.id for writer_task in writer_tasks],
)
)

team.run()
Expand Down
33 changes: 15 additions & 18 deletions docs/griptape-framework/structures/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,35 @@ Let's build a simple workflow. Let's say, we want to write a story in a fantasy
from griptape.tasks import PromptTask
from griptape.structures import Workflow

workflow = Workflow()

world_task = PromptTask(
"Create a fictional world based on the following key words {{ keywords|join(', ') }}",
context={
"keywords": ["fantasy", "ocean", "tidal lock"]
},
id="world"
)

def character_task(task_id, character_name) -> PromptTask:
return PromptTask(
"Based on the following world description create a character named {{ name }}:\n{{ parent_outputs['world'] }}",
context={
"name": character_name
},
id=task_id
id=task_id,
parent_ids=["world"]
)

world_task = PromptTask(
"Create a fictional world based on the following key words {{ keywords|join(', ') }}",
context={
"keywords": ["fantasy", "ocean", "tidal lock"]
},
id="world"
)
workflow.add_task(world_task)
scotty_task = character_task("scotty", "Scotty")
annie_task = character_task("annie", "Annie")

story_task = PromptTask(
"Based on the following description of the world and characters, write a short story:\n{{ parent_outputs['world'] }}\n{{ parent_outputs['scotty'] }}\n{{ parent_outputs['annie'] }}",
id="story"
id="story",
parent_ids=["world", "scotty", "annie"]
)
workflow.add_task(story_task)

character_task_1 = character_task("scotty", "Scotty")
character_task_2 = character_task("annie", "Annie")

# Note the preserve_relationship flag. This ensures that world_task remains a parent of
# story_task so its output can be referenced in the story_task prompt.
workflow.insert_tasks(world_task, [character_task_1, character_task_2], story_task, preserve_relationship=True)
workflow = Workflow(tasks=[world_task, story_task, scotty_task, annie_task, story_task])

workflow.run()
```
Expand Down
22 changes: 22 additions & 0 deletions griptape/structures/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,35 @@ def publish_event(self, event: BaseEvent, flush: bool = False) -> None:
def context(self, task: BaseTask) -> dict[str, Any]:
return {"args": self.execution_args, "structure": self}

def resolve_relationships(self) -> None:
task_by_id = {task.id: task for task in self.tasks}

for task in self.tasks:
# Ensure parents include this task as a child
for parent_id in task.parent_ids:
if parent_id not in task_by_id:
raise ValueError(f"Task with id {parent_id} doesn't exist.")
parent = task_by_id[parent_id]
if task.id not in parent.child_ids:
parent.child_ids.append(task.id)

# Ensure children include this task as a parent
for child_id in task.child_ids:
if child_id not in task_by_id:
raise ValueError(f"Task with id {child_id} doesn't exist.")
child = task_by_id[child_id]
if task.id not in child.parent_ids:
child.parent_ids.append(task.id)

def before_run(self) -> None:
self.publish_event(
StartStructureRunEvent(
structure_id=self.id, input_task_input=self.input_task.input, input_task_output=self.input_task.output
)
)

self.resolve_relationships()

def after_run(self) -> None:
self.publish_event(
FinishStructureRunEvent(
Expand Down
11 changes: 6 additions & 5 deletions griptape/structures/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ class Workflow(Structure):
def add_task(self, task: BaseTask) -> BaseTask:
task.preprocess(self)

if self.output_task:
self.output_task.child_ids.append(task.id)
task.parent_ids.append(self.output_task.id)

self.tasks.append(task)

return task
Expand Down Expand Up @@ -77,6 +73,7 @@ def insert_task(
if parent_task.id in child_task.parent_ids:
child_task.parent_ids.remove(parent_task.id)

last_parent_index = -1
for parent_task in parent_tasks:
# Link the new task to the parent task
if parent_task.id not in task.parent_ids:
Expand All @@ -85,7 +82,11 @@ def insert_task(
parent_task.child_ids.append(task.id)

parent_index = self.tasks.index(parent_task)
self.tasks.insert(parent_index + 1, task)
if parent_index > last_parent_index:
last_parent_index = parent_index

# Insert the new task once, just after the last parent task
self.tasks.insert(last_parent_index + 1, task)

return task

Expand Down
18 changes: 0 additions & 18 deletions griptape/tasks/actions_subtask.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,24 +172,6 @@ def actions_to_dicts(self) -> list[dict]:
def actions_to_json(self) -> str:
return json.dumps(self.actions_to_dicts())

def add_child(self, child: ActionsSubtask) -> ActionsSubtask:
if child.id not in self.child_ids:
self.child_ids.append(child.id)

if self.id not in child.parent_ids:
child.parent_ids.append(self.id)

return child

def add_parent(self, parent: ActionsSubtask) -> ActionsSubtask:
if parent.id not in self.parent_ids:
self.parent_ids.append(parent.id)

if self.id not in parent.child_ids:
parent.child_ids.append(self.id)

return parent

def __init_from_prompt(self, value: str) -> None:
thought_matches = re.findall(self.THOUGHT_PATTERN, value, re.MULTILINE)
actions_matches = re.findall(self.ACTIONS_PATTERN, value, re.DOTALL)
Expand Down
20 changes: 20 additions & 0 deletions griptape/tasks/base_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ def meta_memories(self) -> list[BaseMetaEntry]:
def __str__(self) -> str:
return str(self.output.value)

def add_parents(self, *parents: str | BaseTask) -> None:
for parent in parents:
self.add_parent(parent)

def add_parent(self, parent: str | BaseTask) -> None:
parent_id = parent if isinstance(parent, str) else parent.id

if parent_id not in self.parent_ids:
self.parent_ids.append(parent_id)

def add_children(self, *children: str | BaseTask) -> None:
for child in children:
self.add_child(child)

def add_child(self, child: str | BaseTask) -> None:
child_id = child if isinstance(child, str) else child.id

if child_id not in self.child_ids:
self.child_ids.append(child_id)

def preprocess(self, structure: Structure) -> BaseTask:
self.structure = structure

Expand Down
Loading

0 comments on commit f061d1e

Please sign in to comment.