-
Notifications
You must be signed in to change notification settings - Fork 79
Enable native Dapr workflows with LLM and Agent decorators #232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Roberto Rodriguez <[email protected]>
Signed-off-by: Roberto Rodriguez <[email protected]>
Signed-off-by: Roberto Rodriguez <[email protected]>
Signed-off-by: Roberto Rodriguez <[email protected]>
Signed-off-by: Roberto Rodriguez <[email protected]>
Signed-off-by: Roberto Rodriguez <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
amazingggggggg - thank you for your efforts on this! Few comments for my own understanding/clarity please 🙏 🙌
| # Scalar / positional fallback: bind to the first parameter if present. | ||
| if not signature.parameters: | ||
| return {} | ||
| first_param = next(iter(signature.parameters)) | ||
| return {first_param: raw_input} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to put this inside a loop in case there are multiple parameters?
| ) | ||
|
|
||
|
|
||
| def strip_context_parameter(signature: inspect.Signature) -> inspect.Signature: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still reading through the rest of the changes, but want to post in case i forget later on... do we add back the ctx? or it is not needed in the case of llm_activity or agent_activity?
| ### Agent-based Workflow Patterns | ||
|
|
||
| Learn to orchestrate **autonomous, role-driven agents** inside Dapr Workflows using the `@agent_activity` decorator. | ||
| These patterns focus on chaining and coordinating specialized agents that reason, plan, and act within durable, stateful workflows. | ||
|
|
||
| - **Agent-driven Tasks**: Execute workflow activities through autonomous agents with defined roles and instructions | ||
| - **Sequential & Composed Flows**: Chain multiple agents together, passing context and results between steps | ||
| - **Resilient Orchestration**: Combine agent reasoning with Dapr’s durable state, recovery, and execution guarantees | ||
|
|
||
| This quickstart demonstrates how to design and run **agent-based workflows**, starting with a sequential chain of agents collaborating to complete a shared objective. | ||
|
|
||
| [Go to Agent-based Workflow Patterns](./04-agent-based-workflows/) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you please add if this applies to agent and durableagent classes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it! I will add that. Currently, it does not process Durable Agents. It only works with native Agent class. I asked a similiar quesiton above: Can an Activity trigger a child workflow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DurableAgent within workflow will be great! Tracking that here
Here is an example how to trigger it, not sure if it can be from within an activity:
https://github.com/dapr/python-sdk/blob/main/examples/demo_workflow/app.py#L58
| ### 1. Single LLM Activity (01_single_activity_workflow.py) | ||
|
|
||
| A simple example where the workflow executes one LLM-powered activity that returns a short biography. | ||
|
|
||
| ```python | ||
| import time | ||
| import dapr.ext.workflow as wf | ||
| from dapr.ext.workflow import DaprWorkflowContext | ||
| from dotenv import load_dotenv | ||
| from dapr_agents.llm.dapr import DaprChatClient | ||
| from dapr_agents.workflow.decorators import llm_activity | ||
| # Load environment variables (e.g., API keys, secrets) | ||
| load_dotenv() | ||
| # Initialize the Dapr workflow runtime and LLM client | ||
| runtime = wf.WorkflowRuntime() | ||
| llm = DaprChatClient(component_name="openai") | ||
| @runtime.workflow(name="single_task_workflow") | ||
| def single_task_workflow(ctx: DaprWorkflowContext, name: str): | ||
| """Ask the LLM about a single historical figure and return a short bio.""" | ||
| response = yield ctx.call_activity(describe_person, input={"name": name}) | ||
| return response | ||
| @runtime.activity(name="describe_person") | ||
| @llm_activity( | ||
| prompt="Who was {name}?", | ||
| llm=llm, | ||
| ) | ||
| async def describe_person(ctx, name: str) -> str: | ||
| pass | ||
| if __name__ == "__main__": | ||
| runtime.start() | ||
| time.sleep(5) | ||
| client = wf.DaprWorkflowClient() | ||
| instance_id = client.schedule_new_workflow( | ||
| workflow=single_task_workflow, | ||
| input="Grace Hopper", | ||
| ) | ||
| print(f"Workflow started: {instance_id}") | ||
| state = client.wait_for_workflow_completion(instance_id) | ||
| if not state: | ||
| print("No state returned (instance may not exist).") | ||
| elif state.runtime_status.name == "COMPLETED": | ||
| print(f"Grace Hopper bio:\n{state.serialized_output}") | ||
| else: | ||
| print(f"Workflow ended with status: {state.runtime_status}") | ||
| if state.failure_details: | ||
| fd = state.failure_details | ||
| print("Failure type:", fd.error_type) | ||
| print("Failure message:", fd.message) | ||
| print("Stack trace:\n", fd.stack_trace) | ||
| else: | ||
| print("Custom status:", state.serialized_custom_status) | ||
| runtime.shutdown() | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| ### 1. Single LLM Activity (01_single_activity_workflow.py) | |
| A simple example where the workflow executes one LLM-powered activity that returns a short biography. | |
| ```python | |
| import time | |
| import dapr.ext.workflow as wf | |
| from dapr.ext.workflow import DaprWorkflowContext | |
| from dotenv import load_dotenv | |
| from dapr_agents.llm.dapr import DaprChatClient | |
| from dapr_agents.workflow.decorators import llm_activity | |
| # Load environment variables (e.g., API keys, secrets) | |
| load_dotenv() | |
| # Initialize the Dapr workflow runtime and LLM client | |
| runtime = wf.WorkflowRuntime() | |
| llm = DaprChatClient(component_name="openai") | |
| @runtime.workflow(name="single_task_workflow") | |
| def single_task_workflow(ctx: DaprWorkflowContext, name: str): | |
| """Ask the LLM about a single historical figure and return a short bio.""" | |
| response = yield ctx.call_activity(describe_person, input={"name": name}) | |
| return response | |
| @runtime.activity(name="describe_person") | |
| @llm_activity( | |
| prompt="Who was {name}?", | |
| llm=llm, | |
| ) | |
| async def describe_person(ctx, name: str) -> str: | |
| pass | |
| if __name__ == "__main__": | |
| runtime.start() | |
| time.sleep(5) | |
| client = wf.DaprWorkflowClient() | |
| instance_id = client.schedule_new_workflow( | |
| workflow=single_task_workflow, | |
| input="Grace Hopper", | |
| ) | |
| print(f"Workflow started: {instance_id}") | |
| state = client.wait_for_workflow_completion(instance_id) | |
| if not state: | |
| print("No state returned (instance may not exist).") | |
| elif state.runtime_status.name == "COMPLETED": | |
| print(f"Grace Hopper bio:\n{state.serialized_output}") | |
| else: | |
| print(f"Workflow ended with status: {state.runtime_status}") | |
| if state.failure_details: | |
| fd = state.failure_details | |
| print("Failure type:", fd.error_type) | |
| print("Failure message:", fd.message) | |
| print("Stack trace:\n", fd.stack_trace) | |
| else: | |
| print("Custom status:", state.serialized_custom_status) | |
| runtime.shutdown() | |
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we link to the file and not put the code in the readmes please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! I was following other READE examples, but yeah too much 😅
| # Handle both dict and model cases gracefully | ||
| q_list = ( | ||
| [q["text"] for q in questions["questions"]] | ||
| if isinstance(questions, dict) | ||
| else [q.text for q in questions.questions] | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i see that we have to support both data forms often in our code. Do you by chance know why? Like in this quickstart can we just support one? I think it's bc the underlying state implementation supports just dictionaries, but I'm not sure if that is because of our implementation in dapr agents or because of limitations within the python sdk or something else tbh
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, we should only support dictionaries here. I will fix that.
|
A few thoughts I wrote down while reviewing:
tagging @bibryam too, do you have any thoughts on how this might look when applied to orchestrators? We’re seeing more users on Discord starting to use them... |
Yes! I like the idea of supporting prompts from files 💯. I like that idea. Regarding the I will push a PR on this hopefully tomorrow. I am almost done. After that PR, I will have to figure out the "Observability" challenge. Moving away from a |
There will be efforts in dapr upstream in the sdks to bring in telemetry tracing to the clients. That is currently missing and something that dapr agents will benefit from to where we can just propagate the trace context instead of having our wrapper classes. This will also give us the full story in our workflow activities where right now you do not see things like the underlying activity might call something like a state store so we should see a state trace within the activity. IMO tracing will not be the best until we get that from the sdk side of things, so don't spend tooooooo much time there as it does not have to be perfect, and will have to be updated when sdks have the trace context for us. I also say that because I have also spent a fair amount of time on the tracing, so I know it gets quite complex there 🙃 furthermore, imo, if tracing gets broken on the "legacy" bits, then I don't think we really need to worry about that... just call it out in the PR pls bc before we cut any release I go through and check all the quickstarts (manually until I automate it) so I do confirm things on the tracing side are g2g too. I also really appreciate the PRs separated out some to help with reviewing 🤗 |
|
@Cyb3rWard0g this is a fantastic PR, thank you! |
|
any chance we can add a few tests for the decorators too pls? 🙏 |
Overview
This PR extends Dapr’s native workflow model to support LLM-powered and agent-based activity execution through new unified decorators. Developers can now define, register, and run workflows using standard Dapr patterns (
@runtime.workflow,@runtime.activity) while seamlessly integrating reasoning and automation via@llm_activityand@agent_activity. This approach preserves full control over the workflow runtime while enabling declarative, composable AI-driven orchestration.Key Changes
@llm_activityfor direct LLM-powered activity execution@agent_activityfor integrating autonomous agents in workflowsconvert_result()to handle bothBaseMessageand agent message typesExamples
LLM-based Single Task Workflow
LLM-based Parallel Tasks Workflow
Agent-Based Workflow