diff --git a/cmd/verifyexamples/examples.go b/cmd/verifyexamples/examples.go index 01f97b86..549d603b 100644 --- a/cmd/verifyexamples/examples.go +++ b/cmd/verifyexamples/examples.go @@ -734,6 +734,19 @@ var workflowExamples = []ExampleDefinition{ "The output should not contain error messages or stack traces.", }, }, + { + Name: "03_workflows_agents_workflow_as_an_agent_human_in_the_loop", + ProjectPath: "examples/03-workflows/agents/workflow_as_an_agent_human_in_the_loop", + RequiredEnvironmentVariables: []string{"FOUNDRY_PROJECT_ENDPOINT"}, + OptionalEnvironmentVariables: []string{"FOUNDRY_MODEL"}, + Inputs: inputLines("Y"), + InputDelay: 5 * time.Second, + ExpectedOutputDescription: []string{ + "The output should wrap a workflow as an agent that asks for approval before calling the weather tool.", + "After approval, the workflow should resume and report the weather in Amsterdam (cloudy with a high of 15°C).", + "The output should not contain error messages or stack traces.", + }, + }, { Name: "03_workflows_checkpoint_checkpoint_and_rehydrate", ProjectPath: "examples/03-workflows/checkpoint/checkpoint_and_rehydrate", diff --git a/examples/03-workflows/agents/workflow_as_an_agent_human_in_the_loop/main.go b/examples/03-workflows/agents/workflow_as_an_agent_human_in_the_loop/main.go new file mode 100644 index 00000000..18c0f384 --- /dev/null +++ b/examples/03-workflows/agents/workflow_as_an_agent_human_in_the_loop/main.go @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft. All rights reserved. + +package main + +import ( + "context" + "fmt" + + "github.com/microsoft/agent-framework-go/agent" + "github.com/microsoft/agent-framework-go/examples/internal/demo" + "github.com/microsoft/agent-framework-go/message" + "github.com/microsoft/agent-framework-go/provider/foundryprovider" + "github.com/microsoft/agent-framework-go/tool" + "github.com/microsoft/agent-framework-go/tool/functool" + "github.com/microsoft/agent-framework-go/workflow/agentworkflow" +) + +var logger = demo.NewLogger( + "Workflow as an Agent with Human-in-the-Loop", + "This sample wraps a workflow whose hosted agent requires tool approval, resumed across the agent boundary.", + "Model", demo.FoundryModel, +) + +var weatherTool = functool.MustNew(functool.Config{ + Name: "weather", + Description: "Get the current weather for a given location", +}, func(_ context.Context, location string) (string, error) { + return fmt.Sprintf("The weather in %s is cloudy with a high of 15°C.", location), nil +}) + +func main() { + token := demo.FoundryTokenCredential() + + // The hosted agent's function tool is wrapped with tool.ApprovalRequiredFunc, + // so the workflow raises a ToolApprovalRequestContent before the tool runs. + assistant := foundryprovider.NewAgent( + demo.FoundryProjectEndpoint, + token, + foundryprovider.ModelDeployment(demo.FoundryModel), + foundryprovider.AgentConfig{ + Instructions: "You are a helpful assistant. Use the weather tool to answer questions about the weather.", + Config: agent.Config{ + Name: "WeatherAssistant", + Middlewares: []agent.Middleware{logger}, + Tools: []tool.Tool{tool.ApprovalRequiredFunc(weatherTool)}, + }, + }, + ) + + wf, err := agentworkflow.NewSequentialWorkflowBuilder(assistant). + WithName("weather-approval-workflow"). + Build() + if err != nil { + demo.Panic(err) + } + + // Wrapping the workflow as an agent surfaces the workflow's pending + // approval request as a ResponseUpdate carrying ToolApprovalRequestContent. + // The caller resumes by passing the matching ToolApprovalResponseContent in + // the next run on the SAME session; the provider routes it back into the + // workflow as an ExternalResponse to complete the turn. + wfAgent, err := agentworkflow.NewAgent(wf, agentworkflow.AgentConfig{ + IncludeOutputsInResponse: true, + Config: agent.Config{ + Name: "WeatherWorkflowAgent", + }, + }) + if err != nil { + demo.Panic(err) + } + + ctx := context.Background() + session, err := wfAgent.CreateSession(ctx) + if err != nil { + demo.Panic(err) + } + + demo.Assistant("User: What is the weather like in Amsterdam?") + resp, err := wfAgent.RunText(ctx, "What is the weather like in Amsterdam?", agent.WithSession(session)).Collect() + demo.Response(resp, err) + + // Detect the approval request surfaced across the NewAgent boundary and + // prompt the user for each pending tool call. + var userResponses []message.Content + var approvedRequests bool + for c := range resp.Contents() { + request, ok := c.(*message.ToolApprovalRequestContent) + if !ok { + continue + } + approved := demo.UserInputRequest(request) + if approved { + approvedRequests = true + } + userResponses = append(userResponses, request.CreateResponse(approved, "")) + } + if len(userResponses) == 0 { + demo.Assistant("No approval requests were raised by the workflow.") + return + } + if !approvedRequests { + demo.Assistant("User did not approve any function calls.") + return + } + + // Resume the SAME session with the approval responses so the workflow can + // finish the turn and produce the final answer. + resp, err = wfAgent.RunMessage(ctx, message.New(userResponses...), agent.WithSession(session)).Collect() + demo.Response(resp, err) +} diff --git a/workflow_as_an_agent_human_in_the_loop b/workflow_as_an_agent_human_in_the_loop new file mode 100755 index 00000000..24bace94 Binary files /dev/null and b/workflow_as_an_agent_human_in_the_loop differ