Files
synapse/internal/api/grpcapi/client.go
2026-06-28 03:24:26 +08:00

707 lines
18 KiB
Go

package grpcapi
import (
"context"
"fmt"
"io"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
// =============================================================================
// gRPC Client SDK for Synapse Protocol
// =============================================================================
// GRPCClient is a high-level client for Synapse gRPC services
type GRPCClient struct {
conn *grpc.ClientConn
taskClient TaskServiceClient
messageClient A2AMessageServiceClient
capsClient CapabilitiesServiceClient
groupClient GroupServiceClient
healthClient HealthServiceClient
// Configuration
apiKey string
timeout time.Duration
}
// GRPCClientConfig client configuration
type GRPCClientConfig struct {
Address string
APIKey string
Timeout time.Duration
Insecure bool // For development only
}
// NewGRPCClient creates a new gRPC client
func NewGRPCClient(config GRPCClientConfig) (*GRPCClient, error) {
if config.Timeout == 0 {
config.Timeout = 30 * time.Second
}
var opts []grpc.DialOption
if config.Insecure {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
// Add interceptor for API key
opts = append(opts, grpc.WithUnaryInterceptor(apiKeyInterceptor(config.APIKey)))
opts = append(opts, grpc.WithStreamInterceptor(apiKeyStreamInterceptor(config.APIKey)))
ctx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()
conn, err := grpc.DialContext(ctx, config.Address, opts...)
if err != nil {
return nil, fmt.Errorf("failed to connect: %w", err)
}
return &GRPCClient{
conn: conn,
taskClient: NewTaskServiceClient(conn),
messageClient: NewA2AMessageServiceClient(conn),
capsClient: NewCapabilitiesServiceClient(conn),
groupClient: NewGroupServiceClient(conn),
healthClient: NewHealthServiceClient(conn),
apiKey: config.APIKey,
timeout: config.Timeout,
}, nil
}
// Close closes the client connection
func (c *GRPCClient) Close() error {
return c.conn.Close()
}
// =============================================================================
// Interceptors
// =============================================================================
func apiKeyInterceptor(apiKey string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if apiKey != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", apiKey)
}
return invoker(ctx, method, req, reply, cc, opts...)
}
}
func apiKeyStreamInterceptor(apiKey string) grpc.StreamClientInterceptor {
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if apiKey != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "x-api-key", apiKey)
}
return streamer(ctx, desc, cc, method, opts...)
}
}
// =============================================================================
// Task Operations
// =============================================================================
// SubmitTask submits a new task
func (c *GRPCClient) SubmitTask(ctx context.Context, taskType string, payload map[string]any, opts ...TaskOption) (*SubmitTaskResponse, error) {
req := &SubmitTaskRequest{
TaskType: taskType,
Priority: "normal",
}
// Apply options
for _, opt := range opts {
opt(req)
}
// Convert payload to Any
if payload != nil {
// In real implementation, use protojson.Marshal
req.Payload = &anypb.Any{
Value: []byte(fmt.Sprintf("%v", payload)),
}
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.taskClient.SubmitTask(ctx, req)
}
// TaskOption configures task submission
type TaskOption func(*SubmitTaskRequest)
func WithExternalTaskID(id string) TaskOption {
return func(r *SubmitTaskRequest) {
r.ExternalTaskId = id
}
}
func WithPriority(priority string) TaskOption {
return func(r *SubmitTaskRequest) {
r.Priority = priority
}
}
func WithCallback(url, token string) TaskOption {
return func(r *SubmitTaskRequest) {
r.CallbackUrl = url
r.CallbackToken = token
}
}
func WithTransportMode(mode TransportMode) TaskOption {
return func(r *SubmitTaskRequest) {
r.PreferredTransport = mode
}
}
// GetTask gets task details
func (c *GRPCClient) GetTask(ctx context.Context, taskID string) (*TaskInfo, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.taskClient.GetTask(ctx, &GetTaskRequest{TaskId: taskID})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("task query failed: %s", resp.ErrorMessage)
}
return resp.Task, nil
}
// GetTaskStatus gets simplified task status
func (c *GRPCClient) GetTaskStatus(ctx context.Context, taskID string) (*TaskInfo, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.taskClient.GetTaskStatus(ctx, &GetTaskRequest{TaskId: taskID})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("status query failed: %s", resp.ErrorMessage)
}
return resp.Task, nil
}
// ListTasks lists tasks with filtering
func (c *GRPCClient) ListTasks(ctx context.Context, status string, limit, offset int) (*ListTasksResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.taskClient.ListTasks(ctx, &ListTasksRequest{
Status: status,
Limit: int32(limit),
Offset: int32(offset),
})
}
// CancelTask cancels a task
func (c *GRPCClient) CancelTask(ctx context.Context, taskID string) (*CancelTaskResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.taskClient.CancelTask(ctx, &CancelTaskRequest{TaskId: taskID})
}
// =============================================================================
// Streaming Operations
// =============================================================================
// StreamTaskStatus streams task status updates
func (c *GRPCClient) StreamTaskStatus(ctx context.Context, taskID string) (<-chan *TaskStatusUpdate, error) {
stream, err := c.taskClient.StreamTaskStatus(ctx)
if err != nil {
return nil, fmt.Errorf("failed to start stream: %w", err)
}
// Send subscription request
if err := stream.Send(&TaskStatusStreamRequest{
TaskId: taskID,
Subscribe: true,
}); err != nil {
return nil, fmt.Errorf("failed to subscribe: %w", err)
}
updateChan := make(chan *TaskStatusUpdate, 100)
go func() {
defer close(updateChan)
for {
update, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
log.Printf("[gRPC Stream] Receive error: %v", err)
return
}
updateChan <- update
}
}()
return updateChan, nil
}
// StreamMessages opens a bidirectional message stream
func (c *GRPCClient) StreamMessages(ctx context.Context) (chan<- *A2AMessage, <-chan *A2AMessage, error) {
stream, err := c.messageClient.StreamMessages(ctx)
if err != nil {
return nil, nil, fmt.Errorf("failed to start message stream: %w", err)
}
sendChan := make(chan *A2AMessage, 100)
recvChan := make(chan *A2AMessage, 100)
// Send goroutine
go func() {
for msg := range sendChan {
if err := stream.Send(msg); err != nil {
log.Printf("[gRPC Stream] Send error: %v", err)
return
}
}
}()
// Receive goroutine
go func() {
defer close(recvChan)
for {
msg, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
log.Printf("[gRPC Stream] Receive error: %v", err)
return
}
recvChan <- msg
}
}()
return sendChan, recvChan, nil
}
// =============================================================================
// Message Operations
// =============================================================================
// SendMessage sends a single Synapse message
func (c *GRPCClient) SendMessage(ctx context.Context, msg *A2AMessage) (*SendMessageResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.messageClient.SendMessage(ctx, &SendMessageRequest{Message: msg})
}
// =============================================================================
// Capabilities Operations
// =============================================================================
// GetCapabilities gets agent capabilities
func (c *GRPCClient) GetCapabilities(ctx context.Context) (*AgentCapabilities, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.capsClient.GetCapabilities(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("capabilities query failed: %s", resp.Error)
}
return resp.Capabilities, nil
}
// ListAgents lists available agents
func (c *GRPCClient) ListAgents(ctx context.Context) ([]*AgentInfo, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.capsClient.ListAgents(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("agent list failed")
}
return resp.Agents, nil
}
// =============================================================================
// Group Operations
// =============================================================================
// CreateGroup creates a new group
func (c *GRPCClient) CreateGroup(ctx context.Context, name, description, coordinatorID string, initialMembers []string) (*GroupInfo, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.CreateGroup(ctx, &CreateGroupRequest{
Name: name,
Description: description,
CoordinatorId: coordinatorID,
InitialMembers: initialMembers,
})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("group creation failed: %s", resp.Error)
}
return resp.Group, nil
}
// JoinGroup joins an existing group
func (c *GRPCClient) JoinGroup(ctx context.Context, groupID string, agent *AgentEndpoint) error {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.JoinGroup(ctx, &JoinGroupRequest{
GroupId: groupID,
Agent: agent,
})
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("join failed: %s", resp.Error)
}
return nil
}
// LeaveGroup leaves a group
func (c *GRPCClient) LeaveGroup(ctx context.Context, groupID, agentID string) error {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.LeaveGroup(ctx, &LeaveGroupRequest{
GroupId: groupID,
AgentId: agentID,
})
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("leave failed: %s", resp.Error)
}
return nil
}
// ListGroupMembers lists group members
func (c *GRPCClient) ListGroupMembers(ctx context.Context, groupID string) ([]*GroupMember, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.ListGroupMembers(ctx, &ListGroupMembersRequest{GroupId: groupID})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("member list failed")
}
return resp.Members, nil
}
// =============================================================================
// Voting Operations (Group Protocol)
// =============================================================================
// InitiateVote initiates a vote in a group
func (c *GRPCClient) InitiateVote(ctx context.Context, groupID, initiatorID, question string, options []string, threshold float64, timeoutSeconds int32) (string, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.InitiateVote(ctx, &VoteRequest{
GroupId: groupID,
InitiatorId: initiatorID,
Question: question,
Options: options,
Threshold: threshold,
TimeoutSeconds: timeoutSeconds,
})
if err != nil {
return "", err
}
if !resp.Success {
return "", fmt.Errorf("initiate vote failed: %s", resp.Error)
}
return resp.VoteId, nil
}
// SubmitVote submits a vote
func (c *GRPCClient) SubmitVote(ctx context.Context, voteID, agentID, option, reason string) error {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.SubmitVote(ctx, &SubmitVoteRequest{
VoteId: voteID,
AgentId: agentID,
Option: option,
Reason: reason,
})
if err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("submit vote failed: %s", resp.Error)
}
return nil
}
// GetVoteStatus gets the current status of a vote
func (c *GRPCClient) GetVoteStatus(ctx context.Context, voteID string) (*VoteStatusResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.groupClient.GetVoteStatus(ctx, &GetVoteStatusRequest{VoteId: voteID})
}
// WaitForVote waits for a vote to complete
func (c *GRPCClient) WaitForVote(ctx context.Context, voteID string, interval time.Duration) (*VoteStatusResponse, error) {
if interval == 0 {
interval = 2 * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
status, err := c.GetVoteStatus(ctx, voteID)
if err != nil {
continue
}
if status.Status == "decided" || status.Status == "expired" {
return status, nil
}
}
}
}
// =============================================================================
// Task Split & Merge Operations (Group Protocol)
// =============================================================================
// SplitTask splits a task into subtasks
func (c *GRPCClient) SplitTask(ctx context.Context, groupID, taskID, taskType, requirements string, strategy SplitStrategy) ([]*SubtaskInfo, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
resp, err := c.groupClient.SplitTask(ctx, &SplitTaskRequest{
GroupId: groupID,
TaskId: taskID,
TaskType: taskType,
Requirements: requirements,
Strategy: strategy,
})
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("split task failed: %s", resp.Error)
}
return resp.Subtasks, nil
}
// MergeResults merges subtask results
func (c *GRPCClient) MergeResults(ctx context.Context, groupID, parentTaskID string, strategy MergeStrategy) (*MergeResultsResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.groupClient.MergeResults(ctx, &MergeResultsRequest{
GroupId: groupID,
ParentTaskId: parentTaskID,
Strategy: strategy,
})
}
// =============================================================================
// Health Operations
// =============================================================================
// HealthCheck checks server health
func (c *GRPCClient) HealthCheck(ctx context.Context) (*HealthCheckResponse, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return c.healthClient.HealthCheck(ctx, &emptypb.Empty{})
}
// =============================================================================
// High-Level Helpers
// =============================================================================
// CreateAndWait creates a task and waits for completion via streaming
func (c *GRPCClient) CreateAndWait(ctx context.Context, taskType string, payload map[string]any, timeout time.Duration) (*TaskInfo, error) {
// Submit task
resp, err := c.SubmitTask(ctx, taskType, payload)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("task submission failed: %s", resp.Message)
}
// Stream status until completion
updateChan, err := c.StreamTaskStatus(ctx, resp.TaskId)
if err != nil {
return nil, err
}
// Wait with timeout
if timeout == 0 {
timeout = 5 * time.Minute
}
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case update := <-updateChan:
if update == nil {
return nil, fmt.Errorf("stream closed unexpectedly")
}
switch update.Status {
case TaskStatus_TASK_STATUS_COMPLETED:
return &TaskInfo{
Id: resp.TaskId,
Status: update.Status,
Progress: update.Progress,
Message: update.Message,
}, nil
case TaskStatus_TASK_STATUS_FAILED, TaskStatus_TASK_STATUS_CANCELLED:
return nil, fmt.Errorf("task failed: %s", update.Message)
}
case <-timer.C:
return nil, fmt.Errorf("timeout waiting for task completion")
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// WaitForTask waits for a task to complete using polling
func (c *GRPCClient) WaitForTask(ctx context.Context, taskID string, interval time.Duration) (*TaskInfo, error) {
if interval == 0 {
interval = 5 * time.Second
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
task, err := c.GetTaskStatus(ctx, taskID)
if err != nil {
continue
}
switch task.Status {
case TaskStatus_TASK_STATUS_COMPLETED:
return task, nil
case TaskStatus_TASK_STATUS_FAILED, TaskStatus_TASK_STATUS_CANCELLED:
return task, fmt.Errorf("task failed: %s", task.Message)
}
}
}
}
// =============================================================================
// Builder Pattern for A2AMessage
// =============================================================================
// MessageBuilder builds Synapse messages
type MessageBuilder struct {
msg *A2AMessage
}
// NewMessage creates a new message builder
func NewMessage() *MessageBuilder {
return &MessageBuilder{
msg: &A2AMessage{
Id: generateMessageID(),
Version: "1.0.0",
Timestamp: timestamppb.Now(),
},
}
}
func (b *MessageBuilder) Type(msgType MessageType) *MessageBuilder {
b.msg.Type = msgType
return b
}
func (b *MessageBuilder) From(sender *AgentEndpoint) *MessageBuilder {
b.msg.Sender = sender
return b
}
func (b *MessageBuilder) To(receiver *AgentEndpoint) *MessageBuilder {
b.msg.Receiver = receiver
return b
}
func (b *MessageBuilder) CorrelationID(id string) *MessageBuilder {
b.msg.CorrelationId = id
return b
}
func (b *MessageBuilder) GroupID(id string) *MessageBuilder {
b.msg.GroupId = id
return b
}
func (b *MessageBuilder) Payload(payload []byte) *MessageBuilder {
b.msg.Payload = &anypb.Any{Value: payload}
return b
}
func (b *MessageBuilder) Build() *A2AMessage {
return b.msg
}
func generateMessageID() string {
return fmt.Sprintf("msg-%d", time.Now().UnixNano())
}