Spaces:
Build error
Build error
| // Package agent provides gRPC server implementation for the Agent service | |
| package agent | |
| import ( | |
| "context" | |
| "crypto/rand" | |
| "encoding/hex" | |
| "sync" | |
| "time" | |
| ragv1 "github.com/AmaniQuery/amaniquery/pkg/proto/gen/ragv1" | |
| "go.uber.org/zap" | |
| "google.golang.org/grpc" | |
| "google.golang.org/grpc/codes" | |
| "google.golang.org/grpc/status" | |
| ) | |
| // Server implements the AgentService gRPC server | |
| type Server struct { | |
| ragv1.UnimplementedAgentServiceServer | |
| orchestrator *Orchestrator | |
| logger *zap.Logger | |
| mu sync.RWMutex | |
| } | |
| // NewServer creates a new Agent gRPC server | |
| func NewServer(deps *Dependencies, logger *zap.Logger) *Server { | |
| return &Server{ | |
| orchestrator: NewOrchestrator(deps, logger), | |
| logger: logger, | |
| } | |
| } | |
| // ProcessQuery handles a single query via gRPC (unary RPC) | |
| func (s *Server) ProcessQuery(ctx context.Context, req *ragv1.QueryRequest) (*ragv1.QueryResponse, error) { | |
| if req == nil || req.Query == "" { | |
| return nil, status.Error(codes.InvalidArgument, "query is required") | |
| } | |
| s.logger.Info("processing query", | |
| zap.String("query", req.Query), | |
| zap.String("session_id", req.SessionId), | |
| zap.String("user_id", req.UserId), | |
| ) | |
| // Convert proto request to internal type | |
| internalReq := s.protoToQueryRequest(req) | |
| // Process query through orchestrator | |
| response, err := s.orchestrator.ProcessQuery(ctx, internalReq) | |
| if err != nil { | |
| s.logger.Error("query processing failed", | |
| zap.Error(err), | |
| zap.String("query", req.Query), | |
| ) | |
| return nil, status.Error(codes.Internal, "query processing failed: "+err.Error()) | |
| } | |
| // Convert internal response to proto | |
| return s.queryResponseToProto(response), nil | |
| } | |
| // ProcessQueryStream handles a query with streaming response via gRPC (server streaming RPC) | |
| func (s *Server) ProcessQueryStream(req *ragv1.QueryRequest, stream ragv1.AgentService_ProcessQueryStreamServer) error { | |
| if req == nil || req.Query == "" { | |
| return status.Error(codes.InvalidArgument, "query is required") | |
| } | |
| s.logger.Info("processing streaming query", | |
| zap.String("query", req.Query), | |
| zap.String("session_id", req.SessionId), | |
| ) | |
| // Convert proto request to internal type | |
| internalReq := s.protoToQueryRequest(req) | |
| // Create a wrapper that implements ResponseStream | |
| streamWrapper := &grpcStreamWrapper{ | |
| stream: stream, | |
| logger: s.logger, | |
| } | |
| return s.orchestrator.ProcessQueryStream(stream.Context(), internalReq, streamWrapper) | |
| } | |
| // CreateAgent creates a new agent with specific configuration | |
| func (s *Server) CreateAgent(ctx context.Context, req *ragv1.CreateAgentRequest) (*ragv1.Agent, error) { | |
| if req == nil || req.Name == "" { | |
| return nil, status.Error(codes.InvalidArgument, "agent name is required") | |
| } | |
| s.logger.Info("creating agent", zap.String("name", req.Name)) | |
| // TODO: Implement agent creation logic with persistence | |
| // For now, return a mock agent | |
| return &ragv1.Agent{ | |
| Id: generateAgentID(), | |
| Name: req.Name, | |
| Description: req.Description, | |
| CreatedAt: currentTimestamp(), | |
| Config: req.Config, | |
| }, nil | |
| } | |
| // ExecutePlan executes a pre-defined execution plan | |
| func (s *Server) ExecutePlan(ctx context.Context, req *ragv1.ExecutionPlan) (*ragv1.PlanResult, error) { | |
| if req == nil || req.Id == "" { | |
| return nil, status.Error(codes.InvalidArgument, "plan ID is required") | |
| } | |
| s.logger.Info("executing plan", | |
| zap.String("plan_id", req.Id), | |
| zap.Int("steps", len(req.Steps)), | |
| ) | |
| // TODO: Implement multi-step plan execution | |
| return &ragv1.PlanResult{ | |
| PlanId: req.Id, | |
| Status: ragv1.ExecutionStatus_EXECUTION_STATUS_COMPLETED, | |
| StepResults: make([]*ragv1.StepResult, 0), | |
| FinalAnswer: "Plan execution not yet implemented", | |
| ExecutionTimeMs: 0, | |
| }, nil | |
| } | |
| // GetQueryStatus returns the status of an ongoing query | |
| func (s *Server) GetQueryStatus(ctx context.Context, req *ragv1.QueryStatusRequest) (*ragv1.QueryStatus, error) { | |
| if req == nil || req.QueryId == "" { | |
| return nil, status.Error(codes.InvalidArgument, "query_id is required") | |
| } | |
| // TODO: Implement query status tracking | |
| return &ragv1.QueryStatus{ | |
| QueryId: req.QueryId, | |
| Status: ragv1.ExecutionStatus_EXECUTION_STATUS_COMPLETED, | |
| Progress: 100, | |
| CurrentStep: "completed", | |
| }, nil | |
| } | |
| // RegisterTool registers a tool with the orchestrator | |
| func (s *Server) RegisterTool(name string, tool Tool) error { | |
| return s.orchestrator.RegisterTool(name, tool) | |
| } | |
| // grpcStreamWrapper wraps gRPC stream to implement ResponseStream interface | |
| type grpcStreamWrapper struct { | |
| stream ragv1.AgentService_ProcessQueryStreamServer | |
| logger *zap.Logger | |
| } | |
| // Send implements ResponseStream.Send by converting internal chunk to protobuf | |
| func (w *grpcStreamWrapper) Send(chunk *ResponseChunk) error { | |
| w.logger.Debug("sending stream chunk", | |
| zap.String("type", chunk.Type), | |
| zap.Bool("is_final", chunk.IsFinal), | |
| zap.Int("content_length", len(chunk.Content)), | |
| ) | |
| // Convert chunk type string to proto enum | |
| chunkType := w.stringToChunkType(chunk.Type) | |
| // Convert sources to proto format | |
| protoSources := make([]*ragv1.Source, len(chunk.Sources)) | |
| for i, src := range chunk.Sources { | |
| protoSources[i] = &ragv1.Source{ | |
| Id: src.ID, | |
| Title: src.Title, | |
| Content: src.Content, | |
| Url: src.URL, | |
| Score: src.Score, | |
| Metadata: src.Metadata, | |
| Location: src.Location, | |
| } | |
| } | |
| // Create proto chunk and send | |
| protoChunk := &ragv1.QueryResponseChunk{ | |
| Type: chunkType, | |
| Content: chunk.Content, | |
| Sources: protoSources, | |
| IsFinal: chunk.IsFinal, | |
| } | |
| return w.stream.Send(protoChunk) | |
| } | |
| // stringToChunkType converts string chunk type to proto enum | |
| func (w *grpcStreamWrapper) stringToChunkType(t string) ragv1.ChunkType { | |
| switch t { | |
| case "thinking": | |
| return ragv1.ChunkType_CHUNK_TYPE_THINKING | |
| case "retrieval": | |
| return ragv1.ChunkType_CHUNK_TYPE_RETRIEVAL | |
| case "generation": | |
| return ragv1.ChunkType_CHUNK_TYPE_GENERATION | |
| case "complete": | |
| return ragv1.ChunkType_CHUNK_TYPE_COMPLETE | |
| case "error": | |
| return ragv1.ChunkType_CHUNK_TYPE_ERROR | |
| default: | |
| return ragv1.ChunkType_CHUNK_TYPE_UNSPECIFIED | |
| } | |
| } | |
| // protoToQueryRequest converts proto QueryRequest to internal type | |
| func (s *Server) protoToQueryRequest(req *ragv1.QueryRequest) *QueryRequest { | |
| // Convert conversation history | |
| history := make([]Message, len(req.ConversationHistory)) | |
| for i, msg := range req.ConversationHistory { | |
| history[i] = Message{ | |
| Role: s.protoRoleToString(msg.Role), | |
| Content: msg.Content, | |
| } | |
| } | |
| // Set default options if not provided | |
| maxSources := int(req.Options.GetMaxSources()) | |
| if maxSources == 0 { | |
| maxSources = 10 | |
| } | |
| maxTokens := int(req.Options.GetMaxTokens()) | |
| if maxTokens == 0 { | |
| maxTokens = 4096 | |
| } | |
| temperature := req.Options.GetTemperature() | |
| if temperature == 0 { | |
| temperature = 0.7 | |
| } | |
| return &QueryRequest{ | |
| Query: req.Query, | |
| SessionID: req.SessionId, | |
| UserID: req.UserId, | |
| ConversationHistory: history, | |
| Metadata: req.Metadata, | |
| Options: QueryOptions{ | |
| MaxSources: maxSources, | |
| UseCache: req.Options.GetUseCache(), | |
| EnableAgentic: req.Options.GetEnableAgentic(), | |
| KnowledgeBases: req.Options.GetKnowledgeBases(), | |
| Temperature: temperature, | |
| MaxTokens: maxTokens, | |
| }, | |
| } | |
| } | |
| // queryResponseToProto converts internal QueryResponse to proto type | |
| func (s *Server) queryResponseToProto(resp *QueryResponse) *ragv1.QueryResponse { | |
| // Convert sources | |
| protoSources := make([]*ragv1.Source, len(resp.Sources)) | |
| for i, src := range resp.Sources { | |
| protoSources[i] = &ragv1.Source{ | |
| Id: src.ID, | |
| Title: src.Title, | |
| Content: src.Content, | |
| Url: src.URL, | |
| Score: src.Score, | |
| Metadata: src.Metadata, | |
| Location: src.Location, | |
| } | |
| } | |
| return &ragv1.QueryResponse{ | |
| Answer: resp.Answer, | |
| Sources: protoSources, | |
| Confidence: resp.Confidence, | |
| Metadata: &ragv1.QueryMetadata{ | |
| ProcessingTimeMs: resp.Metadata.ProcessingTimeMs, | |
| ChunksRetrieved: int32(resp.Metadata.ChunksRetrieved), | |
| TokensUsed: int32(resp.Metadata.TokensUsed), | |
| CacheHit: resp.Metadata.CacheHit, | |
| RoutingStrategy: resp.Metadata.RoutingStrategy, | |
| TraceId: resp.Metadata.TraceID, | |
| }, | |
| FollowUpQuestions: resp.FollowUpQuestions, | |
| } | |
| } | |
| // protoRoleToString converts proto MessageRole to string | |
| func (s *Server) protoRoleToString(role ragv1.MessageRole) string { | |
| switch role { | |
| case ragv1.MessageRole_MESSAGE_ROLE_USER: | |
| return "user" | |
| case ragv1.MessageRole_MESSAGE_ROLE_ASSISTANT: | |
| return "assistant" | |
| case ragv1.MessageRole_MESSAGE_ROLE_SYSTEM: | |
| return "system" | |
| default: | |
| return "user" | |
| } | |
| } | |
| // RegisterAgentServiceServer registers the server with gRPC using generated code | |
| func RegisterAgentServiceServer(s *grpc.Server, srv *Server) { | |
| ragv1.RegisterAgentServiceServer(s, srv) | |
| } | |
| // Helper functions | |
| func generateAgentID() string { | |
| b := make([]byte, 16) | |
| rand.Read(b) | |
| return "agent_" + hex.EncodeToString(b) | |
| } | |
| func currentTimestamp() int64 { | |
| return time.Now().Unix() | |
| } | |