diff --git a/agent/a2aagent/a2a_agent.go b/agent/a2aagent/a2a_agent.go index 28e1af70e..df133db00 100644 --- a/agent/a2aagent/a2a_agent.go +++ b/agent/a2aagent/a2a_agent.go @@ -12,10 +12,7 @@ package a2aagent import ( "context" - "encoding/json" "fmt" - "io" - "net/http" "strings" "time" @@ -24,20 +21,16 @@ import ( "trpc.group/trpc-go/trpc-a2a-go/server" "trpc.group/trpc-go/trpc-agent-go/agent" "trpc.group/trpc-go/trpc-agent-go/event" + ia2a "trpc.group/trpc-go/trpc-agent-go/internal/a2a" + "trpc.group/trpc-go/trpc-agent-go/log" "trpc.group/trpc-go/trpc-agent-go/model" "trpc.group/trpc-go/trpc-agent-go/tool" ) -var defaultStreamingChannelSize = 1024 -var defaultNonStreamingChannelSize = 10 - const ( - // AgentCardWellKnownPath is the standard path for agent card discovery - AgentCardWellKnownPath = "/.well-known/agent.json" - // defaultFetchTimeout is the default timeout for fetching agent card - defaultFetchTimeout = 30 * time.Second - // defaultUserIDHeader is the default HTTP header name to send UserID to A2A server - defaultUserIDHeader = "X-User-ID" + defaultStreamingChannelSize = 1024 + defaultNonStreamingChannelSize = 10 + defaultUserIDHeader = "X-User-ID" ) // A2AAgent is an agent that communicates with a remote A2A agent via A2A protocol. @@ -54,9 +47,9 @@ type A2AAgent struct { streamingRespHandler StreamingRespHandler // Handler for streaming responses transferStateKey []string // Keys in session state to transfer to the A2A agent message by metadata userIDHeader string // HTTP header name to send UserID to A2A server + enableStreaming *bool // Explicitly set streaming mode; nil means use agent card capability - httpClient *http.Client - a2aClient *client.A2AClient + a2aClient *client.A2AClient } // New creates a new A2AAgent. @@ -71,74 +64,60 @@ func New(opts ...Option) (*A2AAgent, error) { opt(agent) } - if agent.agentURL != "" && agent.agentCard == nil { - agentCard, err := agent.resolveAgentCardFromURL() - if err != nil { - return nil, fmt.Errorf("failed to resolve agent card: %w", err) - } - agent.agentCard = agentCard + var agentURL string + if agent.agentCard != nil { + agentURL = agent.agentCard.URL + } else if agent.agentURL != "" { + agentURL = agent.agentURL + } else { + log.Info("agent card or agent card url not set") } - if agent.agentCard == nil { - return nil, fmt.Errorf("agent card not set") - } + // Normalize the URL to ensure it has a proper scheme + agentURL = ia2a.NormalizeURL(agentURL) - a2aClient, err := client.NewA2AClient(agent.agentCard.URL, agent.extraA2AOptions...) + // Create A2A client first + a2aClient, err := client.NewA2AClient(agentURL, agent.extraA2AOptions...) if err != nil { - return nil, fmt.Errorf("failed to create A2A client for %s: %w", agent.agentCard.URL, err) + return nil, fmt.Errorf("failed to create A2A client for %s: %w", agentURL, err) } agent.a2aClient = a2aClient - return agent, nil -} - -// resolveAgentCardFromURL fetches agent card from the well-known path -func (r *A2AAgent) resolveAgentCardFromURL() (*server.AgentCard, error) { - agentURL := r.agentURL - // Construct the agent card discovery URL - agentCardURL := strings.TrimSuffix(agentURL, "/") + AgentCardWellKnownPath - - // Create HTTP client if not set - httpClient := r.httpClient - if httpClient == nil { - httpClient = &http.Client{Timeout: defaultFetchTimeout} - } - - // Fetch agent card from well-known path - resp, err := httpClient.Get(agentCardURL) - if err != nil { - return nil, fmt.Errorf("failed to fetch agent card from %s: %w", agentCardURL, err) - } - defer resp.Body.Close() + // If agent card is not set, fetch it using A2A client's GetAgentCard method + if agent.agentCard == nil { + agentCard, err := a2aClient.GetAgentCard(context.Background(), "") + if err != nil { + return nil, fmt.Errorf("failed to fetch agent card from %s: %w", agentURL, err) + } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch agent card from %s: HTTP %d", agentCardURL, resp.StatusCode) - } + // Set name and description from agent card if not already set + if agent.name == "" { + agent.name = agentCard.Name + } + if agent.description == "" { + agent.description = agentCard.Description + } - // Read response body - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read agent card response: %w", err) - } + if agentCard.URL == "" { + agentCard.URL = agentURL + } else { + // Normalize the agent card URL to ensure it has a proper scheme + agentCard.URL = ia2a.NormalizeURL(agentCard.URL) + } - // Parse agent card JSON - var agentCard server.AgentCard - if err := json.Unmarshal(body, &agentCard); err != nil { - return nil, fmt.Errorf("failed to parse agent card JSON: %w", err) - } + // Rebuild a2a client if URL changed + if agentCard.URL != agentURL { + a2aClient, err := client.NewA2AClient(agentCard.URL, agent.extraA2AOptions...) + if err != nil { + return nil, fmt.Errorf("failed to create A2A client for %s: %w", agentCard.URL, err) + } + agent.a2aClient = a2aClient + } - if r.name == "" { - r.name = agentCard.Name + agent.agentCard = agentCard } - if r.description == "" { - r.description = agentCard.Description - } - // If URL is not set in the agent card, use the provided agent URL. - if agentCard.URL == "" { - agentCard.URL = agentURL - } - return &agentCard, nil + return agent, nil } // sendErrorEvent sends an error event to the event channel @@ -189,7 +168,12 @@ func (r *A2AAgent) Run(ctx context.Context, invocation *agent.Invocation) (<-cha // shouldUseStreaming determines whether to use streaming protocol func (r *A2AAgent) shouldUseStreaming() bool { - // Check if agent card supports streaming + // If explicitly set via option, use that value + if r.enableStreaming != nil { + return *r.enableStreaming + } + + // Otherwise check if agent card supports streaming if r.agentCard != nil && r.agentCard.Capabilities.Streaming != nil { return *r.agentCard.Capabilities.Streaming } diff --git a/agent/a2aagent/a2a_agent_option.go b/agent/a2aagent/a2a_agent_option.go index 3f3e6943f..5ea4a52d8 100644 --- a/agent/a2aagent/a2a_agent_option.go +++ b/agent/a2aagent/a2a_agent_option.go @@ -105,3 +105,12 @@ func WithUserIDHeader(header string) Option { } } } + +// WithEnableStreaming explicitly controls whether to use streaming protocol. +// If not set (nil), the agent will use the streaming capability from the agent card. +// This option overrides the agent card's capability setting. +func WithEnableStreaming(enable bool) Option { + return func(a *A2AAgent) { + a.enableStreaming = &enable + } +} diff --git a/agent/a2aagent/a2a_agent_test.go b/agent/a2aagent/a2a_agent_test.go index 39932b306..d5503e2cb 100644 --- a/agent/a2aagent/a2a_agent_test.go +++ b/agent/a2aagent/a2a_agent_test.go @@ -43,7 +43,7 @@ func TestNew(t *testing.T) { opts: []Option{}, setupFunc: func(tc *testCase) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == AgentCardWellKnownPath { + if r.URL.Path == "/.well-known/agent-card.json" { agentCard := server.AgentCard{ Name: "test-agent", Description: "A test agent", @@ -154,7 +154,7 @@ func TestNew(t *testing.T) { }, setupFunc: func(tc *testCase) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == AgentCardWellKnownPath { + if r.URL.Path == "/.well-known/agent-card.json" { agentCard := server.AgentCard{ Name: "test-agent", Description: "Test agent", @@ -531,142 +531,6 @@ func TestA2AAgent_buildA2AMessage(t *testing.T) { } } -func TestA2AAgent_resolveAgentCardFromURL(t *testing.T) { - type testCase struct { - name string - agent *A2AAgent - setupFunc func(tc *testCase) *httptest.Server - validateFunc func(t *testing.T, agentCard *server.AgentCard, err error) - } - - tests := []testCase{ - { - name: "success with valid agent card", - agent: &A2AAgent{}, - setupFunc: func(tc *testCase) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == AgentCardWellKnownPath { - agentCard := server.AgentCard{ - Name: "resolved-agent", - Description: "Resolved from URL", - URL: "http://resolved.com", - } - json.NewEncoder(w).Encode(agentCard) - return - } - w.WriteHeader(http.StatusNotFound) - })) - tc.agent.agentURL = server.URL - return server - }, - validateFunc: func(t *testing.T, agentCard *server.AgentCard, err error) { - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if agentCard == nil { - t.Fatal("expected agent card, got nil") - } - if agentCard.Name != "resolved-agent" { - t.Errorf("expected name 'resolved-agent', got %s", agentCard.Name) - } - if agentCard.Description != "Resolved from URL" { - t.Errorf("expected description 'Resolved from URL', got %s", agentCard.Description) - } - }, - }, - { - name: "fills agent name and description when empty", - agent: &A2AAgent{ - name: "", - description: "", - }, - setupFunc: func(tc *testCase) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - agentCard := server.AgentCard{ - Name: "auto-filled", - Description: "Auto-filled description", - } - json.NewEncoder(w).Encode(agentCard) - })) - tc.agent.agentURL = server.URL - return server - }, - validateFunc: func(t *testing.T, agentCard *server.AgentCard, err error) { - if err != nil { - t.Errorf("expected no error, got %v", err) - } - if agentCard.Name != "auto-filled" { - t.Errorf("expected name 'auto-filled', got %s", agentCard.Name) - } - }, - }, - { - name: "error when HTTP request fails", - agent: &A2AAgent{agentURL: "http://nonexistent.local"}, - setupFunc: func(tc *testCase) *httptest.Server { - return nil - }, - validateFunc: func(t *testing.T, agentCard *server.AgentCard, err error) { - if err == nil { - t.Error("expected error when HTTP request fails") - } - if agentCard != nil { - t.Error("expected agent card to be nil on error") - } - }, - }, - { - name: "error when HTTP status not OK", - agent: &A2AAgent{}, - setupFunc: func(tc *testCase) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - tc.agent.agentURL = server.URL - return server - }, - validateFunc: func(t *testing.T, agentCard *server.AgentCard, err error) { - if err == nil { - t.Error("expected error when HTTP status not OK") - } - if agentCard != nil { - t.Error("expected agent card to be nil on error") - } - }, - }, - { - name: "error when invalid JSON", - agent: &A2AAgent{}, - setupFunc: func(tc *testCase) *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("invalid json")) - })) - tc.agent.agentURL = server.URL - return server - }, - validateFunc: func(t *testing.T, agentCard *server.AgentCard, err error) { - if err == nil { - t.Error("expected error when JSON is invalid") - } - if agentCard != nil { - t.Error("expected agent card to be nil on error") - } - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - server := tc.setupFunc(&tc) - if server != nil { - defer server.Close() - } - agentCard, err := tc.agent.resolveAgentCardFromURL() - tc.validateFunc(t, agentCard, err) - }) - } -} - func TestA2AAgent_Run_ErrorCases(t *testing.T) { type testCase struct { name string @@ -880,7 +744,7 @@ func TestA2ARequestOptions(t *testing.T) { t.Run("validates option types and returns error for invalid types", func(t *testing.T) { // Create test server testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == AgentCardWellKnownPath { + if r.URL.Path == "/.well-known/agent-card.json" { agentCard := server.AgentCard{ Name: "test-agent", Description: "A test agent", @@ -1023,7 +887,7 @@ func TestUserIDHeaderInRequest(t *testing.T) { // Create mock A2A server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == AgentCardWellKnownPath { + if r.URL.Path == "/.well-known/agent-card.json" { // Return agent card with the mock server's URL agentCard := server.AgentCard{ Name: "test-agent", diff --git a/codeexecutor/container/go.mod b/codeexecutor/container/go.mod index 58b4b5c45..1ae694ecd 100644 --- a/codeexecutor/container/go.mod +++ b/codeexecutor/container/go.mod @@ -48,5 +48,5 @@ require ( golang.org/x/sys v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/codeexecutor/container/go.sum b/codeexecutor/container/go.sum index a199da001..c060b100a 100644 --- a/codeexecutor/container/go.sum +++ b/codeexecutor/container/go.sum @@ -134,5 +134,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/event/tags_test.go b/event/tags_test.go index 4db0de847..873fef14d 100644 --- a/event/tags_test.go +++ b/event/tags_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "trpc.group/trpc-go/trpc-agent-go/model" ) // TestWithTag_Single verifies that a single tag is applied without a leading delimiter. @@ -31,16 +30,3 @@ func TestWithTag_Multiple(t *testing.T) { require.Equal(t, "alpha"+TagDelimiter+"beta"+TagDelimiter+"gamma", e.Tag) } - -// TestIsRunnerCompletion verifies the helper correctly identifies runner completion events. -func TestIsRunnerCompletion(t *testing.T) { - // Negative cases - require.False(t, (*Event)(nil).IsRunnerCompletion()) - - e := &Event{Response: &model.Response{Object: model.ObjectTypeChatCompletion, Done: false}} - require.False(t, e.IsRunnerCompletion()) - - // Positive case - done := &Event{Response: &model.Response{Object: model.ObjectTypeRunnerCompletion, Done: true}} - require.True(t, done.IsRunnerCompletion()) -} diff --git a/examples/agui/go.mod b/examples/agui/go.mod index 7ca1cf478..703dc9419 100644 --- a/examples/agui/go.mod +++ b/examples/agui/go.mod @@ -45,5 +45,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240822170219-fc7c04adadcd // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/examples/agui/go.sum b/examples/agui/go.sum index 4652adde4..6dcb5510a 100644 --- a/examples/agui/go.sum +++ b/examples/agui/go.sum @@ -16,6 +16,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/panjf2000/ants/v2 v2.9.0 h1:SztCLkVxBRigbg+vt0S5QvF5vxAbxbKt09/YfAJ0tEo= @@ -89,5 +91,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333 h1:Als3R0+WZSm+bkDVkt5ATElgRixuGRY7iBSEJXBq2XM= -trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/examples/go.mod b/examples/go.mod index 5195707d4..a87b1ab15 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -20,7 +20,7 @@ require ( go.opentelemetry.io/otel/metric v1.29.0 go.opentelemetry.io/otel/trace v1.29.0 go.uber.org/zap v1.27.0 - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a trpc.group/trpc-go/trpc-agent-go v0.2.0 trpc.group/trpc-go/trpc-agent-go/memory/redis v0.2.0 trpc.group/trpc-go/trpc-agent-go/session/redis v0.2.0 diff --git a/examples/go.sum b/examples/go.sum index 532f92355..77d0cbd17 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -174,7 +174,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= trpc.group/trpc-go/trpc-mcp-go v0.0.5 h1:sokuTLv73DYVrsylmCVrUCigLg2WVPvSxz4BUpzESRU= trpc.group/trpc-go/trpc-mcp-go v0.0.5/go.mod h1:q9nAeg9aALr8S38fAPSd+wQVkFpmPW/1r2JGtZAM2ts= diff --git a/examples/knowledge/go.mod b/examples/knowledge/go.mod index 5f8330b12..978bcab39 100644 --- a/examples/knowledge/go.mod +++ b/examples/knowledge/go.mod @@ -85,7 +85,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect google.golang.org/grpc v1.73.0 // indirect google.golang.org/protobuf v1.36.6 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect trpc.group/trpc-go/trpc-agent-go/storage/elasticsearch v0.2.0 // indirect trpc.group/trpc-go/trpc-agent-go/storage/tcvector v0.0.4 // indirect ) diff --git a/examples/knowledge/go.sum b/examples/knowledge/go.sum index c9233b5fa..59f0a6e13 100644 --- a/examples/knowledge/go.sum +++ b/examples/knowledge/go.sum @@ -109,6 +109,8 @@ github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 h1:QwWKgMY28TAXaDl+ github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -289,8 +291,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= -trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333 h1:Als3R0+WZSm+bkDVkt5ATElgRixuGRY7iBSEJXBq2XM= -trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= trpc.group/trpc-go/trpc-agent-go/storage/elasticsearch v0.2.0 h1:TJkM9rYp3GFcyvGWY1lW+g+ABuWzHlvpBbXd0U6iKK0= trpc.group/trpc-go/trpc-agent-go/storage/elasticsearch v0.2.0/go.mod h1:efPOimKMIx1BmLZ9qkpwoZyiSQ62yM/6eMzWocLD7Y0= trpc.group/trpc-go/trpc-agent-go/storage/tcvector v0.0.4 h1:yV87tUOnsmUS7zFy9rEgzdxUfSgcUr6QO5ICyIIoPDY= diff --git a/examples/tailor/go.mod b/examples/tailor/go.mod index 245e602ad..7cc9136fd 100644 --- a/examples/tailor/go.mod +++ b/examples/tailor/go.mod @@ -22,5 +22,5 @@ require ( github.com/tiktoken-go/tokenizer v0.7.0 // indirect go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/examples/tailor/go.sum b/examples/tailor/go.sum index 8e0bc064a..c332ea4aa 100644 --- a/examples/tailor/go.sum +++ b/examples/tailor/go.sum @@ -28,5 +28,5 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333 h1:Als3R0+WZSm+bkDVkt5ATElgRixuGRY7iBSEJXBq2XM= -trpc.group/trpc-go/trpc-a2a-go v0.2.4-0.20250904070130-981d83483333/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/go.mod b/go.mod index f5bb4c5d8..28787888f 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/mattn/go-sqlite3 v1.14.32 github.com/openai/openai-go v1.12.0 - github.com/panjf2000/ants/v2 v2.10.0 + github.com/panjf2000/ants/v2 v2.9.0 github.com/rs/cors v1.11.1 github.com/spaolacci/murmur3 v1.1.0 github.com/stretchr/testify v1.10.0 @@ -34,7 +34,7 @@ require ( golang.org/x/text v0.21.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a trpc.group/trpc-go/trpc-mcp-go v0.0.5 ) diff --git a/go.sum b/go.sum index ded38b6e7..b80888939 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSr github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0= github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= -github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8= -github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= +github.com/panjf2000/ants/v2 v2.9.0 h1:SztCLkVxBRigbg+vt0S5QvF5vxAbxbKt09/YfAJ0tEo= +github.com/panjf2000/ants/v2 v2.9.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -187,7 +187,7 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= trpc.group/trpc-go/trpc-mcp-go v0.0.5 h1:sokuTLv73DYVrsylmCVrUCigLg2WVPvSxz4BUpzESRU= trpc.group/trpc-go/trpc-mcp-go v0.0.5/go.mod h1:q9nAeg9aALr8S38fAPSd+wQVkFpmPW/1r2JGtZAM2ts= diff --git a/internal/a2a/url.go b/internal/a2a/url.go new file mode 100644 index 000000000..c1cb99c87 --- /dev/null +++ b/internal/a2a/url.go @@ -0,0 +1,37 @@ +// +// Tencent is pleased to support the open source community by making trpc-agent-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-agent-go is licensed under the Apache License Version 2.0. +// +// + +// Package a2a provides internal utilities for A2A (Agent-to-Agent) protocol. +package a2a + +import "net/url" + +// NormalizeURL ensures the URL has a scheme. +// If the input already has a scheme (e.g., http://, https://, custom://), it returns it as-is. +// Otherwise, it prepends "http://" +// +// This function is used by both A2A client and server to provide a uniform user experience. +// +// Examples: +// - "localhost:8080" → "http://localhost:8080" +// - "http://example.com" → "http://example.com" (no change) +// - "grpc://service:9090" → "grpc://service:9090" (no change) +func NormalizeURL(urlOrHost string) string { + if urlOrHost == "" { + return "" + } + // Parse the URL to check if it has a valid scheme + u, err := url.Parse(urlOrHost) + if err == nil && u.Scheme != "" && u.Host != "" { + // Has both scheme and host (e.g., http://example.com, custom://service) + return urlOrHost + } + // No valid scheme, add http:// prefix + return "http://" + urlOrHost +} diff --git a/internal/a2a/url_test.go b/internal/a2a/url_test.go new file mode 100644 index 000000000..de0252b9a --- /dev/null +++ b/internal/a2a/url_test.go @@ -0,0 +1,80 @@ +// +// Tencent is pleased to support the open source community by making trpc-agent-go available. +// +// Copyright (C) 2025 Tencent. All rights reserved. +// +// trpc-agent-go is licensed under the Apache License Version 2.0. +// +// + +package a2a + +import "testing" + +func TestNormalizeURL(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "http URL - no change", + input: "http://example.com", + expected: "http://example.com", + }, + { + name: "https URL - no change", + input: "https://example.com", + expected: "https://example.com", + }, + { + name: "custom scheme - no change", + input: "grpc://service:9090", + expected: "grpc://service:9090", + }, + { + name: "host only - add http", + input: "localhost:8080", + expected: "http://localhost:8080", + }, + { + name: "domain only - add http", + input: "example.com", + expected: "http://example.com", + }, + { + name: "IP with port - add http", + input: "192.168.1.1:8080", + expected: "http://192.168.1.1:8080", + }, + { + name: "http URL with path", + input: "http://example.com/api/v1", + expected: "http://example.com/api/v1", + }, + { + name: "host with path - add http", + input: "localhost:8080/api", + expected: "http://localhost:8080/api", + }, + { + name: "custom scheme with path", + input: "custom://service.namespace/path", + expected: "custom://service.namespace/path", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := NormalizeURL(tt.input) + if result != tt.expected { + t.Errorf("NormalizeURL(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} diff --git a/knowledge/document/reader/pdf/go.mod b/knowledge/document/reader/pdf/go.mod index 626fd6ad5..f5802afed 100644 --- a/knowledge/document/reader/pdf/go.mod +++ b/knowledge/document/reader/pdf/go.mod @@ -15,5 +15,5 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/text v0.21.0 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/knowledge/document/reader/pdf/go.sum b/knowledge/document/reader/pdf/go.sum index 881666181..cb268c50e 100644 --- a/knowledge/document/reader/pdf/go.sum +++ b/knowledge/document/reader/pdf/go.sum @@ -20,5 +20,5 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/knowledge/embedder/gemini/go.mod b/knowledge/embedder/gemini/go.mod index ad81023a7..690d6796c 100644 --- a/knowledge/embedder/gemini/go.mod +++ b/knowledge/embedder/gemini/go.mod @@ -42,5 +42,5 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/knowledge/embedder/gemini/go.sum b/knowledge/embedder/gemini/go.sum index e88ccbb99..681b1d014 100644 --- a/knowledge/embedder/gemini/go.sum +++ b/knowledge/embedder/gemini/go.sum @@ -164,5 +164,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/knowledge/vectorstore/elasticsearch/go.mod b/knowledge/vectorstore/elasticsearch/go.mod index 40af683d0..3017d50d8 100644 --- a/knowledge/vectorstore/elasticsearch/go.mod +++ b/knowledge/vectorstore/elasticsearch/go.mod @@ -29,5 +29,5 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/knowledge/vectorstore/elasticsearch/go.sum b/knowledge/vectorstore/elasticsearch/go.sum index 72eed3dc0..a57b72820 100644 --- a/knowledge/vectorstore/elasticsearch/go.sum +++ b/knowledge/vectorstore/elasticsearch/go.sum @@ -50,5 +50,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/knowledge/vectorstore/pgvector/go.mod b/knowledge/vectorstore/pgvector/go.mod index 822de4797..90e4ce0b4 100644 --- a/knowledge/vectorstore/pgvector/go.mod +++ b/knowledge/vectorstore/pgvector/go.mod @@ -23,5 +23,5 @@ require ( golang.org/x/sync v0.12.0 // indirect golang.org/x/text v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/knowledge/vectorstore/pgvector/go.sum b/knowledge/vectorstore/pgvector/go.sum index fb67d751f..5d4a4ba62 100644 --- a/knowledge/vectorstore/pgvector/go.sum +++ b/knowledge/vectorstore/pgvector/go.sum @@ -84,5 +84,5 @@ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/knowledge/vectorstore/tcvector/go.mod b/knowledge/vectorstore/tcvector/go.mod index c7b15862c..65b77c35c 100644 --- a/knowledge/vectorstore/tcvector/go.mod +++ b/knowledge/vectorstore/tcvector/go.mod @@ -35,5 +35,5 @@ require ( google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/knowledge/vectorstore/tcvector/go.sum b/knowledge/vectorstore/tcvector/go.sum index b55032cb7..e030ebcf8 100644 --- a/knowledge/vectorstore/tcvector/go.sum +++ b/knowledge/vectorstore/tcvector/go.sum @@ -63,5 +63,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/memory/redis/go.mod b/memory/redis/go.mod index 26593cac5..517691cc5 100644 --- a/memory/redis/go.mod +++ b/memory/redis/go.mod @@ -25,5 +25,5 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/memory/redis/go.sum b/memory/redis/go.sum index 692fb18d9..2a8fa6c70 100644 --- a/memory/redis/go.sum +++ b/memory/redis/go.sum @@ -30,5 +30,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= diff --git a/server/a2a/server.go b/server/a2a/server.go index 26bea7ceb..ab595f033 100644 --- a/server/a2a/server.go +++ b/server/a2a/server.go @@ -15,6 +15,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "time" "trpc.group/trpc-go/trpc-a2a-go/auth" @@ -23,6 +24,7 @@ import ( "trpc.group/trpc-go/trpc-a2a-go/taskmanager" "trpc.group/trpc-go/trpc-agent-go/agent" "trpc.group/trpc-go/trpc-agent-go/event" + ia2a "trpc.group/trpc-go/trpc-agent-go/internal/a2a" "trpc.group/trpc-go/trpc-agent-go/log" "trpc.group/trpc-go/trpc-agent-go/model" "trpc.group/trpc-go/trpc-agent-go/runner" @@ -47,8 +49,10 @@ func New(opts ...Option) (*a2a.A2AServer, error) { return nil, errors.New("agent is required") } - if options.host == "" { - return nil, errors.New("host is required") + // Host is only required if we need to build an agent card + // If user provides a custom agent card, host is optional + if options.agentCard == nil && options.host == "" { + return nil, errors.New("host is required when agent card is not provided") } return buildA2AServer(options) @@ -61,7 +65,9 @@ func buildAgentCard(options *options) a2a.AgentCard { agent := options.agent desc := agent.Info().Description name := agent.Info().Name - url := fmt.Sprintf("http://%s", options.host) + + // Normalize the host to ensure it has a proper URL scheme + url := ia2a.NormalizeURL(options.host) // Build skills from agent tools skills := buildSkillsFromTools(agent, name, desc) @@ -137,11 +143,15 @@ func buildA2AServer(options *options) (*a2a.A2AServer, error) { userIDHeader = serverUserIDHeader } + // Extract base path from agent card URL for request routing. + // If the URL contains a path component (e.g., "http://example.com/api/v1"), + // it will be extracted and used as the base path for routing incoming requests. + basePath := extractBasePath(ia2a.NormalizeURL(agentCard.URL)) + opts := []a2a.Option{ a2a.WithAuthProvider(&defaultAuthProvider{userIDHeader: userIDHeader}), + a2a.WithBasePath(basePath), } - - // if other AuthProvider is set, user info should be covered opts = append(opts, options.extraOptions...) a2aServer, err := a2a.NewA2AServer(agentCard, taskManager, opts...) if err != nil { @@ -150,6 +160,36 @@ func buildA2AServer(options *options) (*a2a.A2AServer, error) { return a2aServer, nil } +// extractBasePath extracts the path component from a URL for request routing. +// It parses the URL and returns the path if the URL has a valid scheme. +// +// Examples: +// - "http://example.com/api/v1" → "/api/v1" +// - "https://example.com/docs" → "/docs" +// - "grpc://service:9090/rpc" → "/rpc" +// - "http://example.com" → "" (no path) +// - "invalid-url" → "" (no scheme) +// +// The extracted path is used as the base path for routing incoming A2A requests. +func extractBasePath(urlStr string) string { + if urlStr == "" { + return "" + } + + u, err := url.Parse(urlStr) + if err != nil { + return "" + } + + // Extract path if URL has a valid scheme + if u.Scheme != "" { + return u.Path + } + + // No valid scheme, return empty string + return "" +} + // messageProcessor is the message processor for the a2a server. type messageProcessor struct { runner runner.Runner diff --git a/server/a2a/server_option.go b/server/a2a/server_option.go index 5058d1651..ae554ff32 100644 --- a/server/a2a/server_option.go +++ b/server/a2a/server_option.go @@ -133,7 +133,27 @@ func WithProcessMessageHook(hook ProcessMessageHook) Option { } } -// WithHost sets the host to use. +// WithHost sets the host address for the A2A server's agent card URL. +// The host will be normalized to a complete URL and used by other agents to discover and communicate with this agent. +// +// Supported formats: +// - "localhost:8080" → "http://localhost:8080" +// - "example.com" → "http://example.com" +// - "http://example.com/api/v1" → "http://example.com/api/v1" (used as-is) +// - "https://example.com" → "https://example.com" (used as-is) +// - "grpc://service:9090" → "grpc://service:9090" (custom schemes supported) +// +// If the URL contains a path (e.g., "http://example.com/api/v1"), the path will be +// automatically extracted and set as the base path for routing requests. +// +// Example: +// +// server, _ := a2a.New( +// a2a.WithAgent(myAgent), +// a2a.WithHost("localhost:8080"), // URL: "http://localhost:8080", basePath: "" +// // or +// a2a.WithHost("http://example.com/api/v1"), // URL: "http://example.com/api/v1", basePath: "/api/v1" +// ) func WithHost(host string) Option { return func(opts *options) { opts.host = host diff --git a/server/a2a/server_test.go b/server/a2a/server_test.go index 24ba77651..d7a3213d6 100644 --- a/server/a2a/server_test.go +++ b/server/a2a/server_test.go @@ -23,6 +23,7 @@ import ( "trpc.group/trpc-go/trpc-a2a-go/taskmanager" "trpc.group/trpc-go/trpc-agent-go/agent" "trpc.group/trpc-go/trpc-agent-go/event" + ia2a "trpc.group/trpc-go/trpc-agent-go/internal/a2a" "trpc.group/trpc-go/trpc-agent-go/model" "trpc.group/trpc-go/trpc-agent-go/session" "trpc.group/trpc-go/trpc-agent-go/session/inmemory" @@ -1387,13 +1388,25 @@ func TestNew(t *testing.T) { errMsg: "agent is required", }, { - name: "missing host with empty host", + name: "missing host without agent card", opts: []Option{ WithAgent(&mockAgent{name: "test-agent", description: "test description"}, true), WithHost(""), }, wantErr: true, - errMsg: "host is required", + errMsg: "host is required when agent card is not provided", + }, + { + name: "with agent card but no host - should succeed", + opts: []Option{ + WithAgent(&mockAgent{name: "test-agent", description: "test description"}, true), + WithAgentCard(a2a.AgentCard{ + Name: "custom-agent", + Description: "custom description", + URL: "http://custom.example.com", + }), + }, + wantErr: false, }, { name: "no options", @@ -1434,7 +1447,7 @@ func TestBuildAgentCard(t *testing.T) { expected a2a.AgentCard }{ { - name: "agent with no tools", + name: "agent with no tools - host only", options: &options{ agent: &mockAgent{ name: "test-agent", @@ -1464,6 +1477,99 @@ func TestBuildAgentCard(t *testing.T) { DefaultOutputModes: []string{"text"}, }, }, + { + name: "agent with http URL", + options: &options{ + agent: &mockAgent{ + name: "test-agent", + description: "test description", + tools: []tool.Tool{}, + }, + host: "http://example.com:8080", + enableStreaming: true, + }, + expected: a2a.AgentCard{ + Name: "test-agent", + Description: "test description", + URL: "http://example.com:8080", + Capabilities: a2a.AgentCapabilities{ + Streaming: boolPtr(true), + }, + Skills: []a2a.AgentSkill{ + { + Name: "test-agent", + Description: stringPtr("test description"), + InputModes: []string{"text"}, + OutputModes: []string{"text"}, + Tags: []string{"default"}, + }, + }, + DefaultInputModes: []string{"text"}, + DefaultOutputModes: []string{"text"}, + }, + }, + { + name: "agent with https URL", + options: &options{ + agent: &mockAgent{ + name: "test-agent", + description: "test description", + tools: []tool.Tool{}, + }, + host: "https://secure.example.com", + enableStreaming: true, + }, + expected: a2a.AgentCard{ + Name: "test-agent", + Description: "test description", + URL: "https://secure.example.com", + Capabilities: a2a.AgentCapabilities{ + Streaming: boolPtr(true), + }, + Skills: []a2a.AgentSkill{ + { + Name: "test-agent", + Description: stringPtr("test description"), + InputModes: []string{"text"}, + OutputModes: []string{"text"}, + Tags: []string{"default"}, + }, + }, + DefaultInputModes: []string{"text"}, + DefaultOutputModes: []string{"text"}, + }, + }, + { + name: "agent with custom URL", + options: &options{ + agent: &mockAgent{ + name: "custom-agent", + description: "agent with custom scheme", + tools: []tool.Tool{}, + }, + host: "custom://service.namespace", + enableStreaming: true, + }, + expected: a2a.AgentCard{ + Name: "custom-agent", + Description: "agent with custom scheme", + URL: "custom://service.namespace", + Capabilities: a2a.AgentCapabilities{ + Streaming: boolPtr(true), + }, + Skills: []a2a.AgentSkill{ + { + Name: "custom-agent", + Description: stringPtr("agent with custom scheme"), + InputModes: []string{"text"}, + OutputModes: []string{"text"}, + Tags: []string{"default"}, + }, + }, + DefaultInputModes: []string{"text"}, + DefaultOutputModes: []string{"text"}, + }, + }, { name: "agent with tools", options: &options{ @@ -1835,3 +1941,154 @@ func compareStringSlices(a, b []string) bool { } return true } + +func TestNormalizeURL(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "already has http scheme", + input: "http://example.com", + expected: "http://example.com", + }, + { + name: "already has https scheme", + input: "https://example.com", + expected: "https://example.com", + }, + { + name: "custom scheme", + input: "custom://service.namespace", + expected: "custom://service.namespace", + }, + { + name: "custom scheme", + input: "grpc://example.com:9090", + expected: "grpc://example.com:9090", + }, + { + name: "host only - simple domain", + input: "example.com", + expected: "http://example.com", + }, + { + name: "host only - with port", + input: "localhost:8080", + expected: "http://localhost:8080", + }, + { + name: "host only - IP address", + input: "192.168.1.1", + expected: "http://192.168.1.1", + }, + { + name: "host only - IP with port", + input: "127.0.0.1:9999", + expected: "http://127.0.0.1:9999", + }, + { + name: "complete URL with path", + input: "http://example.com/api/v1", + expected: "http://example.com/api/v1", + }, + { + name: "https URL with path and query", + input: "https://example.com/api?key=value", + expected: "https://example.com/api?key=value", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ia2a.NormalizeURL(tt.input) + if result != tt.expected { + t.Errorf("NormalizeURL(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestExtractBasePath(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "http with path", + input: "http://example.com/api/v1", + expected: "/api/v1", + }, + { + name: "https with path", + input: "https://example.com/api/v1/agents", + expected: "/api/v1/agents", + }, + { + name: "http without path", + input: "http://example.com", + expected: "", + }, + { + name: "https without path", + input: "https://example.com:8080", + expected: "", + }, + { + name: "http with root path", + input: "http://example.com/", + expected: "/", + }, + { + name: "custom scheme with path - should return path", + input: "grpc://example.com:9090/service", + expected: "/service", + }, + { + name: "custom scheme without path - should return empty", + input: "custom://service.namespace", + expected: "", + }, + { + name: "http with path and query", + input: "http://example.com/api?key=value", + expected: "/api", + }, + { + name: "https with path and fragment", + input: "https://example.com/docs#section", + expected: "/docs", + }, + { + name: "invalid URL - no scheme", + input: "://invalid", + expected: "", + }, + { + name: "grpc with complex path", + input: "grpc://service:9090/api/v1/rpc", + expected: "/api/v1/rpc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractBasePath(tt.input) + if result != tt.expected { + t.Errorf("extractBasePath(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} diff --git a/session/redis/go.mod b/session/redis/go.mod index 9d40f218d..c5bb4dc99 100644 --- a/session/redis/go.mod +++ b/session/redis/go.mod @@ -26,5 +26,5 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.27.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 // indirect + trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a // indirect ) diff --git a/session/redis/go.sum b/session/redis/go.sum index face1c654..fda57b276 100644 --- a/session/redis/go.sum +++ b/session/redis/go.sum @@ -32,5 +32,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1 h1:P+OyPh+QCNuO8u+M2UPTYZCGKnH9YAcijC8ULokAdTw= -trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251020094851-6ab922c9dab1/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a h1:dOon6HF2sPRFnhCLEiAeKPc21JHL2eX7UBWjIR8PLaY= +trpc.group/trpc-go/trpc-a2a-go v0.2.5-0.20251023030722-7f02b57fd14a/go.mod h1:Gtytau9Uoc3oPo/dpHvKit+tQn9Qlk5XFG1RiZTGqfk=