diff --git a/CHANGELOG.md b/CHANGELOG.md index 591f8d9..146eac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # changelog +## v0.2.4 + +- move card resolver to client/card package (#33) +- Rename `WithOptions` to `WithHeaders` in card_resolver (#33) +- Improved the error message for `GetCard()` in card_resolver (#33) + ## v0.2.3 -fix: add nil check for message in MessageSend and MessageSendStream to prevent panic(#20) +- fix: add nil check for message in MessageSend and MessageSendStream to prevent panic(#20) ## v0.2.2 1. Ensured final events are always enqueued and processed (#17) diff --git a/sdk/client/card/card_resolver.go b/sdk/client/card/card_resolver.go new file mode 100644 index 0000000..29dd617 --- /dev/null +++ b/sdk/client/card/card_resolver.go @@ -0,0 +1,121 @@ +// Copyright 2025 yeeaiclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package card + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + log "github.com/yeeaiclub/a2a-go/internal/logger" + "github.com/yeeaiclub/a2a-go/sdk/types" +) + +// A2ACardResolver is responsible for retrieving agent card information from a specified endpoint. +// It provides a way to fetch [types.AgentCard] data using HTTP requests with configurable options. +type A2ACardResolver struct { + client *http.Client // HTTP client used for making requests + baseUrl string // Base URL of the agent service + agentCardPath string // Path to the agent card endpoint + options map[string]string // HTTP headers to include in requests +} + +// NewA2ACardResolver creates a new instance of A2ACardResolver with the provided configuration. +func NewA2ACardResolver(client *http.Client, baseURL string, options ...A2ACardResolverOption) *A2ACardResolver { + r := A2ACardResolver{ + client: client, + baseUrl: baseURL, + agentCardPath: types.AgentCardPath, // Default path from types package + } + + // Apply all provided configuration options + for _, opt := range options { + r = opt.Option(r) + } + return &r +} + +// GetAgentCard retrieves an agent card from the configured base URL. +// It sends an HTTP GET request to the agent card endpoint and returns the parsed card data. +func (a *A2ACardResolver) GetAgentCard(ctx context.Context) (*types.AgentCard, error) { + targetURL, err := url.JoinPath(a.baseUrl, a.agentCardPath) + if err != nil { + return nil, fmt.Errorf("failed to construct url %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to construct request %w", err) + } + + if a.options != nil { + for key, value := range a.options { + req.Header.Set(key, value) + } + } + + resp, err := a.client.Do(req) + if err != nil { + return nil, err + } + + defer func() { + if err := resp.Body.Close(); err != nil { + log.Error(ctx, "failed to close response body", err) + } + }() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get agent card: status code %d", resp.StatusCode) + } + + var card types.AgentCard + err = json.NewDecoder(resp.Body).Decode(&card) + if err != nil { + return nil, fmt.Errorf("failed to parse body: %w", err) + } + return &card, nil +} + +// A2ACardResolverOption defines an interface for configuring A2ACardResolver instances. +type A2ACardResolverOption interface { + Option(resolver A2ACardResolver) A2ACardResolver +} + +type A2ACardResolverOptionFunc func(resolver A2ACardResolver) A2ACardResolver + +// Option applies the function to the resolver and returns the updated resolver. +func (fn A2ACardResolverOptionFunc) Option(resolver A2ACardResolver) A2ACardResolver { + return fn(resolver) +} + +// WithAgentCardPath configures the specific path used to retrieve the agent card. +// By default, the resolver uses the standard path defined in types.AgentCardPath. +func WithAgentCardPath(path string) A2ACardResolverOption { + return A2ACardResolverOptionFunc(func(resolver A2ACardResolver) A2ACardResolver { + resolver.agentCardPath = path + return resolver + }) +} + +// WithHeader configures custom HTTP headers to be included in the agent card request. +func WithHeader(header map[string]string) A2ACardResolverOption { + return A2ACardResolverOptionFunc(func(resolver A2ACardResolver) A2ACardResolver { + resolver.options = header + return resolver + }) +} diff --git a/sdk/client/card_resolver_test.go b/sdk/client/card/card_resolver_test.go similarity index 93% rename from sdk/client/card_resolver_test.go rename to sdk/client/card/card_resolver_test.go index 925f625..128e615 100644 --- a/sdk/client/card_resolver_test.go +++ b/sdk/client/card/card_resolver_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package client +package card import ( "encoding/json" @@ -28,11 +28,11 @@ import ( func TestGetAgentCard(t *testing.T) { testcases := []struct { name string - want types.AgentCard + want *types.AgentCard }{ { name: "success response", - want: types.AgentCard{ + want: &types.AgentCard{ Name: "hello, word", Description: "a hello word agent", DefaultInputModes: []string{"text"}, @@ -57,7 +57,7 @@ func TestGetAgentCard(t *testing.T) { http.DefaultClient, server.URL, ) - card, err := resolver.GetAgentCard() + card, err := resolver.GetAgentCard(t.Context()) require.NoError(t, err) assert.Equal(t, tc.want, card) }) diff --git a/sdk/client/card_resolver.go b/sdk/client/card_resolver.go deleted file mode 100644 index 250acd3..0000000 --- a/sdk/client/card_resolver.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2025 yeeaiclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package client - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/yeeaiclub/a2a-go/sdk/types" -) - -type A2ACardResolver struct { - client *http.Client - baseUrl string - agentCardPath string - options map[string]string -} - -type A2ACardResolverOption interface { - Option(resolver A2ACardResolver) A2ACardResolver -} - -type A2ACardResolverOptionFunc func(resolver A2ACardResolver) A2ACardResolver - -func (fn A2ACardResolverOptionFunc) Option(resolver A2ACardResolver) A2ACardResolver { - return fn(resolver) -} - -func WithAgentCardPath(agentCardPath string) A2ACardResolverOption { - return A2ACardResolverOptionFunc(func(resolver A2ACardResolver) A2ACardResolver { - resolver.agentCardPath = agentCardPath - return resolver - }) -} - -func WithOptions(options map[string]string) A2ACardResolverOption { - return A2ACardResolverOptionFunc(func(resolver A2ACardResolver) A2ACardResolver { - resolver.options = options - return resolver - }) -} - -func NewA2ACardResolver(client *http.Client, baseUrl string, options ...A2ACardResolverOption) *A2ACardResolver { - r := A2ACardResolver{ - client: client, - baseUrl: baseUrl, - agentCardPath: types.AgentCardPath, - } - - for _, opt := range options { - r = opt.Option(r) - } - return &r -} - -func (a *A2ACardResolver) GetAgentCard() (types.AgentCard, error) { - targetUrl := fmt.Sprintf("%s/%s", a.baseUrl, a.agentCardPath) - req, err := http.NewRequest(http.MethodGet, targetUrl, nil) - if err != nil { - return types.AgentCard{}, err - } - if a.options != nil { - for key, value := range a.options { - req.Header.Set(key, value) - } - } - resp, err := a.client.Do(req) - if err != nil { - return types.AgentCard{}, err - } - var card types.AgentCard - err = json.NewDecoder(resp.Body).Decode(&card) - if err != nil { - return types.AgentCard{}, err - } - return card, nil -}