Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion examples/langgraph_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@
"framework_type": "langgraph",
"graph": "mailcomposer.mailcomposer:graph"
}
}
},
{
"type": "docker",
"name": "docker",
"image": "agntcy/wfsm-mailcomposer:<YOUR_TAG>"
}
],
"env_vars": [
{
Expand Down
15 changes: 14 additions & 1 deletion wfsm/assets/agent.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ ARG BASE_IMAGE=ghcr.io/agntcy/acp/wfsrv:latest
FROM $BASE_IMAGE

ARG AGENT_DIR
ARG AGENT_FRAMEWORK
ARG AGENT_OBJECT

WORKDIR /opt/agent-workflow-server

COPY $AGENT_DIR /opt/agent_src
RUN poetry run pip install /opt/agent_src

CMD ["poetry" ,"run", "server"]
COPY manifest.json /opt/spec/manifest.json
ENV AGENT_MANIFEST_PATH=/opt/spec/manifest.json

COPY start_agws.sh /opt/start_agws.sh
RUN chmod +x /opt/start_agws.sh

ENV AGWS_STORAGE_FILE=/opt/storage/agws_storage.pkl

ENV AGENT_FRAMEWORK=$AGENT_FRAMEWORK
ENV AGENT_OBJECT=$AGENT_OBJECT

ENTRYPOINT ["/opt/start_agws.sh"]
4 changes: 2 additions & 2 deletions wfsm/assets/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import _ "embed"
//go:embed agent.Dockerfile
var AgentBuilderDockerfile []byte

//go:embed workflowserver.Dockerfile
var WorkflowServerDockerfile []byte
//go:embed start_agws.sh
var StartAGWSScript []byte
5 changes: 5 additions & 0 deletions wfsm/assets/start_agws.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
: ${AGENT_ID:?"agent id must be provided"}
export AGENTS_REF="{\"$AGENT_ID\": \"$AGENT_OBJECT\"}"
# Run the Poetry server
poetry run server
8 changes: 2 additions & 6 deletions wfsm/internal/builder/container/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,10 @@ func NewContainerAgentBuilder() internal.AgentDeploymentBuilder {
}

func (b *cbuilder) Build(ctx context.Context, inputSpec internal.AgentSpec) (internal.AgentDeploymentBuildSpec, error) {
dockerDeployment := inputSpec.Manifest.Deployment.DeploymentOptions[inputSpec.SelectedDeploymentOption].DockerDeployment
return internal.AgentDeploymentBuildSpec{
AgentSpec: inputSpec,
Image: getImageName(inputSpec),
Image: dockerDeployment.Image,
ServiceName: inputSpec.DeploymentName,
}, nil
}

func getImageName(spec internal.AgentSpec) string {
dopts := spec.Manifest.Deployment.DeploymentOptions[spec.SelectedDeploymentOption]
return dopts.DockerDeployment.Image
}
2 changes: 1 addition & 1 deletion wfsm/internal/builder/python/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (b *pyBuilder) Build(ctx context.Context, inputSpec internal.AgentSpec) (in
}

imageName := strings.Join([]string{AgentImage, inputSpec.Manifest.Metadata.Ref.Name}, "-")
imgNameWithTag, err := EnsureContainerImage(ctx, imageName, agSrc, b.deleteBuildFolders, b.forceBuild, b.baseImage)
imgNameWithTag, err := EnsureContainerImage(ctx, imageName, agSrc, inputSpec, b.deleteBuildFolders, b.forceBuild, b.baseImage)
if err != nil {
return deploymentSpec, err
}
Expand Down
40 changes: 35 additions & 5 deletions wfsm/internal/builder/python/image_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import (
"path"

"github.com/cisco-eti/wfsm/assets"
"github.com/cisco-eti/wfsm/internal"
"github.com/cisco-eti/wfsm/internal/builder/python/source"
containerclient "github.com/cisco-eti/wfsm/internal/container_client"
"github.com/cisco-eti/wfsm/internal/util"
"github.com/cisco-eti/wfsm/manifests"
"github.com/containerd/errdefs"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
Expand All @@ -33,7 +35,7 @@ var containerImageBuildLock = util.NewStripedLock(100)
// EnsureContainerImage - ensure container image is available. If the image exists, it returns the name of the
// existing image, otherwise it builds a new image with the necessary packages installed
// and returns its name.
func EnsureContainerImage(ctx context.Context, img string, src source.AgentSource, deleteBuildFolders bool, forceBuild bool, baseImage string) (string, error) {
func EnsureContainerImage(ctx context.Context, img string, src source.AgentSource, inputSpec internal.AgentSpec, deleteBuildFolders bool, forceBuild bool, baseImage string) (string, error) {

log := zerolog.Ctx(ctx)
ctx = log.WithContext(ctx)
Expand Down Expand Up @@ -67,8 +69,16 @@ func EnsureContainerImage(ctx context.Context, img string, src source.AgentSourc
}()
}

// calc. hash based on agent source files will be used as image tag
hashCode := calculateHash(agentSrcPath, baseImage)
//copy manifest file to workspace
manifestFileBuf, err := manifests.NewNullableAgentManifest(&inputSpec.Manifest).MarshalJSON()
if err != nil {
return "", fmt.Errorf("failed to marshal agent manifest: %v", err)
}
manifestFile := path.Join(workspacePath, "manifest.json")
err = os.WriteFile(manifestFile, manifestFileBuf, util.OwnerCanReadWrite)

// calc. hash based on agent source files and manifest file and use as image tag
hashCode := calculateHash(workspacePath, baseImage)
img = fmt.Sprintf("%s:%s", img, hashCode)

client, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
Expand Down Expand Up @@ -107,7 +117,7 @@ func EnsureContainerImage(ctx context.Context, img string, src source.AgentSourc
}

// build image
err = buildImage(ctx, client, img, workspacePath, agentSourceDir, assets.AgentBuilderDockerfile, deleteBuildFolders, baseImage)
err = buildImage(ctx, client, img, workspacePath, inputSpec, agentSourceDir, assets.AgentBuilderDockerfile, baseImage)
if err != nil {
return "", fmt.Errorf("failed to build image %s: %w", img, err)
}
Expand Down Expand Up @@ -138,13 +148,16 @@ func findImage(ctx context.Context, client *dockerclient.Client, img string) (bo
return false, nil
}

func buildImage(ctx context.Context, client *dockerclient.Client, img string, workspacePath string, agentSourceDir string, dockerFile []byte, deleteBuildFolders bool, baseImage string) error {
func buildImage(ctx context.Context, client *dockerclient.Client, img string, workspacePath string, inputSpec internal.AgentSpec, agentSourceDir string, dockerFile []byte, baseImage string) error {
log := zerolog.Ctx(ctx)
log.Info().Str("image", img).Msg("building image")

if err := os.WriteFile(path.Join(workspacePath, "Dockerfile"), dockerFile, util.OwnerCanReadWrite); err != nil {
return fmt.Errorf("failed to write dockerfile to temporary workspace dir for building image: %w", err)
}
if err := os.WriteFile(path.Join(workspacePath, "start_agws.sh"), assets.StartAGWSScript, util.OwnerCanReadWrite); err != nil {
return fmt.Errorf("failed to write dockerfile to temporary workspace dir for building image: %w", err)
}

imageBuildContext, err := containerclient.CreateBuildContext(workspacePath)
if err != nil {
Expand All @@ -162,6 +175,23 @@ func buildImage(ctx context.Context, client *dockerclient.Client, img string, wo
"BASE_IMAGE": &baseImage,
}

srcDeployment := inputSpec.Manifest.Deployment.DeploymentOptions[inputSpec.SelectedDeploymentOption].SourceCodeDeployment
if srcDeployment.FrameworkConfig.LangGraphConfig != nil {

buildArgs["AGENT_FRAMEWORK"] = &srcDeployment.FrameworkConfig.LangGraphConfig.FrameworkType
graph := srcDeployment.FrameworkConfig.LangGraphConfig.Graph
buildArgs["AGENT_OBJECT"] = &graph

} else if srcDeployment.FrameworkConfig.LlamaIndexConfig != nil {

buildArgs["AGENT_FRAMEWORK"] = &srcDeployment.FrameworkConfig.LlamaIndexConfig.FrameworkType
path := srcDeployment.FrameworkConfig.LlamaIndexConfig.Path
buildArgs["AGENT_OBJECT"] = &path

} else {
return fmt.Errorf("unsupported framework config")
}

buildResp, err := client.ImageBuild(ctx, imageBuildContext, types.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{img},
Expand Down
41 changes: 3 additions & 38 deletions wfsm/internal/platforms/docker_compose/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/cisco-eti/wfsm/internal"
containerClient "github.com/cisco-eti/wfsm/internal/container_client"
"github.com/cisco-eti/wfsm/internal/util"
"github.com/cisco-eti/wfsm/manifests"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/flags"
Expand Down Expand Up @@ -190,32 +189,13 @@ func (r *runner) getMainAgentPublicPort(ctx context.Context, cli *dockerClient.C

func (r *runner) createServiceConfig(projectName string, deploymentSpec internal.AgentDeploymentBuildSpec) (*types.ServiceConfig, error) {

agentID := deploymentSpec.AgentID
apiKey := deploymentSpec.ApiKey

manifestPath := "/opt/storage/manifest.json"
envVars := deploymentSpec.EnvVars
envVars["AGENT_MANIFEST_PATH"] = manifestPath

envVars["API_HOST"] = APIHost
envVars["API_PORT"] = APIPort
envVars["API_KEY"] = apiKey

srcDeployment := deploymentSpec.Manifest.Deployment.DeploymentOptions[deploymentSpec.SelectedDeploymentOption].SourceCodeDeployment
if srcDeployment.FrameworkConfig.LangGraphConfig != nil {

envVars["AGENT_FRAMEWORK"] = "langgraph"
graph := srcDeployment.FrameworkConfig.LangGraphConfig.Graph
envVars["AGENTS_REF"] = fmt.Sprintf(`{"%s": "%s"}`, agentID, graph)

} else if srcDeployment.FrameworkConfig.LlamaIndexConfig != nil {

envVars["AGENT_FRAMEWORK"] = "llamaindex"
path := srcDeployment.FrameworkConfig.LlamaIndexConfig.Path
envVars["AGENTS_REF"] = fmt.Sprintf(`{"%s": "%s"}`, agentID, path)

} else {
return nil, fmt.Errorf("unsupported framework config")
}
envVars["API_KEY"] = deploymentSpec.ApiKey
envVars["AGENT_ID"] = deploymentSpec.AgentID

agDeploymentFolder := path.Join(r.hostStorageFolder, deploymentSpec.DeploymentName)
// make sure the folder exists
Expand All @@ -225,27 +205,12 @@ func (r *runner) createServiceConfig(projectName string, deploymentSpec internal
}
}

envVars["AGWS_STORAGE_FILE"] = path.Join("/opt/storage", fmt.Sprintf("agws_storage.pkl"))

manifestFileBuf, err := manifests.NewNullableAgentManifest(&deploymentSpec.Manifest).MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal agent manifest: %v", err)
}

if r.hostStorageFolder != "" {
err = os.WriteFile(path.Join(agDeploymentFolder, "manifest.json"), manifestFileBuf, util.OwnerCanReadWrite)
if err != nil {
return nil, fmt.Errorf("failed to write manifest to temporary workspace dir: %v", err)
}
}

sc := types.ServiceConfig{
Name: deploymentSpec.ServiceName,
Labels: map[string]string{
api.ProjectLabel: projectName,
api.OneoffLabel: "False",
api.ServiceLabel: deploymentSpec.ServiceName,
ManifestCheckSum: util.CalculateCheckSum(manifestFileBuf),
},
//ContainerName: serviceName,
Image: deploymentSpec.Image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@ name: test-agent-A
services:
test-agent-a-service:
environment:
AGENT_FRAMEWORK: langgraph
AGENT_MANIFEST_PATH: /opt/storage/manifest.json
AGENTS_REF: '{"d8084dc6-52c4-4316-8460-8f43b64db17a": "agentA.graph"}'
AGWS_STORAGE_FILE: /opt/storage/agws_storage.pkl
AGENT_ID: "d8084dc6-52c4-4316-8460-8f43b64db17a"
API_HOST: 0.0.0.0
API_KEY: 4a69e02d-b03a-47e4-99ab-f0782be35f62
API_PORT: "8000"
Expand All @@ -18,7 +15,6 @@ services:
com.docker.compose.oneoff: "False"
com.docker.compose.project: test-agent-A
com.docker.compose.service: test-agent-a-service
org.agntcy.wfsm.manifest: 564b6d6e97132d6fcae6e051be4bb5e8e44b375634dbaf28e9af5721ff243d35
ports:
- host_ip: 0.0.0.0
mode: ingress
Expand All @@ -31,10 +27,7 @@ services:
type: bind
test-agent-b-service:
environment:
AGENT_FRAMEWORK: langgraph
AGENT_MANIFEST_PATH: /opt/storage/manifest.json
AGENTS_REF: '{"39c8d1ab-d155-440c-aa4c-7b2d244d1c09": "agentB.graph"}'
AGWS_STORAGE_FILE: /opt/storage/agws_storage.pkl
AGENT_ID: "39c8d1ab-d155-440c-aa4c-7b2d244d1c09"
API_HOST: 0.0.0.0
API_KEY: 657425ba-fc18-4a6d-9144-14e6a79fdcf4
API_PORT: "8000"
Expand All @@ -44,7 +37,6 @@ services:
com.docker.compose.oneoff: "False"
com.docker.compose.project: test-agent-A
com.docker.compose.service: test-agent-b-service
org.agntcy.wfsm.manifest: 03c11ba7287f0e661c950946cf9afa66e7b1418e147dabeba72fc38080b8beed
volumes:
- source: .wfsm/test-agent-B
target: /opt/storage
Expand Down
6 changes: 0 additions & 6 deletions wfsm/internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"net"
"os/user"
"runtime"

"github.com/opencontainers/go-digest"
)

const OwnerCanReadWrite = 0777
Expand Down Expand Up @@ -41,7 +39,3 @@ func GetHomeDir() (string, error) {
}
return usr.HomeDir, nil
}

func CalculateCheckSum(data []byte) string {
return digest.SHA256.FromBytes(data).Encoded()
}
31 changes: 9 additions & 22 deletions wfsm/internal/wfsm/manifest/agent_spec_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func (a *AgentSpecBuilder) BuildAgentSpec(ctx context.Context, manifestPath stri
return fmt.Errorf("manifest validation failed: %s", err)
}

selectedDeploymentOptionIdx, err := manifestSvc.GetDeploymentOptionIdx(selectedDeploymentOption)
if err != nil {
return err
}

manifest := manifestSvc.GetManifest()
if deploymentName == "" {
deploymentName = manifest.Metadata.Ref.Name
Expand All @@ -54,18 +59,16 @@ func (a *AgentSpecBuilder) BuildAgentSpec(ctx context.Context, manifestPath stri
return fmt.Errorf("agent deployment name must be unique: %s", deploymentName)
}

selectedDeploymentOptionIdx := 0
if selectedDeploymentOption != nil {
selectedDeploymentOptionIdx = getSelectedDeploymentOptionIdx(manifest.Deployment.DeploymentOptions, *selectedDeploymentOption)
}
agentID := uuid.NewString()
apiKey := uuid.New().String()

agentSpec := internal.AgentSpec{
DeploymentName: deploymentName,
Manifest: manifest,
SelectedDeploymentOption: selectedDeploymentOptionIdx,
EnvVars: envVarValues.Values,
AgentID: uuid.NewString(),
ApiKey: uuid.NewString(),
AgentID: agentID,
ApiKey: apiKey,
}
a.AgentSpecs[deploymentName] = agentSpec

Expand Down Expand Up @@ -154,22 +157,6 @@ func mergeDepEnvVarValues(dest []manifests.EnvVarValues, src []manifests.EnvVarV
return dest
}

func getSelectedDeploymentOptionIdx(options []manifests.AgentDeploymentDeploymentOptionsInner, option string) int {
for i, opt := range options {
if opt.SourceCodeDeployment != nil &&
opt.SourceCodeDeployment.Name != nil &&
*opt.SourceCodeDeployment.Name == option {
return i
}
if opt.DockerDeployment != nil &&
opt.DockerDeployment.Name != nil &&
*opt.DockerDeployment.Name == option {
return i
}
}
return 0
}

func LoadEnvVars(envFilePath string) (manifests.EnvVarValues, error) {
file, err := os.Open(envFilePath)
if err != nil {
Expand Down
21 changes: 20 additions & 1 deletion wfsm/internal/wfsm/manifest/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

type ManifestService interface {
Validate() error
GetDeploymentOptionIdx(option *string) (int, error)
GetManifest() manifests.AgentManifest
}

Expand All @@ -40,7 +41,6 @@ func (m manifestService) Validate() error {
if m.manifest.Metadata.Ref.Version == "" {
return errors.New("invalid agent manifest: no version found in manifest")
}
//TODO what elso to validate here?
return m.ValidateDeploymentOptions()
}

Expand All @@ -55,6 +55,25 @@ func (m manifestService) ValidateDeploymentOptions() error {
return nil
}

func (m manifestService) GetDeploymentOptionIdx(option *string) (int, error) {
if option == nil || len(*option) == 0 {
return 0, nil
}
for i, opt := range m.manifest.Deployment.DeploymentOptions {
if opt.SourceCodeDeployment != nil &&
opt.SourceCodeDeployment.Name != nil &&
*opt.SourceCodeDeployment.Name == *option {
return i, nil
}
if opt.DockerDeployment != nil &&
opt.DockerDeployment.Name != nil &&
*opt.DockerDeployment.Name == *option {
return i, nil
}
}
return 0, fmt.Errorf("invalid agent manifest: deployment option %s not found", *option)
}

func loadManifest(filePath string) (manifests.AgentManifest, error) {
file, err := os.Open(filePath)
if err != nil {
Expand Down
Loading
Loading