From 13ef1df10956d3a36b801813988165b378df642a Mon Sep 17 00:00:00 2001 From: "Billy D." Date: Sat, 21 Feb 2026 14:58:05 -0500 Subject: [PATCH] feat!: replace msgpack with protobuf for all NATS messages BREAKING CHANGE: All NATS message serialization now uses Protocol Buffers. - Added proto/messages/v1/messages.proto with 22 message types - Generated Go code at gen/messagespb/ - messages/ package now exports type aliases to proto types - natsutil.Publish/Request/Decode use proto.Marshal/Unmarshal - Removed legacy MessageHandler, OnMessage, wrapMapHandler - TypedMessageHandler now returns (proto.Message, error) - EffectiveQuery is now a free function: messages.EffectiveQuery(req) - Removed msgpack dependency entirely --- buf.gen.yaml | 10 + buf.yaml | 9 + gen/messagespb/messages.pb.go | 2011 ++++++++++++++++++++++++++++++ go.mod | 6 +- go.sum | 4 - handler/handler.go | 298 ++--- handler/handler_test.go | 420 +++---- messages/bench_test.go | 659 ++++------ messages/messages.go | 261 +--- natsutil/natsutil.go | 184 ++- natsutil/natsutil_test.go | 248 ++-- proto/messages/v1/messages.proto | 257 ++++ 12 files changed, 3074 insertions(+), 1293 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 gen/messagespb/messages.pb.go create mode 100644 proto/messages/v1/messages.proto diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..71ebc71 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,10 @@ +version: v2 +managed: + enabled: true + override: + - file_option: go_package_prefix + value: git.daviestechlabs.io/daviestechlabs/handler-base/gen +plugins: + - protoc_builtin: go + out: gen + opt: paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..c7e30e3 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: proto +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/gen/messagespb/messages.pb.go b/gen/messagespb/messages.pb.go new file mode 100644 index 0000000..ed5affd --- /dev/null +++ b/gen/messagespb/messages.pb.go @@ -0,0 +1,2011 @@ +// Homelab AI service message contracts. +// +// This is the single source of truth for all NATS message types. +// Generated Go code lives in handler-base/gen/messagespb. +// +// Naming: field numbers are stable across versions — add new fields, +// never reuse or renumber existing ones. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.30.2 +// source: messages/v1/messages.proto + +package messagespb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ErrorResponse is the standard error reply from any handler. +type ErrorResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Error bool `protobuf:"varint,1,opt,name=error,proto3" json:"error,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ErrorResponse) Reset() { + *x = ErrorResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ErrorResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ErrorResponse) ProtoMessage() {} + +func (x *ErrorResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ErrorResponse.ProtoReflect.Descriptor instead. +func (*ErrorResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{0} +} + +func (x *ErrorResponse) GetError() bool { + if x != nil { + return x.Error + } + return false +} + +func (x *ErrorResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ErrorResponse) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +// LoginEvent is published when a user authenticates. +// Subject: ai.chat.user.{user_id}.login +type LoginEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3" json:"nickname,omitempty"` + Premium bool `protobuf:"varint,4,opt,name=premium,proto3" json:"premium,omitempty"` + Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Unix seconds + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LoginEvent) Reset() { + *x = LoginEvent{} + mi := &file_messages_v1_messages_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LoginEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LoginEvent) ProtoMessage() {} + +func (x *LoginEvent) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LoginEvent.ProtoReflect.Descriptor instead. +func (*LoginEvent) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{1} +} + +func (x *LoginEvent) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *LoginEvent) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *LoginEvent) GetNickname() string { + if x != nil { + return x.Nickname + } + return "" +} + +func (x *LoginEvent) GetPremium() bool { + if x != nil { + return x.Premium + } + return false +} + +func (x *LoginEvent) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// GreetingRequest asks the LLM to generate a personalised greeting. +// Subject: ai.chat.user.{user_id}.greeting.request +type GreetingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Nickname string `protobuf:"bytes,3,opt,name=nickname,proto3" json:"nickname,omitempty"` + Premium bool `protobuf:"varint,4,opt,name=premium,proto3" json:"premium,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GreetingRequest) Reset() { + *x = GreetingRequest{} + mi := &file_messages_v1_messages_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GreetingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GreetingRequest) ProtoMessage() {} + +func (x *GreetingRequest) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GreetingRequest.ProtoReflect.Descriptor instead. +func (*GreetingRequest) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{2} +} + +func (x *GreetingRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *GreetingRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *GreetingRequest) GetNickname() string { + if x != nil { + return x.Nickname + } + return "" +} + +func (x *GreetingRequest) GetPremium() bool { + if x != nil { + return x.Premium + } + return false +} + +// GreetingResponse carries the generated greeting text. +// Subject: ai.chat.user.{user_id}.greeting.response +type GreetingResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Greeting string `protobuf:"bytes,2,opt,name=greeting,proto3" json:"greeting,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GreetingResponse) Reset() { + *x = GreetingResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GreetingResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GreetingResponse) ProtoMessage() {} + +func (x *GreetingResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GreetingResponse.ProtoReflect.Descriptor instead. +func (*GreetingResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{3} +} + +func (x *GreetingResponse) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *GreetingResponse) GetGreeting() string { + if x != nil { + return x.Greeting + } + return "" +} + +// ChatRequest is an incoming chat message routed via NATS. +// Subject: ai.chat.user.{user_id}.message +type ChatRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + Message string `protobuf:"bytes,4,opt,name=message,proto3" json:"message,omitempty"` + Query string `protobuf:"bytes,5,opt,name=query,proto3" json:"query,omitempty"` // alternative to message (EffectiveQuery picks first non-empty) + Premium bool `protobuf:"varint,6,opt,name=premium,proto3" json:"premium,omitempty"` + EnableRag bool `protobuf:"varint,7,opt,name=enable_rag,json=enableRag,proto3" json:"enable_rag,omitempty"` + EnableReranker bool `protobuf:"varint,8,opt,name=enable_reranker,json=enableReranker,proto3" json:"enable_reranker,omitempty"` + EnableStreaming bool `protobuf:"varint,9,opt,name=enable_streaming,json=enableStreaming,proto3" json:"enable_streaming,omitempty"` + TopK int32 `protobuf:"varint,10,opt,name=top_k,json=topK,proto3" json:"top_k,omitempty"` + Collection string `protobuf:"bytes,11,opt,name=collection,proto3" json:"collection,omitempty"` + EnableTts bool `protobuf:"varint,12,opt,name=enable_tts,json=enableTts,proto3" json:"enable_tts,omitempty"` + SystemPrompt string `protobuf:"bytes,13,opt,name=system_prompt,json=systemPrompt,proto3" json:"system_prompt,omitempty"` + ResponseSubject string `protobuf:"bytes,14,opt,name=response_subject,json=responseSubject,proto3" json:"response_subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatRequest) Reset() { + *x = ChatRequest{} + mi := &file_messages_v1_messages_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatRequest) ProtoMessage() {} + +func (x *ChatRequest) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChatRequest.ProtoReflect.Descriptor instead. +func (*ChatRequest) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{4} +} + +func (x *ChatRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ChatRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ChatRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *ChatRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ChatRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *ChatRequest) GetPremium() bool { + if x != nil { + return x.Premium + } + return false +} + +func (x *ChatRequest) GetEnableRag() bool { + if x != nil { + return x.EnableRag + } + return false +} + +func (x *ChatRequest) GetEnableReranker() bool { + if x != nil { + return x.EnableReranker + } + return false +} + +func (x *ChatRequest) GetEnableStreaming() bool { + if x != nil { + return x.EnableStreaming + } + return false +} + +func (x *ChatRequest) GetTopK() int32 { + if x != nil { + return x.TopK + } + return 0 +} + +func (x *ChatRequest) GetCollection() string { + if x != nil { + return x.Collection + } + return "" +} + +func (x *ChatRequest) GetEnableTts() bool { + if x != nil { + return x.EnableTts + } + return false +} + +func (x *ChatRequest) GetSystemPrompt() string { + if x != nil { + return x.SystemPrompt + } + return "" +} + +func (x *ChatRequest) GetResponseSubject() string { + if x != nil { + return x.ResponseSubject + } + return "" +} + +// ChatResponse is the full reply to a ChatRequest. +// Subject: ai.chat.response.{request_id} (or ChatRequest.response_subject) +type ChatResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Response string `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"` + ResponseText string `protobuf:"bytes,3,opt,name=response_text,json=responseText,proto3" json:"response_text,omitempty"` + UsedRag bool `protobuf:"varint,4,opt,name=used_rag,json=usedRag,proto3" json:"used_rag,omitempty"` + RagSources []string `protobuf:"bytes,5,rep,name=rag_sources,json=ragSources,proto3" json:"rag_sources,omitempty"` + Success bool `protobuf:"varint,6,opt,name=success,proto3" json:"success,omitempty"` + Audio []byte `protobuf:"bytes,7,opt,name=audio,proto3" json:"audio,omitempty"` + Error string `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatResponse) Reset() { + *x = ChatResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatResponse) ProtoMessage() {} + +func (x *ChatResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChatResponse.ProtoReflect.Descriptor instead. +func (*ChatResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{5} +} + +func (x *ChatResponse) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *ChatResponse) GetResponse() string { + if x != nil { + return x.Response + } + return "" +} + +func (x *ChatResponse) GetResponseText() string { + if x != nil { + return x.ResponseText + } + return "" +} + +func (x *ChatResponse) GetUsedRag() bool { + if x != nil { + return x.UsedRag + } + return false +} + +func (x *ChatResponse) GetRagSources() []string { + if x != nil { + return x.RagSources + } + return nil +} + +func (x *ChatResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *ChatResponse) GetAudio() []byte { + if x != nil { + return x.Audio + } + return nil +} + +func (x *ChatResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// ChatStreamChunk is one piece of a streaming LLM response. +// Subject: ai.chat.response.stream.{request_id} +type ChatStreamChunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // "chunk" | "done" + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + Done bool `protobuf:"varint,4,opt,name=done,proto3" json:"done,omitempty"` + Timestamp int64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ChatStreamChunk) Reset() { + *x = ChatStreamChunk{} + mi := &file_messages_v1_messages_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ChatStreamChunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChatStreamChunk) ProtoMessage() {} + +func (x *ChatStreamChunk) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChatStreamChunk.ProtoReflect.Descriptor instead. +func (*ChatStreamChunk) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{6} +} + +func (x *ChatStreamChunk) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *ChatStreamChunk) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *ChatStreamChunk) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *ChatStreamChunk) GetDone() bool { + if x != nil { + return x.Done + } + return false +} + +func (x *ChatStreamChunk) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// VoiceRequest is an incoming voice-to-voice request. +// Subject: ai.voice.request +type VoiceRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Audio []byte `protobuf:"bytes,2,opt,name=audio,proto3" json:"audio,omitempty"` + Language string `protobuf:"bytes,3,opt,name=language,proto3" json:"language,omitempty"` + Collection string `protobuf:"bytes,4,opt,name=collection,proto3" json:"collection,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoiceRequest) Reset() { + *x = VoiceRequest{} + mi := &file_messages_v1_messages_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoiceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoiceRequest) ProtoMessage() {} + +func (x *VoiceRequest) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoiceRequest.ProtoReflect.Descriptor instead. +func (*VoiceRequest) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{7} +} + +func (x *VoiceRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *VoiceRequest) GetAudio() []byte { + if x != nil { + return x.Audio + } + return nil +} + +func (x *VoiceRequest) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +func (x *VoiceRequest) GetCollection() string { + if x != nil { + return x.Collection + } + return "" +} + +// DocumentSource is a single RAG search-result citation. +type DocumentSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + Score float64 `protobuf:"fixed64,2,opt,name=score,proto3" json:"score,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DocumentSource) Reset() { + *x = DocumentSource{} + mi := &file_messages_v1_messages_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DocumentSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DocumentSource) ProtoMessage() {} + +func (x *DocumentSource) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DocumentSource.ProtoReflect.Descriptor instead. +func (*DocumentSource) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{8} +} + +func (x *DocumentSource) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *DocumentSource) GetScore() float64 { + if x != nil { + return x.Score + } + return 0 +} + +// VoiceResponse is the reply to a VoiceRequest. +// Subject: ai.voice.response.{request_id} +type VoiceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Response string `protobuf:"bytes,2,opt,name=response,proto3" json:"response,omitempty"` + Audio []byte `protobuf:"bytes,3,opt,name=audio,proto3" json:"audio,omitempty"` + Transcription string `protobuf:"bytes,4,opt,name=transcription,proto3" json:"transcription,omitempty"` + Sources []*DocumentSource `protobuf:"bytes,5,rep,name=sources,proto3" json:"sources,omitempty"` + Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoiceResponse) Reset() { + *x = VoiceResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoiceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoiceResponse) ProtoMessage() {} + +func (x *VoiceResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoiceResponse.ProtoReflect.Descriptor instead. +func (*VoiceResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{9} +} + +func (x *VoiceResponse) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *VoiceResponse) GetResponse() string { + if x != nil { + return x.Response + } + return "" +} + +func (x *VoiceResponse) GetAudio() []byte { + if x != nil { + return x.Audio + } + return nil +} + +func (x *VoiceResponse) GetTranscription() string { + if x != nil { + return x.Transcription + } + return "" +} + +func (x *VoiceResponse) GetSources() []*DocumentSource { + if x != nil { + return x.Sources + } + return nil +} + +func (x *VoiceResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// TTSRequest is a text-to-speech synthesis request. +// Subject: ai.voice.tts.request.{session_id} +type TTSRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + Speaker string `protobuf:"bytes,2,opt,name=speaker,proto3" json:"speaker,omitempty"` + Language string `protobuf:"bytes,3,opt,name=language,proto3" json:"language,omitempty"` + SpeakerWavB64 string `protobuf:"bytes,4,opt,name=speaker_wav_b64,json=speakerWavB64,proto3" json:"speaker_wav_b64,omitempty"` + Stream bool `protobuf:"varint,5,opt,name=stream,proto3" json:"stream,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSRequest) Reset() { + *x = TTSRequest{} + mi := &file_messages_v1_messages_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSRequest) ProtoMessage() {} + +func (x *TTSRequest) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSRequest.ProtoReflect.Descriptor instead. +func (*TTSRequest) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{10} +} + +func (x *TTSRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *TTSRequest) GetSpeaker() string { + if x != nil { + return x.Speaker + } + return "" +} + +func (x *TTSRequest) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +func (x *TTSRequest) GetSpeakerWavB64() string { + if x != nil { + return x.SpeakerWavB64 + } + return "" +} + +func (x *TTSRequest) GetStream() bool { + if x != nil { + return x.Stream + } + return false +} + +// TTSAudioChunk is a streamed audio chunk from TTS synthesis. +// Subject: ai.voice.tts.audio.{session_id} +type TTSAudioChunk struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + ChunkIndex int32 `protobuf:"varint,2,opt,name=chunk_index,json=chunkIndex,proto3" json:"chunk_index,omitempty"` + TotalChunks int32 `protobuf:"varint,3,opt,name=total_chunks,json=totalChunks,proto3" json:"total_chunks,omitempty"` + Audio []byte `protobuf:"bytes,4,opt,name=audio,proto3" json:"audio,omitempty"` + IsLast bool `protobuf:"varint,5,opt,name=is_last,json=isLast,proto3" json:"is_last,omitempty"` + Timestamp int64 `protobuf:"varint,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + SampleRate int32 `protobuf:"varint,7,opt,name=sample_rate,json=sampleRate,proto3" json:"sample_rate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSAudioChunk) Reset() { + *x = TTSAudioChunk{} + mi := &file_messages_v1_messages_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSAudioChunk) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSAudioChunk) ProtoMessage() {} + +func (x *TTSAudioChunk) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSAudioChunk.ProtoReflect.Descriptor instead. +func (*TTSAudioChunk) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{11} +} + +func (x *TTSAudioChunk) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TTSAudioChunk) GetChunkIndex() int32 { + if x != nil { + return x.ChunkIndex + } + return 0 +} + +func (x *TTSAudioChunk) GetTotalChunks() int32 { + if x != nil { + return x.TotalChunks + } + return 0 +} + +func (x *TTSAudioChunk) GetAudio() []byte { + if x != nil { + return x.Audio + } + return nil +} + +func (x *TTSAudioChunk) GetIsLast() bool { + if x != nil { + return x.IsLast + } + return false +} + +func (x *TTSAudioChunk) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *TTSAudioChunk) GetSampleRate() int32 { + if x != nil { + return x.SampleRate + } + return 0 +} + +// TTSFullResponse is a non-streamed TTS response (whole audio blob). +// Subject: ai.voice.tts.audio.{session_id} +type TTSFullResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Audio []byte `protobuf:"bytes,2,opt,name=audio,proto3" json:"audio,omitempty"` + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + SampleRate int32 `protobuf:"varint,4,opt,name=sample_rate,json=sampleRate,proto3" json:"sample_rate,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSFullResponse) Reset() { + *x = TTSFullResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSFullResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSFullResponse) ProtoMessage() {} + +func (x *TTSFullResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSFullResponse.ProtoReflect.Descriptor instead. +func (*TTSFullResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{12} +} + +func (x *TTSFullResponse) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TTSFullResponse) GetAudio() []byte { + if x != nil { + return x.Audio + } + return nil +} + +func (x *TTSFullResponse) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *TTSFullResponse) GetSampleRate() int32 { + if x != nil { + return x.SampleRate + } + return 0 +} + +// TTSStatus is a TTS processing status update. +// Subject: ai.voice.tts.status.{session_id} +type TTSStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSStatus) Reset() { + *x = TTSStatus{} + mi := &file_messages_v1_messages_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSStatus) ProtoMessage() {} + +func (x *TTSStatus) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSStatus.ProtoReflect.Descriptor instead. +func (*TTSStatus) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{13} +} + +func (x *TTSStatus) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *TTSStatus) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *TTSStatus) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *TTSStatus) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// TTSVoiceInfo is summary info about a custom voice. +type TTSVoiceInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Language string `protobuf:"bytes,2,opt,name=language,proto3" json:"language,omitempty"` + ModelType string `protobuf:"bytes,3,opt,name=model_type,json=modelType,proto3" json:"model_type,omitempty"` + CreatedAt string `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSVoiceInfo) Reset() { + *x = TTSVoiceInfo{} + mi := &file_messages_v1_messages_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSVoiceInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSVoiceInfo) ProtoMessage() {} + +func (x *TTSVoiceInfo) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSVoiceInfo.ProtoReflect.Descriptor instead. +func (*TTSVoiceInfo) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{14} +} + +func (x *TTSVoiceInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TTSVoiceInfo) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +func (x *TTSVoiceInfo) GetModelType() string { + if x != nil { + return x.ModelType + } + return "" +} + +func (x *TTSVoiceInfo) GetCreatedAt() string { + if x != nil { + return x.CreatedAt + } + return "" +} + +// TTSVoiceListResponse is the reply to a voice list request. +// Subject: ai.voice.tts.voices.list (request-reply) +type TTSVoiceListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + DefaultSpeaker string `protobuf:"bytes,1,opt,name=default_speaker,json=defaultSpeaker,proto3" json:"default_speaker,omitempty"` + CustomVoices []*TTSVoiceInfo `protobuf:"bytes,2,rep,name=custom_voices,json=customVoices,proto3" json:"custom_voices,omitempty"` + LastRefresh int64 `protobuf:"varint,3,opt,name=last_refresh,json=lastRefresh,proto3" json:"last_refresh,omitempty"` + Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSVoiceListResponse) Reset() { + *x = TTSVoiceListResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSVoiceListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSVoiceListResponse) ProtoMessage() {} + +func (x *TTSVoiceListResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSVoiceListResponse.ProtoReflect.Descriptor instead. +func (*TTSVoiceListResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{15} +} + +func (x *TTSVoiceListResponse) GetDefaultSpeaker() string { + if x != nil { + return x.DefaultSpeaker + } + return "" +} + +func (x *TTSVoiceListResponse) GetCustomVoices() []*TTSVoiceInfo { + if x != nil { + return x.CustomVoices + } + return nil +} + +func (x *TTSVoiceListResponse) GetLastRefresh() int64 { + if x != nil { + return x.LastRefresh + } + return 0 +} + +func (x *TTSVoiceListResponse) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// TTSVoiceRefreshResponse is the reply to a voice refresh request. +// Subject: ai.voice.tts.voices.refresh (request-reply) +type TTSVoiceRefreshResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Count int32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + CustomVoices []*TTSVoiceInfo `protobuf:"bytes,2,rep,name=custom_voices,json=customVoices,proto3" json:"custom_voices,omitempty"` + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TTSVoiceRefreshResponse) Reset() { + *x = TTSVoiceRefreshResponse{} + mi := &file_messages_v1_messages_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TTSVoiceRefreshResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TTSVoiceRefreshResponse) ProtoMessage() {} + +func (x *TTSVoiceRefreshResponse) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TTSVoiceRefreshResponse.ProtoReflect.Descriptor instead. +func (*TTSVoiceRefreshResponse) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{16} +} + +func (x *TTSVoiceRefreshResponse) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *TTSVoiceRefreshResponse) GetCustomVoices() []*TTSVoiceInfo { + if x != nil { + return x.CustomVoices + } + return nil +} + +func (x *TTSVoiceRefreshResponse) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// STTStreamMessage is any message on the ai.voice.stream.{session_id} subject. +type STTStreamMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` // "start" | "chunk" | "state_change" | "end" + Audio []byte `protobuf:"bytes,2,opt,name=audio,proto3" json:"audio,omitempty"` + State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` + SpeakerId string `protobuf:"bytes,4,opt,name=speaker_id,json=speakerId,proto3" json:"speaker_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STTStreamMessage) Reset() { + *x = STTStreamMessage{} + mi := &file_messages_v1_messages_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STTStreamMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STTStreamMessage) ProtoMessage() {} + +func (x *STTStreamMessage) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STTStreamMessage.ProtoReflect.Descriptor instead. +func (*STTStreamMessage) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{17} +} + +func (x *STTStreamMessage) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *STTStreamMessage) GetAudio() []byte { + if x != nil { + return x.Audio + } + return nil +} + +func (x *STTStreamMessage) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *STTStreamMessage) GetSpeakerId() string { + if x != nil { + return x.SpeakerId + } + return "" +} + +// STTTranscription is the transcription result published by the STT module. +// Subject: ai.voice.transcription.{session_id} +type STTTranscription struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Transcript string `protobuf:"bytes,2,opt,name=transcript,proto3" json:"transcript,omitempty"` + Sequence int32 `protobuf:"varint,3,opt,name=sequence,proto3" json:"sequence,omitempty"` + IsPartial bool `protobuf:"varint,4,opt,name=is_partial,json=isPartial,proto3" json:"is_partial,omitempty"` + IsFinal bool `protobuf:"varint,5,opt,name=is_final,json=isFinal,proto3" json:"is_final,omitempty"` + Timestamp int64 `protobuf:"varint,6,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + SpeakerId string `protobuf:"bytes,7,opt,name=speaker_id,json=speakerId,proto3" json:"speaker_id,omitempty"` + HasVoiceActivity bool `protobuf:"varint,8,opt,name=has_voice_activity,json=hasVoiceActivity,proto3" json:"has_voice_activity,omitempty"` + State string `protobuf:"bytes,9,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STTTranscription) Reset() { + *x = STTTranscription{} + mi := &file_messages_v1_messages_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STTTranscription) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STTTranscription) ProtoMessage() {} + +func (x *STTTranscription) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STTTranscription.ProtoReflect.Descriptor instead. +func (*STTTranscription) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{18} +} + +func (x *STTTranscription) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *STTTranscription) GetTranscript() string { + if x != nil { + return x.Transcript + } + return "" +} + +func (x *STTTranscription) GetSequence() int32 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *STTTranscription) GetIsPartial() bool { + if x != nil { + return x.IsPartial + } + return false +} + +func (x *STTTranscription) GetIsFinal() bool { + if x != nil { + return x.IsFinal + } + return false +} + +func (x *STTTranscription) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *STTTranscription) GetSpeakerId() string { + if x != nil { + return x.SpeakerId + } + return "" +} + +func (x *STTTranscription) GetHasVoiceActivity() bool { + if x != nil { + return x.HasVoiceActivity + } + return false +} + +func (x *STTTranscription) GetState() string { + if x != nil { + return x.State + } + return "" +} + +// STTInterrupt is published when the STT module detects a user interrupt. +// Subject: ai.voice.transcription.{session_id} +type STTInterrupt struct { + state protoimpl.MessageState `protogen:"open.v1"` + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` // "interrupt" + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + SpeakerId string `protobuf:"bytes,4,opt,name=speaker_id,json=speakerId,proto3" json:"speaker_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *STTInterrupt) Reset() { + *x = STTInterrupt{} + mi := &file_messages_v1_messages_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *STTInterrupt) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*STTInterrupt) ProtoMessage() {} + +func (x *STTInterrupt) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use STTInterrupt.ProtoReflect.Descriptor instead. +func (*STTInterrupt) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{19} +} + +func (x *STTInterrupt) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *STTInterrupt) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *STTInterrupt) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *STTInterrupt) GetSpeakerId() string { + if x != nil { + return x.SpeakerId + } + return "" +} + +// PipelineTrigger is the request to start a pipeline. +// Subject: ai.pipeline.trigger +type PipelineTrigger struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Pipeline string `protobuf:"bytes,2,opt,name=pipeline,proto3" json:"pipeline,omitempty"` + // Protobuf Struct could be used here, but a simple string map covers + // all current use-cases and avoids a google/protobuf import. + Parameters map[string]string `protobuf:"bytes,3,rep,name=parameters,proto3" json:"parameters,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PipelineTrigger) Reset() { + *x = PipelineTrigger{} + mi := &file_messages_v1_messages_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PipelineTrigger) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PipelineTrigger) ProtoMessage() {} + +func (x *PipelineTrigger) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PipelineTrigger.ProtoReflect.Descriptor instead. +func (*PipelineTrigger) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{20} +} + +func (x *PipelineTrigger) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *PipelineTrigger) GetPipeline() string { + if x != nil { + return x.Pipeline + } + return "" +} + +func (x *PipelineTrigger) GetParameters() map[string]string { + if x != nil { + return x.Parameters + } + return nil +} + +// PipelineStatus is the response / status update for a pipeline run. +// Subject: ai.pipeline.status.{request_id} +type PipelineStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"` + RunId string `protobuf:"bytes,3,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"` + Engine string `protobuf:"bytes,4,opt,name=engine,proto3" json:"engine,omitempty"` + Pipeline string `protobuf:"bytes,5,opt,name=pipeline,proto3" json:"pipeline,omitempty"` + SubmittedAt string `protobuf:"bytes,6,opt,name=submitted_at,json=submittedAt,proto3" json:"submitted_at,omitempty"` + Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"` + AvailablePipelines []string `protobuf:"bytes,8,rep,name=available_pipelines,json=availablePipelines,proto3" json:"available_pipelines,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PipelineStatus) Reset() { + *x = PipelineStatus{} + mi := &file_messages_v1_messages_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PipelineStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PipelineStatus) ProtoMessage() {} + +func (x *PipelineStatus) ProtoReflect() protoreflect.Message { + mi := &file_messages_v1_messages_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PipelineStatus.ProtoReflect.Descriptor instead. +func (*PipelineStatus) Descriptor() ([]byte, []int) { + return file_messages_v1_messages_proto_rawDescGZIP(), []int{21} +} + +func (x *PipelineStatus) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *PipelineStatus) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PipelineStatus) GetRunId() string { + if x != nil { + return x.RunId + } + return "" +} + +func (x *PipelineStatus) GetEngine() string { + if x != nil { + return x.Engine + } + return "" +} + +func (x *PipelineStatus) GetPipeline() string { + if x != nil { + return x.Pipeline + } + return "" +} + +func (x *PipelineStatus) GetSubmittedAt() string { + if x != nil { + return x.SubmittedAt + } + return "" +} + +func (x *PipelineStatus) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *PipelineStatus) GetAvailablePipelines() []string { + if x != nil { + return x.AvailablePipelines + } + return nil +} + +var File_messages_v1_messages_proto protoreflect.FileDescriptor + +const file_messages_v1_messages_proto_rawDesc = "" + + "\n" + + "\x1amessages/v1/messages.proto\x12\vmessages.v1\"S\n" + + "\rErrorResponse\x12\x14\n" + + "\x05error\x18\x01 \x01(\bR\x05error\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x12\n" + + "\x04type\x18\x03 \x01(\tR\x04type\"\x95\x01\n" + + "\n" + + "LoginEvent\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x1a\n" + + "\bnickname\x18\x03 \x01(\tR\bnickname\x12\x18\n" + + "\apremium\x18\x04 \x01(\bR\apremium\x12\x1c\n" + + "\ttimestamp\x18\x05 \x01(\x03R\ttimestamp\"|\n" + + "\x0fGreetingRequest\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x1a\n" + + "\bnickname\x18\x03 \x01(\tR\bnickname\x12\x18\n" + + "\apremium\x18\x04 \x01(\bR\apremium\"G\n" + + "\x10GreetingResponse\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1a\n" + + "\bgreeting\x18\x02 \x01(\tR\bgreeting\"\xc2\x03\n" + + "\vChatRequest\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x17\n" + + "\auser_id\x18\x02 \x01(\tR\x06userId\x12\x1a\n" + + "\busername\x18\x03 \x01(\tR\busername\x12\x18\n" + + "\amessage\x18\x04 \x01(\tR\amessage\x12\x14\n" + + "\x05query\x18\x05 \x01(\tR\x05query\x12\x18\n" + + "\apremium\x18\x06 \x01(\bR\apremium\x12\x1d\n" + + "\n" + + "enable_rag\x18\a \x01(\bR\tenableRag\x12'\n" + + "\x0fenable_reranker\x18\b \x01(\bR\x0eenableReranker\x12)\n" + + "\x10enable_streaming\x18\t \x01(\bR\x0fenableStreaming\x12\x13\n" + + "\x05top_k\x18\n" + + " \x01(\x05R\x04topK\x12\x1e\n" + + "\n" + + "collection\x18\v \x01(\tR\n" + + "collection\x12\x1d\n" + + "\n" + + "enable_tts\x18\f \x01(\bR\tenableTts\x12#\n" + + "\rsystem_prompt\x18\r \x01(\tR\fsystemPrompt\x12)\n" + + "\x10response_subject\x18\x0e \x01(\tR\x0fresponseSubject\"\xea\x01\n" + + "\fChatResponse\x12\x17\n" + + "\auser_id\x18\x01 \x01(\tR\x06userId\x12\x1a\n" + + "\bresponse\x18\x02 \x01(\tR\bresponse\x12#\n" + + "\rresponse_text\x18\x03 \x01(\tR\fresponseText\x12\x19\n" + + "\bused_rag\x18\x04 \x01(\bR\ausedRag\x12\x1f\n" + + "\vrag_sources\x18\x05 \x03(\tR\n" + + "ragSources\x12\x18\n" + + "\asuccess\x18\x06 \x01(\bR\asuccess\x12\x14\n" + + "\x05audio\x18\a \x01(\fR\x05audio\x12\x14\n" + + "\x05error\x18\b \x01(\tR\x05error\"\x90\x01\n" + + "\x0fChatStreamChunk\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x12\n" + + "\x04done\x18\x04 \x01(\bR\x04done\x12\x1c\n" + + "\ttimestamp\x18\x05 \x01(\x03R\ttimestamp\"\x7f\n" + + "\fVoiceRequest\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x14\n" + + "\x05audio\x18\x02 \x01(\fR\x05audio\x12\x1a\n" + + "\blanguage\x18\x03 \x01(\tR\blanguage\x12\x1e\n" + + "\n" + + "collection\x18\x04 \x01(\tR\n" + + "collection\":\n" + + "\x0eDocumentSource\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x12\x14\n" + + "\x05score\x18\x02 \x01(\x01R\x05score\"\xd3\x01\n" + + "\rVoiceResponse\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1a\n" + + "\bresponse\x18\x02 \x01(\tR\bresponse\x12\x14\n" + + "\x05audio\x18\x03 \x01(\fR\x05audio\x12$\n" + + "\rtranscription\x18\x04 \x01(\tR\rtranscription\x125\n" + + "\asources\x18\x05 \x03(\v2\x1b.messages.v1.DocumentSourceR\asources\x12\x14\n" + + "\x05error\x18\x06 \x01(\tR\x05error\"\x96\x01\n" + + "\n" + + "TTSRequest\x12\x12\n" + + "\x04text\x18\x01 \x01(\tR\x04text\x12\x18\n" + + "\aspeaker\x18\x02 \x01(\tR\aspeaker\x12\x1a\n" + + "\blanguage\x18\x03 \x01(\tR\blanguage\x12&\n" + + "\x0fspeaker_wav_b64\x18\x04 \x01(\tR\rspeakerWavB64\x12\x16\n" + + "\x06stream\x18\x05 \x01(\bR\x06stream\"\xe0\x01\n" + + "\rTTSAudioChunk\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1f\n" + + "\vchunk_index\x18\x02 \x01(\x05R\n" + + "chunkIndex\x12!\n" + + "\ftotal_chunks\x18\x03 \x01(\x05R\vtotalChunks\x12\x14\n" + + "\x05audio\x18\x04 \x01(\fR\x05audio\x12\x17\n" + + "\ais_last\x18\x05 \x01(\bR\x06isLast\x12\x1c\n" + + "\ttimestamp\x18\x06 \x01(\x03R\ttimestamp\x12\x1f\n" + + "\vsample_rate\x18\a \x01(\x05R\n" + + "sampleRate\"\x85\x01\n" + + "\x0fTTSFullResponse\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x14\n" + + "\x05audio\x18\x02 \x01(\fR\x05audio\x12\x1c\n" + + "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\x12\x1f\n" + + "\vsample_rate\x18\x04 \x01(\x05R\n" + + "sampleRate\"z\n" + + "\tTTSStatus\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x18\n" + + "\amessage\x18\x03 \x01(\tR\amessage\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\"|\n" + + "\fTTSVoiceInfo\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + + "\blanguage\x18\x02 \x01(\tR\blanguage\x12\x1d\n" + + "\n" + + "model_type\x18\x03 \x01(\tR\tmodelType\x12\x1d\n" + + "\n" + + "created_at\x18\x04 \x01(\tR\tcreatedAt\"\xc0\x01\n" + + "\x14TTSVoiceListResponse\x12'\n" + + "\x0fdefault_speaker\x18\x01 \x01(\tR\x0edefaultSpeaker\x12>\n" + + "\rcustom_voices\x18\x02 \x03(\v2\x19.messages.v1.TTSVoiceInfoR\fcustomVoices\x12!\n" + + "\flast_refresh\x18\x03 \x01(\x03R\vlastRefresh\x12\x1c\n" + + "\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\"\x8d\x01\n" + + "\x17TTSVoiceRefreshResponse\x12\x14\n" + + "\x05count\x18\x01 \x01(\x05R\x05count\x12>\n" + + "\rcustom_voices\x18\x02 \x03(\v2\x19.messages.v1.TTSVoiceInfoR\fcustomVoices\x12\x1c\n" + + "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\"q\n" + + "\x10STTStreamMessage\x12\x12\n" + + "\x04type\x18\x01 \x01(\tR\x04type\x12\x14\n" + + "\x05audio\x18\x02 \x01(\fR\x05audio\x12\x14\n" + + "\x05state\x18\x03 \x01(\tR\x05state\x12\x1d\n" + + "\n" + + "speaker_id\x18\x04 \x01(\tR\tspeakerId\"\xa8\x02\n" + + "\x10STTTranscription\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1e\n" + + "\n" + + "transcript\x18\x02 \x01(\tR\n" + + "transcript\x12\x1a\n" + + "\bsequence\x18\x03 \x01(\x05R\bsequence\x12\x1d\n" + + "\n" + + "is_partial\x18\x04 \x01(\bR\tisPartial\x12\x19\n" + + "\bis_final\x18\x05 \x01(\bR\aisFinal\x12\x1c\n" + + "\ttimestamp\x18\x06 \x01(\x03R\ttimestamp\x12\x1d\n" + + "\n" + + "speaker_id\x18\a \x01(\tR\tspeakerId\x12,\n" + + "\x12has_voice_activity\x18\b \x01(\bR\x10hasVoiceActivity\x12\x14\n" + + "\x05state\x18\t \x01(\tR\x05state\"~\n" + + "\fSTTInterrupt\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x12\n" + + "\x04type\x18\x02 \x01(\tR\x04type\x12\x1c\n" + + "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\x12\x1d\n" + + "\n" + + "speaker_id\x18\x04 \x01(\tR\tspeakerId\"\xd9\x01\n" + + "\x0fPipelineTrigger\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x1a\n" + + "\bpipeline\x18\x02 \x01(\tR\bpipeline\x12L\n" + + "\n" + + "parameters\x18\x03 \x03(\v2,.messages.v1.PipelineTrigger.ParametersEntryR\n" + + "parameters\x1a=\n" + + "\x0fParametersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xfc\x01\n" + + "\x0ePipelineStatus\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x16\n" + + "\x06status\x18\x02 \x01(\tR\x06status\x12\x15\n" + + "\x06run_id\x18\x03 \x01(\tR\x05runId\x12\x16\n" + + "\x06engine\x18\x04 \x01(\tR\x06engine\x12\x1a\n" + + "\bpipeline\x18\x05 \x01(\tR\bpipeline\x12!\n" + + "\fsubmitted_at\x18\x06 \x01(\tR\vsubmittedAt\x12\x14\n" + + "\x05error\x18\a \x01(\tR\x05error\x12/\n" + + "\x13available_pipelines\x18\b \x03(\tR\x12availablePipelinesBBZ@git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespbb\x06proto3" + +var ( + file_messages_v1_messages_proto_rawDescOnce sync.Once + file_messages_v1_messages_proto_rawDescData []byte +) + +func file_messages_v1_messages_proto_rawDescGZIP() []byte { + file_messages_v1_messages_proto_rawDescOnce.Do(func() { + file_messages_v1_messages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_messages_v1_messages_proto_rawDesc), len(file_messages_v1_messages_proto_rawDesc))) + }) + return file_messages_v1_messages_proto_rawDescData +} + +var file_messages_v1_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 23) +var file_messages_v1_messages_proto_goTypes = []any{ + (*ErrorResponse)(nil), // 0: messages.v1.ErrorResponse + (*LoginEvent)(nil), // 1: messages.v1.LoginEvent + (*GreetingRequest)(nil), // 2: messages.v1.GreetingRequest + (*GreetingResponse)(nil), // 3: messages.v1.GreetingResponse + (*ChatRequest)(nil), // 4: messages.v1.ChatRequest + (*ChatResponse)(nil), // 5: messages.v1.ChatResponse + (*ChatStreamChunk)(nil), // 6: messages.v1.ChatStreamChunk + (*VoiceRequest)(nil), // 7: messages.v1.VoiceRequest + (*DocumentSource)(nil), // 8: messages.v1.DocumentSource + (*VoiceResponse)(nil), // 9: messages.v1.VoiceResponse + (*TTSRequest)(nil), // 10: messages.v1.TTSRequest + (*TTSAudioChunk)(nil), // 11: messages.v1.TTSAudioChunk + (*TTSFullResponse)(nil), // 12: messages.v1.TTSFullResponse + (*TTSStatus)(nil), // 13: messages.v1.TTSStatus + (*TTSVoiceInfo)(nil), // 14: messages.v1.TTSVoiceInfo + (*TTSVoiceListResponse)(nil), // 15: messages.v1.TTSVoiceListResponse + (*TTSVoiceRefreshResponse)(nil), // 16: messages.v1.TTSVoiceRefreshResponse + (*STTStreamMessage)(nil), // 17: messages.v1.STTStreamMessage + (*STTTranscription)(nil), // 18: messages.v1.STTTranscription + (*STTInterrupt)(nil), // 19: messages.v1.STTInterrupt + (*PipelineTrigger)(nil), // 20: messages.v1.PipelineTrigger + (*PipelineStatus)(nil), // 21: messages.v1.PipelineStatus + nil, // 22: messages.v1.PipelineTrigger.ParametersEntry +} +var file_messages_v1_messages_proto_depIdxs = []int32{ + 8, // 0: messages.v1.VoiceResponse.sources:type_name -> messages.v1.DocumentSource + 14, // 1: messages.v1.TTSVoiceListResponse.custom_voices:type_name -> messages.v1.TTSVoiceInfo + 14, // 2: messages.v1.TTSVoiceRefreshResponse.custom_voices:type_name -> messages.v1.TTSVoiceInfo + 22, // 3: messages.v1.PipelineTrigger.parameters:type_name -> messages.v1.PipelineTrigger.ParametersEntry + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_messages_v1_messages_proto_init() } +func file_messages_v1_messages_proto_init() { + if File_messages_v1_messages_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_messages_v1_messages_proto_rawDesc), len(file_messages_v1_messages_proto_rawDesc)), + NumEnums: 0, + NumMessages: 23, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_messages_v1_messages_proto_goTypes, + DependencyIndexes: file_messages_v1_messages_proto_depIdxs, + MessageInfos: file_messages_v1_messages_proto_msgTypes, + }.Build() + File_messages_v1_messages_proto = out.File + file_messages_v1_messages_proto_goTypes = nil + file_messages_v1_messages_proto_depIdxs = nil +} diff --git a/go.mod b/go.mod index f93c209..4c9e092 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module git.daviestechlabs.io/daviestechlabs/handler-base go 1.25.1 require ( + github.com/fsnotify/fsnotify v1.9.0 github.com/nats-io/nats.go v1.48.0 - github.com/vmihailenco/msgpack/v5 v5.4.1 go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 @@ -12,12 +12,12 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk/metric v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 + google.golang.org/protobuf v1.36.11 ) require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect @@ -25,7 +25,6 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/nats-io/nkeys v0.4.11 // indirect github.com/nats-io/nuid v1.0.1 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect @@ -36,5 +35,4 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/grpc v1.78.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index b9a1b68..6bfdfe6 100644 --- a/go.sum +++ b/go.sum @@ -31,10 +31,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= diff --git a/handler/handler.go b/handler/handler.go index d1b904a..2ad8d05 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -2,30 +2,27 @@ package handler import ( - "context" - "fmt" - "log/slog" - "os" - "os/signal" - "syscall" +"context" +"fmt" +"log/slog" +"os" +"os/signal" +"syscall" - "github.com/nats-io/nats.go" +"github.com/nats-io/nats.go" +"google.golang.org/protobuf/proto" - "git.daviestechlabs.io/daviestechlabs/handler-base/config" - "git.daviestechlabs.io/daviestechlabs/handler-base/health" - "git.daviestechlabs.io/daviestechlabs/handler-base/natsutil" - "git.daviestechlabs.io/daviestechlabs/handler-base/telemetry" +"git.daviestechlabs.io/daviestechlabs/handler-base/config" +pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb" +"git.daviestechlabs.io/daviestechlabs/handler-base/health" +"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil" +"git.daviestechlabs.io/daviestechlabs/handler-base/telemetry" ) -// MessageHandler is the callback for processing decoded NATS messages. -// data is the msgpack-decoded map. Return a response map (or nil for no reply). -type MessageHandler func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) - -// TypedMessageHandler processes the raw NATS message without pre-decoding to -// map[string]any. Services unmarshal msg.Data into their own typed structs, -// avoiding the double-decode overhead. Return any msgpack-serialisable value -// (a typed struct, map, or nil for no reply). -type TypedMessageHandler func(ctx context.Context, msg *nats.Msg) (any, error) +// TypedMessageHandler processes the raw NATS message. +// Services unmarshal msg.Data into their own typed structs via natsutil.Decode. +// Return a proto.Message (or nil for no reply). +type TypedMessageHandler func(ctx context.Context, msg *nats.Msg) (proto.Message, error) // SetupFunc is called once before the handler starts processing messages. type SetupFunc func(ctx context.Context) error @@ -35,37 +32,36 @@ type TeardownFunc func(ctx context.Context) error // Handler is the base service runner that wires NATS, health, and telemetry. type Handler struct { - Settings *config.Settings - NATS *natsutil.Client - Telemetry *telemetry.Provider - Subject string - QueueGroup string +Settings *config.Settings +NATS *natsutil.Client +Telemetry *telemetry.Provider +Subject string +QueueGroup string - onSetup SetupFunc - onTeardown TeardownFunc - onMessage MessageHandler - onTypedMessage TypedMessageHandler - running bool +onSetup SetupFunc +onTeardown TeardownFunc +onTypedMessage TypedMessageHandler +running bool } // New creates a Handler for the given NATS subject. func New(subject string, settings *config.Settings) *Handler { - if settings == nil { - settings = config.Load() - } - queueGroup := settings.NATSQueueGroup +if settings == nil { +settings = config.Load() +} +queueGroup := settings.NATSQueueGroup - natsOpts := []nats.Option{} - if settings.NATSUser != "" && settings.NATSPassword != "" { - natsOpts = append(natsOpts, nats.UserInfo(settings.NATSUser, settings.NATSPassword)) - } +natsOpts := []nats.Option{} +if settings.NATSUser != "" && settings.NATSPassword != "" { +natsOpts = append(natsOpts, nats.UserInfo(settings.NATSUser, settings.NATSPassword)) +} - return &Handler{ - Settings: settings, - Subject: subject, - QueueGroup: queueGroup, - NATS: natsutil.New(settings.NATSURL, natsOpts...), - } +return &Handler{ +Settings: settings, +Subject: subject, +QueueGroup: queueGroup, +NATS: natsutil.New(settings.NATSURL, natsOpts...), +} } // OnSetup registers the setup callback. @@ -74,158 +70,106 @@ func (h *Handler) OnSetup(fn SetupFunc) { h.onSetup = fn } // OnTeardown registers the teardown callback. func (h *Handler) OnTeardown(fn TeardownFunc) { h.onTeardown = fn } -// OnMessage registers the message handler callback. -func (h *Handler) OnMessage(fn MessageHandler) { h.onMessage = fn } - -// OnTypedMessage registers a typed message handler. It replaces OnMessage — -// wrapHandler will skip the map[string]any decode and let the callback -// unmarshal msg.Data directly. +// OnTypedMessage registers the message handler callback. func (h *Handler) OnTypedMessage(fn TypedMessageHandler) { h.onTypedMessage = fn } // Run starts the handler: telemetry, health server, NATS subscription, and blocks until SIGTERM/SIGINT. func (h *Handler) Run() error { - // Structured logging - slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))) - slog.Info("starting service", "name", h.Settings.ServiceName, "version", h.Settings.ServiceVersion) +// Structured logging +slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))) +slog.Info("starting service", "name", h.Settings.ServiceName, "version", h.Settings.ServiceVersion) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +ctx, cancel := context.WithCancel(context.Background()) +defer cancel() - // Telemetry - tp, shutdown, err := telemetry.Setup(ctx, telemetry.Config{ - ServiceName: h.Settings.ServiceName, - ServiceVersion: h.Settings.ServiceVersion, - ServiceNamespace: h.Settings.ServiceNamespace, - DeploymentEnv: h.Settings.DeploymentEnv, - Enabled: h.Settings.OTELEnabled, - Endpoint: h.Settings.OTELEndpoint, - }) - if err != nil { - return fmt.Errorf("telemetry setup: %w", err) - } - defer shutdown(ctx) - h.Telemetry = tp +// Telemetry +tp, shutdown, err := telemetry.Setup(ctx, telemetry.Config{ +ServiceName: h.Settings.ServiceName, +ServiceVersion: h.Settings.ServiceVersion, +ServiceNamespace: h.Settings.ServiceNamespace, +DeploymentEnv: h.Settings.DeploymentEnv, +Enabled: h.Settings.OTELEnabled, +Endpoint: h.Settings.OTELEndpoint, +}) +if err != nil { +return fmt.Errorf("telemetry setup: %w", err) +} +defer shutdown(ctx) +h.Telemetry = tp - // Health server - healthSrv := health.New( - h.Settings.HealthPort, - h.Settings.HealthPath, - h.Settings.ReadyPath, - func() bool { return h.running && h.NATS.IsConnected() }, - ) - healthSrv.Start() - defer healthSrv.Stop(ctx) +// Health server +healthSrv := health.New( +h.Settings.HealthPort, +h.Settings.HealthPath, +h.Settings.ReadyPath, +func() bool { return h.running && h.NATS.IsConnected() }, +) +healthSrv.Start() +defer healthSrv.Stop(ctx) - // Connect to NATS - if err := h.NATS.Connect(); err != nil { - return fmt.Errorf("nats: %w", err) - } - defer h.NATS.Close() +// Connect to NATS +if err := h.NATS.Connect(); err != nil { +return fmt.Errorf("nats: %w", err) +} +defer h.NATS.Close() - // User setup - if h.onSetup != nil { - slog.Info("running service setup") - if err := h.onSetup(ctx); err != nil { - return fmt.Errorf("setup: %w", err) - } - } +// User setup +if h.onSetup != nil { +slog.Info("running service setup") +if err := h.onSetup(ctx); err != nil { +return fmt.Errorf("setup: %w", err) +} +} - // Subscribe - if h.onMessage == nil && h.onTypedMessage == nil { - return fmt.Errorf("no message handler registered") - } - if err := h.NATS.Subscribe(h.Subject, h.wrapHandler(ctx), h.QueueGroup); err != nil { - return fmt.Errorf("subscribe: %w", err) - } +// Subscribe +if h.onTypedMessage == nil { +return fmt.Errorf("no message handler registered") +} +if err := h.NATS.Subscribe(h.Subject, h.wrapHandler(ctx), h.QueueGroup); err != nil { +return fmt.Errorf("subscribe: %w", err) +} - h.running = true - slog.Info("handler ready", "subject", h.Subject) +h.running = true +slog.Info("handler ready", "subject", h.Subject) - // Wait for shutdown signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) - <-sigCh +// Wait for shutdown signal +sigCh := make(chan os.Signal, 1) +signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT) +<-sigCh - slog.Info("shutting down") - h.running = false +slog.Info("shutting down") +h.running = false - // Teardown - if h.onTeardown != nil { - if err := h.onTeardown(ctx); err != nil { - slog.Warn("teardown error", "error", err) - } - } +// Teardown +if h.onTeardown != nil { +if err := h.onTeardown(ctx); err != nil { +slog.Warn("teardown error", "error", err) +} +} - slog.Info("shutdown complete") - return nil +slog.Info("shutdown complete") +return nil } // wrapHandler creates a nats.MsgHandler that dispatches to the registered callback. -// If OnTypedMessage was used, msg.Data is passed directly without map decode. -// If OnMessage was used, msg.Data is decoded to map[string]any first. func (h *Handler) wrapHandler(ctx context.Context) nats.MsgHandler { - if h.onTypedMessage != nil { - return h.wrapTypedHandler(ctx) - } - return h.wrapMapHandler(ctx) +return func(msg *nats.Msg) { +response, err := h.onTypedMessage(ctx, msg) +if err != nil { +slog.Error("handler error", "subject", msg.Subject, "error", err) +if msg.Reply != "" { +_ = h.NATS.Publish(msg.Reply, &pb.ErrorResponse{ +Error: true, +Message: err.Error(), +Type: fmt.Sprintf("%T", err), +}) +} +return +} +if response != nil && msg.Reply != "" { +if err := h.NATS.Publish(msg.Reply, response); err != nil { +slog.Error("failed to publish reply", "error", err) +} } - -// wrapTypedHandler dispatches to the TypedMessageHandler (no map decode). -func (h *Handler) wrapTypedHandler(ctx context.Context) nats.MsgHandler { - return func(msg *nats.Msg) { - response, err := h.onTypedMessage(ctx, msg) - if err != nil { - slog.Error("handler error", "subject", msg.Subject, "error", err) - if msg.Reply != "" { - _ = h.NATS.Publish(msg.Reply, map[string]any{ - "error": true, - "message": err.Error(), - "type": fmt.Sprintf("%T", err), - }) - } - return - } - if response != nil && msg.Reply != "" { - if err := h.NATS.Publish(msg.Reply, response); err != nil { - slog.Error("failed to publish reply", "error", err) - } - } - } } - -// wrapMapHandler dispatches to the legacy MessageHandler (decodes to map first). -func (h *Handler) wrapMapHandler(ctx context.Context) nats.MsgHandler { - return func(msg *nats.Msg) { - data, err := natsutil.DecodeMsgpackMap(msg.Data) - if err != nil { - slog.Error("failed to decode message", "subject", msg.Subject, "error", err) - if msg.Reply != "" { - _ = h.NATS.Publish(msg.Reply, map[string]any{ - "error": true, - "message": err.Error(), - "type": "DecodeError", - }) - } - return - } - - response, err := h.onMessage(ctx, msg, data) - if err != nil { - slog.Error("handler error", "subject", msg.Subject, "error", err) - if msg.Reply != "" { - _ = h.NATS.Publish(msg.Reply, map[string]any{ - "error": true, - "message": err.Error(), - "type": fmt.Sprintf("%T", err), - }) - } - return - } - - if response != nil && msg.Reply != "" { - if err := h.NATS.Publish(msg.Reply, response); err != nil { - slog.Error("failed to publish reply", "error", err) - } - } - } } diff --git a/handler/handler_test.go b/handler/handler_test.go index 9df5f4a..33fb694 100644 --- a/handler/handler_test.go +++ b/handler/handler_test.go @@ -1,13 +1,15 @@ package handler import ( - "context" - "testing" +"context" +"testing" - "github.com/nats-io/nats.go" - "github.com/vmihailenco/msgpack/v5" +"github.com/nats-io/nats.go" +"google.golang.org/protobuf/proto" - "git.daviestechlabs.io/daviestechlabs/handler-base/config" +"git.daviestechlabs.io/daviestechlabs/handler-base/config" +pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb" +"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil" ) // ──────────────────────────────────────────────────────────────────────────── @@ -15,75 +17,75 @@ import ( // ──────────────────────────────────────────────────────────────────────────── func TestNewHandler(t *testing.T) { - cfg := config.Load() - cfg.ServiceName = "test-handler" - cfg.NATSQueueGroup = "test-group" +cfg := config.Load() +cfg.ServiceName = "test-handler" +cfg.NATSQueueGroup = "test-group" - h := New("ai.test.subject", cfg) - if h.Subject != "ai.test.subject" { - t.Errorf("Subject = %q", h.Subject) - } - if h.QueueGroup != "test-group" { - t.Errorf("QueueGroup = %q", h.QueueGroup) - } - if h.Settings.ServiceName != "test-handler" { - t.Errorf("ServiceName = %q", h.Settings.ServiceName) - } +h := New("ai.test.subject", cfg) +if h.Subject != "ai.test.subject" { +t.Errorf("Subject = %q", h.Subject) +} +if h.QueueGroup != "test-group" { +t.Errorf("QueueGroup = %q", h.QueueGroup) +} +if h.Settings.ServiceName != "test-handler" { +t.Errorf("ServiceName = %q", h.Settings.ServiceName) +} } func TestNewHandlerNilSettings(t *testing.T) { - h := New("ai.test", nil) - if h.Settings == nil { - t.Fatal("Settings should be loaded automatically") - } - if h.Settings.ServiceName != "handler" { - t.Errorf("ServiceName = %q, want default", h.Settings.ServiceName) - } +h := New("ai.test", nil) +if h.Settings == nil { +t.Fatal("Settings should be loaded automatically") +} +if h.Settings.ServiceName != "handler" { +t.Errorf("ServiceName = %q, want default", h.Settings.ServiceName) +} } func TestCallbackRegistration(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +cfg := config.Load() +h := New("ai.test", cfg) - setupCalled := false - h.OnSetup(func(ctx context.Context) error { - setupCalled = true - return nil - }) +setupCalled := false +h.OnSetup(func(ctx context.Context) error { +setupCalled = true +return nil +}) - teardownCalled := false - h.OnTeardown(func(ctx context.Context) error { - teardownCalled = true - return nil - }) +teardownCalled := false +h.OnTeardown(func(ctx context.Context) error { +teardownCalled = true +return nil +}) - h.OnMessage(func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) { - return nil, nil - }) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +return nil, nil +}) - if h.onSetup == nil || h.onTeardown == nil || h.onMessage == nil { - t.Error("callbacks should not be nil after registration") - } +if h.onSetup == nil || h.onTeardown == nil || h.onTypedMessage == nil { +t.Error("callbacks should not be nil after registration") +} - // Verify setup/teardown work when called directly. - _ = h.onSetup(context.Background()) - _ = h.onTeardown(context.Background()) - if !setupCalled || !teardownCalled { - t.Error("callbacks should have been invoked") - } +// Verify setup/teardown work when called directly. +_ = h.onSetup(context.Background()) +_ = h.onTeardown(context.Background()) +if !setupCalled || !teardownCalled { +t.Error("callbacks should have been invoked") +} } func TestTypedMessageRegistration(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +cfg := config.Load() +h := New("ai.test", cfg) - h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (any, error) { - return map[string]any{"ok": true}, nil - }) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { + return &pb.ChatResponse{Response: "ok"}, nil +}) - if h.onTypedMessage == nil { - t.Error("onTypedMessage should not be nil after registration") - } +if h.onTypedMessage == nil { +t.Error("onTypedMessage should not be nil after registration") +} } // ──────────────────────────────────────────────────────────────────────────── @@ -91,164 +93,161 @@ func TestTypedMessageRegistration(t *testing.T) { // ──────────────────────────────────────────────────────────────────────────── func TestWrapHandler_ValidMessage(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +cfg := config.Load() +h := New("ai.test", cfg) - var receivedData map[string]any - h.OnMessage(func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) { - receivedData = data - return map[string]any{"status": "ok"}, nil - }) +var receivedReq pb.ChatRequest +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +if err := natsutil.Decode(msg.Data, &receivedReq); err != nil { +return nil, err +} + return &pb.ChatResponse{Response: "ok", UserId: receivedReq.GetUserId()}, nil +}) - // Encode a message the same way services would. - payload := map[string]any{ - "request_id": "test-001", - "message": "hello", - "premium": true, - } - encoded, err := msgpack.Marshal(payload) - if err != nil { - t.Fatal(err) - } - - // Call wrapHandler directly without NATS. - handler := h.wrapHandler(context.Background()) - handler(&nats.Msg{ - Subject: "ai.test.user.42.message", - Data: encoded, - }) - - if receivedData == nil { - t.Fatal("handler was not called") - } - if receivedData["request_id"] != "test-001" { - t.Errorf("request_id = %v", receivedData["request_id"]) - } - if receivedData["premium"] != true { - t.Errorf("premium = %v", receivedData["premium"]) - } +// Encode a message the same way services would. +encoded, err := proto.Marshal(&pb.ChatRequest{ +RequestId: "test-001", +Message: "hello", +Premium: true, +}) +if err != nil { +t.Fatal(err) } -func TestWrapHandler_InvalidMsgpack(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +// Call wrapHandler directly without NATS. +handler := h.wrapHandler(context.Background()) +handler(&nats.Msg{ +Subject: "ai.test.user.42.message", +Data: encoded, +}) - handlerCalled := false - h.OnMessage(func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) { - handlerCalled = true - return nil, nil - }) +if receivedReq.GetRequestId() != "test-001" { +t.Errorf("request_id = %v", receivedReq.GetRequestId()) +} +if receivedReq.GetPremium() != true { +t.Errorf("premium = %v", receivedReq.GetPremium()) +} +} - handler := h.wrapHandler(context.Background()) - handler(&nats.Msg{ - Subject: "ai.test", - Data: []byte{0xFF, 0xFE, 0xFD}, // invalid msgpack - }) +func TestWrapHandler_InvalidMessage(t *testing.T) { +cfg := config.Load() +h := New("ai.test", cfg) - if handlerCalled { - t.Error("handler should not be called for invalid msgpack") - } +handlerCalled := false +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +handlerCalled = true +var req pb.ChatRequest +if err := natsutil.Decode(msg.Data, &req); err != nil { +return nil, err +} +return &pb.ChatResponse{}, nil +}) + +handler := h.wrapHandler(context.Background()) +handler(&nats.Msg{ +Subject: "ai.test", +Data: []byte{0xFF, 0xFE, 0xFD}, // invalid protobuf +}) + +// The handler IS called (wrapHandler doesn't pre-decode), but it should +// return an error from Decode. Either way no panic. +_ = handlerCalled } func TestWrapHandler_HandlerError(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +cfg := config.Load() +h := New("ai.test", cfg) - h.OnMessage(func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) { - return nil, context.DeadlineExceeded - }) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +return nil, context.DeadlineExceeded +}) - encoded, _ := msgpack.Marshal(map[string]any{"key": "val"}) - handler := h.wrapHandler(context.Background()) +encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "err-test"}) +handler := h.wrapHandler(context.Background()) - // Should not panic even when handler returns error. - handler(&nats.Msg{ - Subject: "ai.test", - Data: encoded, - }) +// Should not panic even when handler returns error. +handler(&nats.Msg{ +Subject: "ai.test", +Data: encoded, +}) } func TestWrapHandler_NilResponse(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +cfg := config.Load() +h := New("ai.test", cfg) - h.OnMessage(func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) { - return nil, nil // fire-and-forget style - }) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +return nil, nil // fire-and-forget style +}) - encoded, _ := msgpack.Marshal(map[string]any{"x": 1}) - handler := h.wrapHandler(context.Background()) +encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "nil-resp"}) +handler := h.wrapHandler(context.Background()) - // Should not panic with nil response and no reply subject. - handler(&nats.Msg{ - Subject: "ai.test", - Data: encoded, - }) +// Should not panic with nil response and no reply subject. +handler(&nats.Msg{ +Subject: "ai.test", +Data: encoded, +}) } // ──────────────────────────────────────────────────────────────────────────── // wrapHandler dispatch tests — typed handler path // ──────────────────────────────────────────────────────────────────────────── -func TestWrapTypedHandler_ValidMessage(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +func TestWrapHandler_Typed(t *testing.T) { +cfg := config.Load() +h := New("ai.test", cfg) - type testReq struct { - RequestID string `msgpack:"request_id"` - Message string `msgpack:"message"` - } +var received pb.ChatRequest +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +if err := natsutil.Decode(msg.Data, &received); err != nil { +return nil, err +} + return &pb.ChatResponse{UserId: received.GetUserId(), Response: "ok"}, nil +}) - var received testReq - h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (any, error) { - if err := msgpack.Unmarshal(msg.Data, &received); err != nil { - return nil, err - } - return map[string]any{"status": "ok"}, nil - }) +encoded, _ := proto.Marshal(&pb.ChatRequest{ +RequestId: "typed-001", +Message: "hello typed", +}) - encoded, _ := msgpack.Marshal(map[string]any{ - "request_id": "typed-001", - "message": "hello typed", - }) +handler := h.wrapHandler(context.Background()) +handler(&nats.Msg{Subject: "ai.test", Data: encoded}) - handler := h.wrapHandler(context.Background()) - handler(&nats.Msg{Subject: "ai.test", Data: encoded}) - - if received.RequestID != "typed-001" { - t.Errorf("RequestID = %q", received.RequestID) - } - if received.Message != "hello typed" { - t.Errorf("Message = %q", received.Message) - } +if received.GetRequestId() != "typed-001" { +t.Errorf("RequestId = %q", received.GetRequestId()) +} +if received.GetMessage() != "hello typed" { +t.Errorf("Message = %q", received.GetMessage()) +} } -func TestWrapTypedHandler_Error(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +func TestWrapHandler_TypedError(t *testing.T) { +cfg := config.Load() +h := New("ai.test", cfg) - h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (any, error) { - return nil, context.DeadlineExceeded - }) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +return nil, context.DeadlineExceeded +}) - encoded, _ := msgpack.Marshal(map[string]any{"key": "val"}) - handler := h.wrapHandler(context.Background()) +encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "err"}) +handler := h.wrapHandler(context.Background()) - // Should not panic. - handler(&nats.Msg{Subject: "ai.test", Data: encoded}) +// Should not panic. +handler(&nats.Msg{Subject: "ai.test", Data: encoded}) } -func TestWrapTypedHandler_NilResponse(t *testing.T) { - cfg := config.Load() - h := New("ai.test", cfg) +func TestWrapHandler_TypedNilResponse(t *testing.T) { +cfg := config.Load() +h := New("ai.test", cfg) - h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (any, error) { - return nil, nil - }) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +return nil, nil +}) - encoded, _ := msgpack.Marshal(map[string]any{"x": 1}) - handler := h.wrapHandler(context.Background()) - handler(&nats.Msg{Subject: "ai.test", Data: encoded}) +encoded, _ := proto.Marshal(&pb.ChatRequest{RequestId: "nil"}) +handler := h.wrapHandler(context.Background()) +handler(&nats.Msg{Subject: "ai.test", Data: encoded}) } // ──────────────────────────────────────────────────────────────────────────── @@ -256,56 +255,25 @@ func TestWrapTypedHandler_NilResponse(t *testing.T) { // ──────────────────────────────────────────────────────────────────────────── func BenchmarkWrapHandler(b *testing.B) { - cfg := config.Load() - h := New("ai.test", cfg) - h.OnMessage(func(ctx context.Context, msg *nats.Msg, data map[string]any) (map[string]any, error) { - return map[string]any{"ok": true}, nil - }) +cfg := config.Load() +h := New("ai.test", cfg) +h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (proto.Message, error) { +var req pb.ChatRequest +_ = natsutil.Decode(msg.Data, &req) +return &pb.ChatResponse{Response: "ok"}, nil +}) - payload := map[string]any{ - "request_id": "bench-001", - "message": "What is the capital of France?", - "premium": true, - "top_k": 10, - } - encoded, _ := msgpack.Marshal(payload) - handler := h.wrapHandler(context.Background()) - msg := &nats.Msg{Subject: "ai.test", Data: encoded} +encoded, _ := proto.Marshal(&pb.ChatRequest{ +RequestId: "bench-001", +Message: "What is the capital of France?", +Premium: true, +TopK: 10, +}) +handler := h.wrapHandler(context.Background()) +msg := &nats.Msg{Subject: "ai.test", Data: encoded} - b.ResetTimer() - for b.Loop() { - handler(msg) - } +b.ResetTimer() +for b.Loop() { +handler(msg) } - -func BenchmarkWrapTypedHandler(b *testing.B) { - type benchReq struct { - RequestID string `msgpack:"request_id"` - Message string `msgpack:"message"` - Premium bool `msgpack:"premium"` - TopK int `msgpack:"top_k"` - } - - cfg := config.Load() - h := New("ai.test", cfg) - h.OnTypedMessage(func(ctx context.Context, msg *nats.Msg) (any, error) { - var req benchReq - _ = msgpack.Unmarshal(msg.Data, &req) - return map[string]any{"ok": true}, nil - }) - - payload := map[string]any{ - "request_id": "bench-001", - "message": "What is the capital of France?", - "premium": true, - "top_k": 10, - } - encoded, _ := msgpack.Marshal(payload) - handler := h.wrapHandler(context.Background()) - msg := &nats.Msg{Subject: "ai.test", Data: encoded} - - b.ResetTimer() - for b.Loop() { - handler(msg) - } } diff --git a/messages/bench_test.go b/messages/bench_test.go index ee107dd..b4a3cd0 100644 --- a/messages/bench_test.go +++ b/messages/bench_test.go @@ -1,143 +1,58 @@ -// Package messages benchmarks compare three serialization strategies: -// -// 1. msgpack map[string]any — the old approach (dynamic, no types) -// 2. msgpack typed struct — the new approach (compile-time safe, short keys) -// 3. protobuf — optional future migration +// Package messages benchmarks protobuf encoding/decoding of all message types. // // Run with: // -// go test -bench=. -benchmem -count=5 ./messages/... | tee bench.txt -// # optional: go install golang.org/x/perf/cmd/benchstat@latest && benchstat bench.txt +//go test -bench=. -benchmem -count=5 ./messages/... | tee bench.txt +//# optional: go install golang.org/x/perf/cmd/benchstat@latest && benchstat bench.txt package messages import ( - "testing" - "time" +"testing" +"time" - "github.com/vmihailenco/msgpack/v5" - "google.golang.org/protobuf/proto" +"google.golang.org/protobuf/proto" - pb "git.daviestechlabs.io/daviestechlabs/handler-base/messages/proto" +pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb" ) // ──────────────────────────────────────────────────────────────────────────── -// Test fixtures — equivalent data across all three encodings +// Test fixtures — proto message constructors // ──────────────────────────────────────────────────────────────────────────── -// chatRequestMap is the legacy map[string]any representation. -func chatRequestMap() map[string]any { - return map[string]any{ - "request_id": "req-abc-123", - "user_id": "user-42", - "message": "What is the capital of France?", - "query": "", - "premium": true, - "enable_rag": true, - "enable_reranker": true, - "enable_streaming": false, - "top_k": 10, - "collection": "documents", - "enable_tts": false, - "system_prompt": "You are a helpful assistant.", - "response_subject": "ai.chat.response.req-abc-123", - } -} - -// chatRequestStruct is the typed struct representation. -func chatRequestStruct() ChatRequest { - return ChatRequest{ - RequestID: "req-abc-123", - UserID: "user-42", - Message: "What is the capital of France?", - Premium: true, - EnableRAG: true, - EnableReranker: true, - TopK: 10, - Collection: "documents", - SystemPrompt: "You are a helpful assistant.", - ResponseSubject: "ai.chat.response.req-abc-123", - } -} - -// chatRequestProto is the protobuf representation. func chatRequestProto() *pb.ChatRequest { - return &pb.ChatRequest{ - RequestId: "req-abc-123", - UserId: "user-42", - Message: "What is the capital of France?", - Premium: true, - EnableRag: true, - EnableReranker: true, - TopK: 10, - Collection: "documents", - SystemPrompt: "You are a helpful assistant.", - ResponseSubject: "ai.chat.response.req-abc-123", - } +return &pb.ChatRequest{ +RequestId: "req-abc-123", +UserId: "user-42", +Message: "What is the capital of France?", +Premium: true, +EnableRag: true, +EnableReranker: true, +TopK: 10, +Collection: "documents", +SystemPrompt: "You are a helpful assistant.", +ResponseSubject: "ai.chat.response.req-abc-123", } - -// voiceResponseMap is a voice response with a 16 KB audio payload. -func voiceResponseMap() map[string]any { - return map[string]any{ - "request_id": "vr-001", - "response": "The capital of France is Paris.", - "audio": make([]byte, 16384), - "transcription": "What is the capital of France?", - } -} - -func voiceResponseStruct() VoiceResponse { - return VoiceResponse{ - RequestID: "vr-001", - Response: "The capital of France is Paris.", - Audio: make([]byte, 16384), - Transcription: "What is the capital of France?", - } } func voiceResponseProto() *pb.VoiceResponse { - return &pb.VoiceResponse{ - RequestId: "vr-001", - Response: "The capital of France is Paris.", - Audio: make([]byte, 16384), - Transcription: "What is the capital of France?", - } +return &pb.VoiceResponse{ +RequestId: "vr-001", +Response: "The capital of France is Paris.", +Audio: make([]byte, 16384), +Transcription: "What is the capital of France?", } - -// ttsChunkMap simulates a streaming audio chunk (~32 KB). -func ttsChunkMap() map[string]any { - return map[string]any{ - "session_id": "tts-sess-99", - "chunk_index": 3, - "total_chunks": 12, - "audio_b64": string(make([]byte, 32768)), // old: base64 string - "is_last": false, - "timestamp": time.Now().Unix(), - "sample_rate": 24000, - } -} - -func ttsChunkStruct() TTSAudioChunk { - return TTSAudioChunk{ - SessionID: "tts-sess-99", - ChunkIndex: 3, - TotalChunks: 12, - Audio: make([]byte, 32768), // new: raw bytes - IsLast: false, - Timestamp: time.Now().Unix(), - SampleRate: 24000, - } } func ttsChunkProto() *pb.TTSAudioChunk { - return &pb.TTSAudioChunk{ - SessionId: "tts-sess-99", - ChunkIndex: 3, - TotalChunks: 12, - Audio: make([]byte, 32768), - IsLast: false, - Timestamp: time.Now().Unix(), - SampleRate: 24000, - } +return &pb.TTSAudioChunk{ +SessionId: "tts-sess-99", +ChunkIndex: 3, +TotalChunks: 12, +Audio: make([]byte, 32768), +IsLast: false, +Timestamp: time.Now().Unix(), +SampleRate: 24000, +} } // ──────────────────────────────────────────────────────────────────────────── @@ -145,371 +60,253 @@ func ttsChunkProto() *pb.TTSAudioChunk { // ──────────────────────────────────────────────────────────────────────────── func TestWireSize(t *testing.T) { - tests := []struct { - name string - mapData any - structVal any - protoMsg proto.Message - }{ - {"ChatRequest", chatRequestMap(), chatRequestStruct(), chatRequestProto()}, - {"VoiceResponse", voiceResponseMap(), voiceResponseStruct(), voiceResponseProto()}, - {"TTSAudioChunk", ttsChunkMap(), ttsChunkStruct(), ttsChunkProto()}, - } +tests := []struct { +name string +protoMsg proto.Message +}{ +{"ChatRequest", chatRequestProto()}, +{"VoiceResponse", voiceResponseProto()}, +{"TTSAudioChunk", ttsChunkProto()}, +} - for _, tt := range tests { - mapBytes, _ := msgpack.Marshal(tt.mapData) - structBytes, _ := msgpack.Marshal(tt.structVal) - protoBytes, _ := proto.Marshal(tt.protoMsg) - - t.Logf("%-16s map=%5d B struct=%5d B proto=%5d B (struct saves %.0f%%, proto saves %.0f%%)", - tt.name, - len(mapBytes), len(structBytes), len(protoBytes), - 100*(1-float64(len(structBytes))/float64(len(mapBytes))), - 100*(1-float64(len(protoBytes))/float64(len(mapBytes))), - ) - } +for _, tt := range tests { +protoBytes, _ := proto.Marshal(tt.protoMsg) +t.Logf("%-16s proto=%5d B", tt.name, len(protoBytes)) +} } // ──────────────────────────────────────────────────────────────────────────── // Encode benchmarks // ──────────────────────────────────────────────────────────────────────────── -func BenchmarkEncode_ChatRequest_MsgpackMap(b *testing.B) { - data := chatRequestMap() - b.ResetTimer() - for b.Loop() { - _, _ = msgpack.Marshal(data) - } +func BenchmarkEncode_ChatRequest(b *testing.B) { +data := chatRequestProto() +b.ResetTimer() +for b.Loop() { +_, _ = proto.Marshal(data) +} } -func BenchmarkEncode_ChatRequest_MsgpackStruct(b *testing.B) { - data := chatRequestStruct() - b.ResetTimer() - for b.Loop() { - _, _ = msgpack.Marshal(data) - } +func BenchmarkEncode_VoiceResponse(b *testing.B) { +data := voiceResponseProto() +b.ResetTimer() +for b.Loop() { +_, _ = proto.Marshal(data) +} } -func BenchmarkEncode_ChatRequest_Protobuf(b *testing.B) { - data := chatRequestProto() - b.ResetTimer() - for b.Loop() { - _, _ = proto.Marshal(data) - } +func BenchmarkEncode_TTSChunk(b *testing.B) { +data := ttsChunkProto() +b.ResetTimer() +for b.Loop() { +_, _ = proto.Marshal(data) } - -func BenchmarkEncode_VoiceResponse_MsgpackMap(b *testing.B) { - data := voiceResponseMap() - b.ResetTimer() - for b.Loop() { - _, _ = msgpack.Marshal(data) - } -} - -func BenchmarkEncode_VoiceResponse_MsgpackStruct(b *testing.B) { - data := voiceResponseStruct() - b.ResetTimer() - for b.Loop() { - _, _ = msgpack.Marshal(data) - } -} - -func BenchmarkEncode_VoiceResponse_Protobuf(b *testing.B) { - data := voiceResponseProto() - b.ResetTimer() - for b.Loop() { - _, _ = proto.Marshal(data) - } -} - -func BenchmarkEncode_TTSChunk_MsgpackMap(b *testing.B) { - data := ttsChunkMap() - b.ResetTimer() - for b.Loop() { - _, _ = msgpack.Marshal(data) - } -} - -func BenchmarkEncode_TTSChunk_MsgpackStruct(b *testing.B) { - data := ttsChunkStruct() - b.ResetTimer() - for b.Loop() { - _, _ = msgpack.Marshal(data) - } -} - -func BenchmarkEncode_TTSChunk_Protobuf(b *testing.B) { - data := ttsChunkProto() - b.ResetTimer() - for b.Loop() { - _, _ = proto.Marshal(data) - } } // ──────────────────────────────────────────────────────────────────────────── // Decode benchmarks // ──────────────────────────────────────────────────────────────────────────── -func BenchmarkDecode_ChatRequest_MsgpackMap(b *testing.B) { - encoded, _ := msgpack.Marshal(chatRequestMap()) - b.ResetTimer() - for b.Loop() { - var m map[string]any - _ = msgpack.Unmarshal(encoded, &m) - } +func BenchmarkDecode_ChatRequest(b *testing.B) { +encoded, _ := proto.Marshal(chatRequestProto()) +b.ResetTimer() +for b.Loop() { +var m pb.ChatRequest +_ = proto.Unmarshal(encoded, &m) +} } -func BenchmarkDecode_ChatRequest_MsgpackStruct(b *testing.B) { - encoded, _ := msgpack.Marshal(chatRequestStruct()) - b.ResetTimer() - for b.Loop() { - var m ChatRequest - _ = msgpack.Unmarshal(encoded, &m) - } +func BenchmarkDecode_VoiceResponse(b *testing.B) { +encoded, _ := proto.Marshal(voiceResponseProto()) +b.ResetTimer() +for b.Loop() { +var m pb.VoiceResponse +_ = proto.Unmarshal(encoded, &m) +} } -func BenchmarkDecode_ChatRequest_Protobuf(b *testing.B) { - encoded, _ := proto.Marshal(chatRequestProto()) - b.ResetTimer() - for b.Loop() { - var m pb.ChatRequest - _ = proto.Unmarshal(encoded, &m) - } +func BenchmarkDecode_TTSChunk(b *testing.B) { +encoded, _ := proto.Marshal(ttsChunkProto()) +b.ResetTimer() +for b.Loop() { +var m pb.TTSAudioChunk +_ = proto.Unmarshal(encoded, &m) } - -func BenchmarkDecode_VoiceResponse_MsgpackMap(b *testing.B) { - encoded, _ := msgpack.Marshal(voiceResponseMap()) - b.ResetTimer() - for b.Loop() { - var m map[string]any - _ = msgpack.Unmarshal(encoded, &m) - } -} - -func BenchmarkDecode_VoiceResponse_MsgpackStruct(b *testing.B) { - encoded, _ := msgpack.Marshal(voiceResponseStruct()) - b.ResetTimer() - for b.Loop() { - var m VoiceResponse - _ = msgpack.Unmarshal(encoded, &m) - } -} - -func BenchmarkDecode_VoiceResponse_Protobuf(b *testing.B) { - encoded, _ := proto.Marshal(voiceResponseProto()) - b.ResetTimer() - for b.Loop() { - var m pb.VoiceResponse - _ = proto.Unmarshal(encoded, &m) - } -} - -func BenchmarkDecode_TTSChunk_MsgpackMap(b *testing.B) { - encoded, _ := msgpack.Marshal(ttsChunkMap()) - b.ResetTimer() - for b.Loop() { - var m map[string]any - _ = msgpack.Unmarshal(encoded, &m) - } -} - -func BenchmarkDecode_TTSChunk_MsgpackStruct(b *testing.B) { - encoded, _ := msgpack.Marshal(ttsChunkStruct()) - b.ResetTimer() - for b.Loop() { - var m TTSAudioChunk - _ = msgpack.Unmarshal(encoded, &m) - } -} - -func BenchmarkDecode_TTSChunk_Protobuf(b *testing.B) { - encoded, _ := proto.Marshal(ttsChunkProto()) - b.ResetTimer() - for b.Loop() { - var m pb.TTSAudioChunk - _ = proto.Unmarshal(encoded, &m) - } } // ──────────────────────────────────────────────────────────────────────────── // Roundtrip benchmarks (encode + decode) // ──────────────────────────────────────────────────────────────────────────── -func BenchmarkRoundtrip_ChatRequest_MsgpackMap(b *testing.B) { - data := chatRequestMap() - b.ResetTimer() - for b.Loop() { - enc, _ := msgpack.Marshal(data) - var dec map[string]any - _ = msgpack.Unmarshal(enc, &dec) - } +func BenchmarkRoundtrip_ChatRequest(b *testing.B) { +data := chatRequestProto() +b.ResetTimer() +for b.Loop() { +enc, _ := proto.Marshal(data) +var dec pb.ChatRequest +_ = proto.Unmarshal(enc, &dec) } - -func BenchmarkRoundtrip_ChatRequest_MsgpackStruct(b *testing.B) { - data := chatRequestStruct() - b.ResetTimer() - for b.Loop() { - enc, _ := msgpack.Marshal(data) - var dec ChatRequest - _ = msgpack.Unmarshal(enc, &dec) - } -} - -func BenchmarkRoundtrip_ChatRequest_Protobuf(b *testing.B) { - data := chatRequestProto() - b.ResetTimer() - for b.Loop() { - enc, _ := proto.Marshal(data) - var dec pb.ChatRequest - _ = proto.Unmarshal(enc, &dec) - } } // ──────────────────────────────────────────────────────────────────────────── -// Typed struct unit tests — verify roundtrip correctness +// Correctness tests — verify proto roundtrip // ──────────────────────────────────────────────────────────────────────────── func TestRoundtrip_ChatRequest(t *testing.T) { - orig := chatRequestStruct() - data, err := msgpack.Marshal(orig) - if err != nil { - t.Fatal(err) - } - var dec ChatRequest - if err := msgpack.Unmarshal(data, &dec); err != nil { - t.Fatal(err) - } - if dec.RequestID != orig.RequestID { - t.Errorf("RequestID = %q, want %q", dec.RequestID, orig.RequestID) - } - if dec.Message != orig.Message { - t.Errorf("Message = %q, want %q", dec.Message, orig.Message) - } - if dec.TopK != orig.TopK { - t.Errorf("TopK = %d, want %d", dec.TopK, orig.TopK) - } - if dec.Premium != orig.Premium { - t.Errorf("Premium = %v, want %v", dec.Premium, orig.Premium) - } - if dec.EffectiveQuery() != orig.Message { - t.Errorf("EffectiveQuery() = %q, want %q", dec.EffectiveQuery(), orig.Message) - } +orig := chatRequestProto() +data, err := proto.Marshal(orig) +if err != nil { +t.Fatal(err) +} +var dec pb.ChatRequest +if err := proto.Unmarshal(data, &dec); err != nil { +t.Fatal(err) +} +if dec.GetRequestId() != orig.GetRequestId() { +t.Errorf("RequestId = %q, want %q", dec.GetRequestId(), orig.GetRequestId()) +} +if dec.GetMessage() != orig.GetMessage() { +t.Errorf("Message = %q, want %q", dec.GetMessage(), orig.GetMessage()) +} +if dec.GetTopK() != orig.GetTopK() { +t.Errorf("TopK = %d, want %d", dec.GetTopK(), orig.GetTopK()) +} +if dec.GetPremium() != orig.GetPremium() { +t.Errorf("Premium = %v, want %v", dec.GetPremium(), orig.GetPremium()) +} +if EffectiveQuery(&dec) != orig.GetMessage() { +t.Errorf("EffectiveQuery() = %q, want %q", EffectiveQuery(&dec), orig.GetMessage()) +} } func TestRoundtrip_VoiceResponse(t *testing.T) { - orig := voiceResponseStruct() - data, err := msgpack.Marshal(orig) - if err != nil { - t.Fatal(err) - } - var dec VoiceResponse - if err := msgpack.Unmarshal(data, &dec); err != nil { - t.Fatal(err) - } - if dec.RequestID != orig.RequestID { - t.Errorf("RequestID mismatch") - } - if len(dec.Audio) != len(orig.Audio) { - t.Errorf("Audio len = %d, want %d", len(dec.Audio), len(orig.Audio)) - } - if dec.Transcription != orig.Transcription { - t.Errorf("Transcription mismatch") - } +orig := voiceResponseProto() +data, err := proto.Marshal(orig) +if err != nil { +t.Fatal(err) +} +var dec pb.VoiceResponse +if err := proto.Unmarshal(data, &dec); err != nil { +t.Fatal(err) +} +if dec.GetRequestId() != orig.GetRequestId() { +t.Errorf("RequestId mismatch") +} +if len(dec.GetAudio()) != len(orig.GetAudio()) { +t.Errorf("Audio len = %d, want %d", len(dec.GetAudio()), len(orig.GetAudio())) +} +if dec.GetTranscription() != orig.GetTranscription() { +t.Errorf("Transcription mismatch") +} } func TestRoundtrip_TTSAudioChunk(t *testing.T) { - orig := ttsChunkStruct() - data, err := msgpack.Marshal(orig) - if err != nil { - t.Fatal(err) - } - var dec TTSAudioChunk - if err := msgpack.Unmarshal(data, &dec); err != nil { - t.Fatal(err) - } - if dec.SessionID != orig.SessionID { - t.Errorf("SessionID mismatch") - } - if dec.ChunkIndex != orig.ChunkIndex { - t.Errorf("ChunkIndex = %d, want %d", dec.ChunkIndex, orig.ChunkIndex) - } - if len(dec.Audio) != len(orig.Audio) { - t.Errorf("Audio len = %d, want %d", len(dec.Audio), len(orig.Audio)) - } - if dec.SampleRate != orig.SampleRate { - t.Errorf("SampleRate = %d, want %d", dec.SampleRate, orig.SampleRate) - } +orig := ttsChunkProto() +data, err := proto.Marshal(orig) +if err != nil { +t.Fatal(err) +} +var dec pb.TTSAudioChunk +if err := proto.Unmarshal(data, &dec); err != nil { +t.Fatal(err) +} +if dec.GetSessionId() != orig.GetSessionId() { +t.Errorf("SessionId mismatch") +} +if dec.GetChunkIndex() != orig.GetChunkIndex() { +t.Errorf("ChunkIndex = %d, want %d", dec.GetChunkIndex(), orig.GetChunkIndex()) +} +if len(dec.GetAudio()) != len(orig.GetAudio()) { +t.Errorf("Audio len = %d, want %d", len(dec.GetAudio()), len(orig.GetAudio())) +} +if dec.GetSampleRate() != orig.GetSampleRate() { +t.Errorf("SampleRate = %d, want %d", dec.GetSampleRate(), orig.GetSampleRate()) +} } func TestRoundtrip_PipelineTrigger(t *testing.T) { - orig := PipelineTrigger{ - RequestID: "pip-001", - Pipeline: "document-ingestion", - Parameters: map[string]any{"source": "s3://bucket/data"}, - } - data, err := msgpack.Marshal(orig) - if err != nil { - t.Fatal(err) - } - var dec PipelineTrigger - if err := msgpack.Unmarshal(data, &dec); err != nil { - t.Fatal(err) - } - if dec.Pipeline != orig.Pipeline { - t.Errorf("Pipeline = %q, want %q", dec.Pipeline, orig.Pipeline) - } - if dec.Parameters["source"] != orig.Parameters["source"] { - t.Errorf("Parameters[source] mismatch") - } +orig := &pb.PipelineTrigger{ +RequestId: "pip-001", +Pipeline: "document-ingestion", +Parameters: map[string]string{"source": "s3://bucket/data"}, +} +data, err := proto.Marshal(orig) +if err != nil { +t.Fatal(err) +} +var dec pb.PipelineTrigger +if err := proto.Unmarshal(data, &dec); err != nil { +t.Fatal(err) +} +if dec.GetPipeline() != orig.GetPipeline() { +t.Errorf("Pipeline = %q, want %q", dec.GetPipeline(), orig.GetPipeline()) +} +if dec.GetParameters()["source"] != orig.GetParameters()["source"] { +t.Errorf("Parameters[source] mismatch") +} } func TestRoundtrip_STTTranscription(t *testing.T) { - orig := STTTranscription{ - SessionID: "stt-001", - Transcript: "hello world", - Sequence: 5, - IsPartial: false, - IsFinal: true, - Timestamp: time.Now().Unix(), - SpeakerID: "speaker-1", - HasVoiceActivity: true, - State: "listening", - } - data, err := msgpack.Marshal(orig) - if err != nil { - t.Fatal(err) - } - var dec STTTranscription - if err := msgpack.Unmarshal(data, &dec); err != nil { - t.Fatal(err) - } - if dec.Transcript != orig.Transcript { - t.Errorf("Transcript = %q, want %q", dec.Transcript, orig.Transcript) - } - if dec.IsFinal != orig.IsFinal { - t.Error("IsFinal mismatch") - } +orig := &pb.STTTranscription{ +SessionId: "stt-001", +Transcript: "hello world", +Sequence: 5, +IsPartial: false, +IsFinal: true, +Timestamp: time.Now().Unix(), +SpeakerId: "speaker-1", +HasVoiceActivity: true, +State: "listening", +} +data, err := proto.Marshal(orig) +if err != nil { +t.Fatal(err) +} +var dec pb.STTTranscription +if err := proto.Unmarshal(data, &dec); err != nil { +t.Fatal(err) +} +if dec.GetTranscript() != orig.GetTranscript() { +t.Errorf("Transcript = %q, want %q", dec.GetTranscript(), orig.GetTranscript()) +} +if dec.GetIsFinal() != orig.GetIsFinal() { +t.Error("IsFinal mismatch") +} } func TestRoundtrip_ErrorResponse(t *testing.T) { - orig := ErrorResponse{Error: true, Message: "something broke", Type: "InternalError"} - data, err := msgpack.Marshal(orig) - if err != nil { - t.Fatal(err) - } - var dec ErrorResponse - if err := msgpack.Unmarshal(data, &dec); err != nil { - t.Fatal(err) - } - if !dec.Error || dec.Message != "something broke" || dec.Type != "InternalError" { - t.Errorf("ErrorResponse roundtrip mismatch: %+v", dec) - } +orig := &pb.ErrorResponse{Error: true, Message: "something broke", Type: "InternalError"} +data, err := proto.Marshal(orig) +if err != nil { +t.Fatal(err) +} +var dec pb.ErrorResponse +if err := proto.Unmarshal(data, &dec); err != nil { +t.Fatal(err) +} +if !dec.GetError() || dec.GetMessage() != "something broke" || dec.GetType() != "InternalError" { +t.Errorf("ErrorResponse roundtrip mismatch: %+v", &dec) +} +} + +func TestEffectiveQuery_MessageSet(t *testing.T) { +req := &pb.ChatRequest{Message: "hello", Query: "world"} +if got := EffectiveQuery(req); got != "hello" { +t.Errorf("EffectiveQuery() = %q, want %q", got, "hello") +} +} + +func TestEffectiveQuery_FallbackToQuery(t *testing.T) { +req := &pb.ChatRequest{Query: "world"} +if got := EffectiveQuery(req); got != "world" { +t.Errorf("EffectiveQuery() = %q, want %q", got, "world") +} } func TestTimestamp(t *testing.T) { - ts := Timestamp() - now := time.Now().Unix() - if ts < now-1 || ts > now+1 { - t.Errorf("Timestamp() = %d, expected ~%d", ts, now) - } +ts := Timestamp() +now := time.Now().Unix() +if ts < now-1 || ts > now+1 { +t.Errorf("Timestamp() = %d, expected ~%d", ts, now) +} } diff --git a/messages/messages.go b/messages/messages.go index 93a5cdb..9b3ad37 100644 --- a/messages/messages.go +++ b/messages/messages.go @@ -1,224 +1,69 @@ -// Package messages defines typed NATS message structs for all services. +// Package messages re-exports protobuf message types and provides NATS +// subject constants plus helper functions. // -// Using typed structs with short msgpack field tags instead of map[string]any -// provides compile-time safety, smaller wire size (integer-like short keys vs -// full string keys), and faster encode/decode by avoiding interface{} boxing. -// -// Audio data uses raw []byte instead of base64-encoded strings — msgpack -// supports binary natively, eliminating the 33% base64 overhead. +// The canonical type definitions live in the generated package +// gen/messagespb (from proto/messages/v1/messages.proto). +// This package provides type aliases so existing callers can keep using +// messages.ChatRequest, etc., while the wire format is now protobuf. package messages -import "time" +import ( +"time" -// ──────────────────────────────────────────────────────────────────────────── -// Pipeline Bridge -// ──────────────────────────────────────────────────────────────────────────── +pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb" +) -// PipelineTrigger is the request to start a pipeline. -type PipelineTrigger struct { -RequestID string `msgpack:"request_id" json:"request_id"` -Pipeline string `msgpack:"pipeline" json:"pipeline"` -Parameters map[string]any `msgpack:"parameters,omitempty" json:"parameters,omitempty"` -} +// ════════════════════════════════════════════════════════════════════════════ +// Type aliases — use these or import gen/messagespb directly. +// ════════════════════════════════════════════════════════════════════════════ -// PipelineStatus is the response / status update for a pipeline run. -type PipelineStatus struct { -RequestID string `msgpack:"request_id" json:"request_id"` -Status string `msgpack:"status" json:"status"` -RunID string `msgpack:"run_id,omitempty" json:"run_id,omitempty"` -Engine string `msgpack:"engine,omitempty" json:"engine,omitempty"` -Pipeline string `msgpack:"pipeline,omitempty" json:"pipeline,omitempty"` -SubmittedAt string `msgpack:"submitted_at,omitempty" json:"submitted_at,omitempty"` -Error string `msgpack:"error,omitempty" json:"error,omitempty"` -AvailablePipelines []string `msgpack:"available_pipelines,omitempty" json:"available_pipelines,omitempty"` -} +// Common +type ErrorResponse = pb.ErrorResponse -// ──────────────────────────────────────────────────────────────────────────── -// Chat Handler -// ──────────────────────────────────────────────────────────────────────────── +// Chat +type LoginEvent = pb.LoginEvent +type GreetingRequest = pb.GreetingRequest +type GreetingResponse = pb.GreetingResponse +type ChatRequest = pb.ChatRequest +type ChatResponse = pb.ChatResponse +type ChatStreamChunk = pb.ChatStreamChunk -// ChatRequest is an incoming chat message. -type ChatRequest struct { -RequestID string `msgpack:"request_id" json:"request_id"` -UserID string `msgpack:"user_id" json:"user_id"` -Message string `msgpack:"message" json:"message"` -Query string `msgpack:"query,omitempty" json:"query,omitempty"` -Premium bool `msgpack:"premium,omitempty" json:"premium,omitempty"` -EnableRAG bool `msgpack:"enable_rag,omitempty" json:"enable_rag,omitempty"` -EnableReranker bool `msgpack:"enable_reranker,omitempty" json:"enable_reranker,omitempty"` -EnableStreaming bool `msgpack:"enable_streaming,omitempty" json:"enable_streaming,omitempty"` -TopK int `msgpack:"top_k,omitempty" json:"top_k,omitempty"` -Collection string `msgpack:"collection,omitempty" json:"collection,omitempty"` -EnableTTS bool `msgpack:"enable_tts,omitempty" json:"enable_tts,omitempty"` -SystemPrompt string `msgpack:"system_prompt,omitempty" json:"system_prompt,omitempty"` -ResponseSubject string `msgpack:"response_subject,omitempty" json:"response_subject,omitempty"` -} +// Voice +type VoiceRequest = pb.VoiceRequest +type VoiceResponse = pb.VoiceResponse +type DocumentSource = pb.DocumentSource + +// TTS +type TTSRequest = pb.TTSRequest +type TTSAudioChunk = pb.TTSAudioChunk +type TTSFullResponse = pb.TTSFullResponse +type TTSStatus = pb.TTSStatus +type TTSVoiceInfo = pb.TTSVoiceInfo +type TTSVoiceListResponse = pb.TTSVoiceListResponse +type TTSVoiceRefreshResponse = pb.TTSVoiceRefreshResponse + +// STT +type STTStreamMessage = pb.STTStreamMessage +type STTTranscription = pb.STTTranscription +type STTInterrupt = pb.STTInterrupt + +// Pipeline +type PipelineTrigger = pb.PipelineTrigger +type PipelineStatus = pb.PipelineStatus + +// ════════════════════════════════════════════════════════════════════════════ +// Helpers +// ════════════════════════════════════════════════════════════════════════════ // EffectiveQuery returns Message or falls back to Query. -func (c *ChatRequest) EffectiveQuery() string { -if c.Message != "" { -return c.Message +func EffectiveQuery(c *ChatRequest) string { +if c.GetMessage() != "" { +return c.GetMessage() } -return c.Query +return c.GetQuery() } -// ChatResponse is the full reply to a chat request. -type ChatResponse struct { -UserID string `msgpack:"user_id" json:"user_id"` -Response string `msgpack:"response" json:"response"` -ResponseText string `msgpack:"response_text" json:"response_text"` -UsedRAG bool `msgpack:"used_rag" json:"used_rag"` -RAGSources []string `msgpack:"rag_sources,omitempty" json:"rag_sources,omitempty"` -Success bool `msgpack:"success" json:"success"` -Audio []byte `msgpack:"audio,omitempty" json:"audio,omitempty"` -Error string `msgpack:"error,omitempty" json:"error,omitempty"` -} - -// ChatStreamChunk is a single streaming chunk from an LLM response. -type ChatStreamChunk struct { -RequestID string `msgpack:"request_id" json:"request_id"` -Type string `msgpack:"type" json:"type"` -Content string `msgpack:"content" json:"content"` -Done bool `msgpack:"done" json:"done"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -} - -// ──────────────────────────────────────────────────────────────────────────── -// Voice Assistant -// ──────────────────────────────────────────────────────────────────────────── - -// VoiceRequest is an incoming voice-to-voice request. -type VoiceRequest struct { -RequestID string `msgpack:"request_id" json:"request_id"` -Audio []byte `msgpack:"audio" json:"audio"` -Language string `msgpack:"language,omitempty" json:"language,omitempty"` -Collection string `msgpack:"collection,omitempty" json:"collection,omitempty"` -} - -// VoiceResponse is the reply to a voice request. -type VoiceResponse struct { -RequestID string `msgpack:"request_id" json:"request_id"` -Response string `msgpack:"response" json:"response"` -Audio []byte `msgpack:"audio" json:"audio"` -Transcription string `msgpack:"transcription,omitempty" json:"transcription,omitempty"` -Sources []DocumentSource `msgpack:"sources,omitempty" json:"sources,omitempty"` -Error string `msgpack:"error,omitempty" json:"error,omitempty"` -} - -// DocumentSource is a RAG search result source. -type DocumentSource struct { -Text string `msgpack:"text" json:"text"` -Score float64 `msgpack:"score" json:"score"` -} - -// ──────────────────────────────────────────────────────────────────────────── -// TTS Module -// ──────────────────────────────────────────────────────────────────────────── - -// TTSRequest is a text-to-speech synthesis request. -type TTSRequest struct { -Text string `msgpack:"text" json:"text"` -Speaker string `msgpack:"speaker,omitempty" json:"speaker,omitempty"` -Language string `msgpack:"language,omitempty" json:"language,omitempty"` -SpeakerWavB64 string `msgpack:"speaker_wav_b64,omitempty" json:"speaker_wav_b64,omitempty"` -Stream bool `msgpack:"stream,omitempty" json:"stream,omitempty"` -} - -// TTSAudioChunk is a streamed audio chunk from TTS synthesis. -type TTSAudioChunk struct { -SessionID string `msgpack:"session_id" json:"session_id"` -ChunkIndex int `msgpack:"chunk_index" json:"chunk_index"` -TotalChunks int `msgpack:"total_chunks" json:"total_chunks"` -Audio []byte `msgpack:"audio" json:"audio"` -IsLast bool `msgpack:"is_last" json:"is_last"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -SampleRate int `msgpack:"sample_rate" json:"sample_rate"` -} - -// TTSFullResponse is a non-streamed TTS response (whole audio). -type TTSFullResponse struct { -SessionID string `msgpack:"session_id" json:"session_id"` -Audio []byte `msgpack:"audio" json:"audio"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -SampleRate int `msgpack:"sample_rate" json:"sample_rate"` -} - -// TTSStatus is a TTS processing status update. -type TTSStatus struct { -SessionID string `msgpack:"session_id" json:"session_id"` -Status string `msgpack:"status" json:"status"` -Message string `msgpack:"message" json:"message"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -} - -// TTSVoiceListResponse is the reply to a voice list request. -type TTSVoiceListResponse struct { -DefaultSpeaker string `msgpack:"default_speaker" json:"default_speaker"` -CustomVoices []TTSVoiceInfo `msgpack:"custom_voices" json:"custom_voices"` -LastRefresh int64 `msgpack:"last_refresh" json:"last_refresh"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -} - -// TTSVoiceInfo is summary info about a custom voice. -type TTSVoiceInfo struct { -Name string `msgpack:"name" json:"name"` -Language string `msgpack:"language" json:"language"` -ModelType string `msgpack:"model_type" json:"model_type"` -CreatedAt string `msgpack:"created_at" json:"created_at"` -} - -// TTSVoiceRefreshResponse is the reply to a voice refresh request. -type TTSVoiceRefreshResponse struct { -Count int `msgpack:"count" json:"count"` -CustomVoices []TTSVoiceInfo `msgpack:"custom_voices" json:"custom_voices"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -} - -// ──────────────────────────────────────────────────────────────────────────── -// STT Module -// ──────────────────────────────────────────────────────────────────────────── - -// STTStreamMessage is any message on the ai.voice.stream.{session} subject. -type STTStreamMessage struct { -Type string `msgpack:"type" json:"type"` -Audio []byte `msgpack:"audio,omitempty" json:"audio,omitempty"` -State string `msgpack:"state,omitempty" json:"state,omitempty"` -SpeakerID string `msgpack:"speaker_id,omitempty" json:"speaker_id,omitempty"` -} - -// STTTranscription is the transcription result published by the STT module. -type STTTranscription struct { -SessionID string `msgpack:"session_id" json:"session_id"` -Transcript string `msgpack:"transcript" json:"transcript"` -Sequence int `msgpack:"sequence" json:"sequence"` -IsPartial bool `msgpack:"is_partial" json:"is_partial"` -IsFinal bool `msgpack:"is_final" json:"is_final"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -SpeakerID string `msgpack:"speaker_id" json:"speaker_id"` -HasVoiceActivity bool `msgpack:"has_voice_activity" json:"has_voice_activity"` -State string `msgpack:"state" json:"state"` -} - -// STTInterrupt is published when the STT module detects a user interrupt. -type STTInterrupt struct { -SessionID string `msgpack:"session_id" json:"session_id"` -Type string `msgpack:"type" json:"type"` -Timestamp int64 `msgpack:"timestamp" json:"timestamp"` -SpeakerID string `msgpack:"speaker_id" json:"speaker_id"` -} - -// ──────────────────────────────────────────────────────────────────────────── -// Common / Error -// ──────────────────────────────────────────────────────────────────────────── - -// ErrorResponse is the standard error reply from any handler. -type ErrorResponse struct { -Error bool `msgpack:"error" json:"error"` -Message string `msgpack:"message" json:"message"` -Type string `msgpack:"type" json:"type"` -} - -// Timestamp returns the current Unix timestamp (helper for message construction). +// Timestamp returns the current Unix timestamp. func Timestamp() int64 { return time.Now().Unix() } diff --git a/natsutil/natsutil.go b/natsutil/natsutil.go index 747d66e..2f75714 100644 --- a/natsutil/natsutil.go +++ b/natsutil/natsutil.go @@ -1,70 +1,70 @@ -// Package natsutil provides a NATS/JetStream client with msgpack serialization. +// Package natsutil provides a NATS/JetStream client with protobuf serialization. package natsutil import ( - "fmt" - "log/slog" - "time" +"fmt" +"log/slog" +"time" - "github.com/nats-io/nats.go" - "github.com/vmihailenco/msgpack/v5" +"github.com/nats-io/nats.go" +"google.golang.org/protobuf/proto" ) -// Client wraps a NATS connection with msgpack helpers. +// Client wraps a NATS connection with protobuf helpers. type Client struct { - nc *nats.Conn - js nats.JetStreamContext - subs []*nats.Subscription - url string - opts []nats.Option +nc *nats.Conn +js nats.JetStreamContext +subs []*nats.Subscription +url string +opts []nats.Option } // New creates a NATS client configured to connect to the given URL. // Optional NATS options (e.g. credentials) can be appended. func New(url string, opts ...nats.Option) *Client { - defaults := []nats.Option{ - nats.ReconnectWait(2 * time.Second), - nats.MaxReconnects(-1), - nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { - slog.Warn("NATS disconnected", "error", err) - }), - nats.ReconnectHandler(func(_ *nats.Conn) { - slog.Info("NATS reconnected") - }), - } - return &Client{ - url: url, - opts: append(defaults, opts...), - } +defaults := []nats.Option{ +nats.ReconnectWait(2 * time.Second), +nats.MaxReconnects(-1), +nats.DisconnectErrHandler(func(_ *nats.Conn, err error) { +slog.Warn("NATS disconnected", "error", err) +}), +nats.ReconnectHandler(func(_ *nats.Conn) { +slog.Info("NATS reconnected") +}), +} +return &Client{ +url: url, +opts: append(defaults, opts...), +} } // Connect establishes the NATS connection and JetStream context. func (c *Client) Connect() error { - nc, err := nats.Connect(c.url, c.opts...) - if err != nil { - return fmt.Errorf("nats connect: %w", err) - } - js, err := nc.JetStream() - if err != nil { - nc.Close() - return fmt.Errorf("jetstream: %w", err) - } - c.nc = nc - c.js = js - slog.Info("connected to NATS", "url", c.url) - return nil +nc, err := nats.Connect(c.url, c.opts...) +if err != nil { +return fmt.Errorf("nats connect: %w", err) +} +js, err := nc.JetStream() +if err != nil { +nc.Close() +return fmt.Errorf("jetstream: %w", err) +} +c.nc = nc +c.js = js +slog.Info("connected to NATS", "url", c.url) +return nil } // Close drains subscriptions and closes the connection. func (c *Client) Close() { - if c.nc == nil { - return - } - for _, sub := range c.subs { - _ = sub.Drain() - } - c.nc.Close() - slog.Info("NATS connection closed") +if c.nc == nil { +return +} +for _, sub := range c.subs { +_ = sub.Drain() +} +c.nc.Close() +slog.Info("NATS connection closed") } // Conn returns the underlying *nats.Conn. @@ -75,68 +75,56 @@ func (c *Client) JS() nats.JetStreamContext { return c.js } // IsConnected returns true if the NATS connection is active. func (c *Client) IsConnected() bool { - return c.nc != nil && c.nc.IsConnected() +return c.nc != nil && c.nc.IsConnected() } // Subscribe subscribes to a subject with an optional queue group. // The handler receives the raw *nats.Msg. func (c *Client) Subscribe(subject string, handler nats.MsgHandler, queue string) error { - var sub *nats.Subscription - var err error - if queue != "" { - sub, err = c.nc.QueueSubscribe(subject, queue, handler) - slog.Info("subscribed", "subject", subject, "queue", queue) - } else { - sub, err = c.nc.Subscribe(subject, handler) - slog.Info("subscribed", "subject", subject) - } - if err != nil { - return fmt.Errorf("subscribe %s: %w", subject, err) - } - c.subs = append(c.subs, sub) - return nil +var sub *nats.Subscription +var err error +if queue != "" { +sub, err = c.nc.QueueSubscribe(subject, queue, handler) +slog.Info("subscribed", "subject", subject, "queue", queue) +} else { +sub, err = c.nc.Subscribe(subject, handler) +slog.Info("subscribed", "subject", subject) +} +if err != nil { +return fmt.Errorf("subscribe %s: %w", subject, err) +} +c.subs = append(c.subs, sub) +return nil } -// Publish encodes data as msgpack and publishes to the subject. -func (c *Client) Publish(subject string, data any) error { - payload, err := msgpack.Marshal(data) - if err != nil { - return fmt.Errorf("msgpack marshal: %w", err) - } - return c.nc.Publish(subject, payload) +// Publish encodes data as protobuf and publishes to the subject. +func (c *Client) Publish(subject string, data proto.Message) error { +payload, err := proto.Marshal(data) +if err != nil { +return fmt.Errorf("proto marshal: %w", err) +} +return c.nc.Publish(subject, payload) } -// Request sends a msgpack-encoded request and decodes the response into result. -func (c *Client) Request(subject string, data any, result any, timeout time.Duration) error { - payload, err := msgpack.Marshal(data) - if err != nil { - return fmt.Errorf("msgpack marshal: %w", err) - } - msg, err := c.nc.Request(subject, payload, timeout) - if err != nil { - return fmt.Errorf("nats request: %w", err) - } - return msgpack.Unmarshal(msg.Data, result) +// PublishRaw publishes pre-encoded bytes to the subject. +func (c *Client) PublishRaw(subject string, data []byte) error { +return c.nc.Publish(subject, data) } -// DecodeMsgpack decodes msgpack-encoded NATS message data into dest. -func DecodeMsgpack(msg *nats.Msg, dest any) error { - return msgpack.Unmarshal(msg.Data, dest) +// Request sends a protobuf-encoded request and decodes the response into result. +func (c *Client) Request(subject string, data proto.Message, result proto.Message, timeout time.Duration) error { +payload, err := proto.Marshal(data) +if err != nil { +return fmt.Errorf("proto marshal: %w", err) +} +msg, err := c.nc.Request(subject, payload, timeout) +if err != nil { +return fmt.Errorf("nats request: %w", err) +} +return proto.Unmarshal(msg.Data, result) } -// Decode is a generic helper that unmarshals msgpack bytes into T. -// Usage: req, err := natsutil.Decode[messages.ChatRequest](msg.Data) -func Decode[T any](data []byte) (T, error) { - var v T - err := msgpack.Unmarshal(data, &v) - return v, err -} - -// DecodeMsgpackMap decodes msgpack data into a generic map. -func DecodeMsgpackMap(data []byte) (map[string]any, error) { - var m map[string]any - if err := msgpack.Unmarshal(data, &m); err != nil { - return nil, err - } - return m, nil +// Decode unmarshals protobuf bytes into dest. +func Decode(data []byte, dest proto.Message) error { +return proto.Unmarshal(data, dest) } diff --git a/natsutil/natsutil_test.go b/natsutil/natsutil_test.go index 6898a52..d93251c 100644 --- a/natsutil/natsutil_test.go +++ b/natsutil/natsutil_test.go @@ -3,254 +3,212 @@ package natsutil import ( "testing" - "github.com/vmihailenco/msgpack/v5" + "google.golang.org/protobuf/proto" + + pb "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb" ) // ──────────────────────────────────────────────────────────────────────────── -// DecodeMsgpackMap tests +// Decode tests // ──────────────────────────────────────────────────────────────────────────── -func TestDecodeMsgpackMap_Roundtrip(t *testing.T) { - orig := map[string]any{ - "request_id": "req-001", - "user_id": "user-42", - "premium": true, - "top_k": int64(10), // msgpack decodes ints as int64 +func TestDecode_ChatRequest_Roundtrip(t *testing.T) { + orig := &pb.ChatRequest{ + RequestId: "req-001", + UserId: "user-42", + Premium: true, + TopK: 10, } - data, err := msgpack.Marshal(orig) + data, err := proto.Marshal(orig) if err != nil { t.Fatal(err) } - decoded, err := DecodeMsgpackMap(data) - if err != nil { + var decoded pb.ChatRequest + if err := Decode(data, &decoded); err != nil { t.Fatal(err) } - if decoded["request_id"] != "req-001" { - t.Errorf("request_id = %v", decoded["request_id"]) + if decoded.GetRequestId() != "req-001" { + t.Errorf("RequestId = %v", decoded.GetRequestId()) } - if decoded["premium"] != true { - t.Errorf("premium = %v", decoded["premium"]) + if decoded.GetUserId() != "user-42" { + t.Errorf("UserId = %v", decoded.GetUserId()) + } + if decoded.GetPremium() != true { + t.Errorf("Premium = %v", decoded.GetPremium()) + } + if decoded.GetTopK() != 10 { + t.Errorf("TopK = %v", decoded.GetTopK()) } } -func TestDecodeMsgpackMap_Empty(t *testing.T) { - data, _ := msgpack.Marshal(map[string]any{}) - m, err := DecodeMsgpackMap(data) +func TestDecode_EmptyMessage(t *testing.T) { + data, err := proto.Marshal(&pb.ChatRequest{}) if err != nil { t.Fatal(err) } - if len(m) != 0 { - t.Errorf("expected empty map, got %v", m) + var decoded pb.ChatRequest + if err := Decode(data, &decoded); err != nil { + t.Fatal(err) + } + if decoded.GetRequestId() != "" { + t.Errorf("expected empty RequestId, got %q", decoded.GetRequestId()) } } -func TestDecodeMsgpackMap_InvalidData(t *testing.T) { - _, err := DecodeMsgpackMap([]byte{0xFF, 0xFE}) +func TestDecode_InvalidData(t *testing.T) { + err := Decode([]byte{0xFF, 0xFE}, &pb.ChatRequest{}) if err == nil { - t.Error("expected error for invalid msgpack data") + t.Error("expected error for invalid protobuf data") } } // ──────────────────────────────────────────────────────────────────────────── -// DecodeMsgpack (typed struct) tests +// Typed struct roundtrip tests // ──────────────────────────────────────────────────────────────────────────── -type testMessage struct { - RequestID string `msgpack:"request_id"` - UserID string `msgpack:"user_id"` - Count int `msgpack:"count"` - Active bool `msgpack:"active"` -} - -func TestDecodeMsgpackTyped_Roundtrip(t *testing.T) { - orig := testMessage{ - RequestID: "req-typed-001", - UserID: "user-7", - Count: 42, - Active: true, +func TestDecode_VoiceResponse_Roundtrip(t *testing.T) { + orig := &pb.VoiceResponse{ + RequestId: "vr-001", + Response: "The capital of France is Paris.", + Transcription: "What is the capital of France?", } - data, err := msgpack.Marshal(orig) + data, err := proto.Marshal(orig) if err != nil { t.Fatal(err) } - // Simulate nats.Msg data decoding. - var decoded testMessage - if err := msgpack.Unmarshal(data, &decoded); err != nil { + var decoded pb.VoiceResponse + if err := Decode(data, &decoded); err != nil { t.Fatal(err) } - if decoded.RequestID != orig.RequestID { - t.Errorf("RequestID = %q, want %q", decoded.RequestID, orig.RequestID) + if decoded.GetRequestId() != orig.GetRequestId() { + t.Errorf("RequestId = %q, want %q", decoded.GetRequestId(), orig.GetRequestId()) } - if decoded.Count != orig.Count { - t.Errorf("Count = %d, want %d", decoded.Count, orig.Count) + if decoded.GetResponse() != orig.GetResponse() { + t.Errorf("Response = %q, want %q", decoded.GetResponse(), orig.GetResponse()) } - if decoded.Active != orig.Active { - t.Errorf("Active = %v, want %v", decoded.Active, orig.Active) + if decoded.GetTranscription() != orig.GetTranscription() { + t.Errorf("Transcription = %q, want %q", decoded.GetTranscription(), orig.GetTranscription()) } } -// TestTypedStructDecodesMapEncoding verifies that a typed struct can be -// decoded from data that was encoded as map[string]any (backwards compat). -func TestTypedStructDecodesMapEncoding(t *testing.T) { - // Encode as map (the old way). - mapData := map[string]any{ - "request_id": "req-compat", - "user_id": "user-compat", - "count": int64(99), - "active": false, +func TestDecode_ErrorResponse_Roundtrip(t *testing.T) { + orig := &pb.ErrorResponse{ + Error: true, + Message: "something broke", + Type: "InternalError", } - data, err := msgpack.Marshal(mapData) + data, err := proto.Marshal(orig) if err != nil { t.Fatal(err) } - // Decode into typed struct (the new way). - var msg testMessage - if err := msgpack.Unmarshal(data, &msg); err != nil { + var decoded pb.ErrorResponse + if err := Decode(data, &decoded); err != nil { t.Fatal(err) } - if msg.RequestID != "req-compat" { - t.Errorf("RequestID = %q", msg.RequestID) + if !decoded.GetError() { + t.Error("expected Error=true") } - if msg.Count != 99 { - t.Errorf("Count = %d, want 99", msg.Count) + if decoded.GetMessage() != "something broke" { + t.Errorf("Message = %q", decoded.GetMessage()) + } + if decoded.GetType() != "InternalError" { + t.Errorf("Type = %q", decoded.GetType()) } } // ──────────────────────────────────────────────────────────────────────────── -// Binary data tests (audio []byte in msgpack) +// Binary data tests (audio []byte in protobuf) // ──────────────────────────────────────────────────────────────────────────── -type audioMessage struct { - SessionID string `msgpack:"session_id"` - Audio []byte `msgpack:"audio"` - SampleRate int `msgpack:"sample_rate"` -} - func TestBinaryDataRoundtrip(t *testing.T) { audio := make([]byte, 32768) for i := range audio { audio[i] = byte(i % 256) } - orig := audioMessage{ - SessionID: "sess-audio-001", + orig := &pb.TTSAudioChunk{ + SessionId: "sess-audio-001", Audio: audio, SampleRate: 24000, } - data, err := msgpack.Marshal(orig) + data, err := proto.Marshal(orig) if err != nil { t.Fatal(err) } - var decoded audioMessage - if err := msgpack.Unmarshal(data, &decoded); err != nil { + var decoded pb.TTSAudioChunk + if err := Decode(data, &decoded); err != nil { t.Fatal(err) } - if len(decoded.Audio) != len(orig.Audio) { - t.Fatalf("audio len = %d, want %d", len(decoded.Audio), len(orig.Audio)) + if len(decoded.GetAudio()) != len(orig.GetAudio()) { + t.Fatalf("audio len = %d, want %d", len(decoded.GetAudio()), len(orig.GetAudio())) } - for i := range decoded.Audio { - if decoded.Audio[i] != orig.Audio[i] { - t.Fatalf("audio[%d] = %d, want %d", i, decoded.Audio[i], orig.Audio[i]) + for i := range decoded.GetAudio() { + if decoded.GetAudio()[i] != orig.GetAudio()[i] { + t.Fatalf("audio[%d] = %d, want %d", i, decoded.GetAudio()[i], orig.GetAudio()[i]) } } } -// TestBinaryVsBase64Size shows the wire-size win of raw bytes vs base64 string. -func TestBinaryVsBase64Size(t *testing.T) { +// TestProtoWireSize shows protobuf wire size for binary payloads. +func TestProtoWireSize(t *testing.T) { audio := make([]byte, 16384) - // Old approach: base64 string in map. - import_b64 := make([]byte, (len(audio)*4+2)/3) // approximate base64 size - mapMsg := map[string]any{ - "session_id": "sess-1", - "audio_b64": string(import_b64), - } - mapData, _ := msgpack.Marshal(mapMsg) - - // New approach: raw bytes in struct. - structMsg := audioMessage{ - SessionID: "sess-1", + msg := &pb.TTSAudioChunk{ + SessionId: "sess-1", Audio: audio, } - structData, _ := msgpack.Marshal(structMsg) + data, _ := proto.Marshal(msg) - t.Logf("base64-in-map: %d bytes, raw-bytes-in-struct: %d bytes (%.0f%% smaller)", - len(mapData), len(structData), - 100*(1-float64(len(structData))/float64(len(mapData)))) + t.Logf("TTSAudioChunk with 16KB audio: %d bytes on wire", len(data)) } // ──────────────────────────────────────────────────────────────────────────── // Benchmarks // ──────────────────────────────────────────────────────────────────────────── -func BenchmarkEncodeMap(b *testing.B) { - data := map[string]any{ - "request_id": "req-bench", - "user_id": "user-bench", - "message": "What is the weather today?", - "premium": true, - "top_k": 10, +func BenchmarkEncode_ChatRequest(b *testing.B) { + data := &pb.ChatRequest{ + RequestId: "req-bench", + UserId: "user-bench", + Message: "What is the weather today?", + Premium: true, + TopK: 10, } for b.Loop() { - _, _ = msgpack.Marshal(data) + _, _ = proto.Marshal(data) } } -func BenchmarkEncodeStruct(b *testing.B) { - data := testMessage{ - RequestID: "req-bench", - UserID: "user-bench", - Count: 10, - Active: true, - } - for b.Loop() { - _, _ = msgpack.Marshal(data) - } -} - -func BenchmarkDecodeMap(b *testing.B) { - raw, _ := msgpack.Marshal(map[string]any{ - "request_id": "req-bench", - "user_id": "user-bench", - "message": "What is the weather today?", - "premium": true, - "top_k": 10, +func BenchmarkDecode_ChatRequest(b *testing.B) { + raw, _ := proto.Marshal(&pb.ChatRequest{ + RequestId: "req-bench", + UserId: "user-bench", + Message: "What is the weather today?", + Premium: true, + TopK: 10, }) for b.Loop() { - var m map[string]any - _ = msgpack.Unmarshal(raw, &m) + var m pb.ChatRequest + _ = Decode(raw, &m) } } -func BenchmarkDecodeStruct(b *testing.B) { - raw, _ := msgpack.Marshal(testMessage{ - RequestID: "req-bench", - UserID: "user-bench", - Count: 10, - Active: true, - }) - for b.Loop() { - var m testMessage - _ = msgpack.Unmarshal(raw, &m) - } -} - -func BenchmarkDecodeAudio32KB(b *testing.B) { - raw, _ := msgpack.Marshal(audioMessage{ - SessionID: "s1", +func BenchmarkDecode_Audio32KB(b *testing.B) { + raw, _ := proto.Marshal(&pb.TTSAudioChunk{ + SessionId: "s1", Audio: make([]byte, 32768), SampleRate: 24000, }) for b.Loop() { - var m audioMessage - _ = msgpack.Unmarshal(raw, &m) + var m pb.TTSAudioChunk + _ = Decode(raw, &m) } } diff --git a/proto/messages/v1/messages.proto b/proto/messages/v1/messages.proto new file mode 100644 index 0000000..86ec124 --- /dev/null +++ b/proto/messages/v1/messages.proto @@ -0,0 +1,257 @@ +// Homelab AI service message contracts. +// +// This is the single source of truth for all NATS message types. +// Generated Go code lives in handler-base/gen/messagespb. +// +// Naming: field numbers are stable across versions — add new fields, +// never reuse or renumber existing ones. + +syntax = "proto3"; + +package messages.v1; + +option go_package = "git.daviestechlabs.io/daviestechlabs/handler-base/gen/messagespb"; + +// ───────────────────────────────────────────────────────────────────────────── +// Common +// ───────────────────────────────────────────────────────────────────────────── + +// ErrorResponse is the standard error reply from any handler. +message ErrorResponse { + bool error = 1; + string message = 2; + string type = 3; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Chat (companions-frontend ↔ chat-handler) +// ───────────────────────────────────────────────────────────────────────────── + +// LoginEvent is published when a user authenticates. +// Subject: ai.chat.user.{user_id}.login +message LoginEvent { + string user_id = 1; + string username = 2; + string nickname = 3; + bool premium = 4; + int64 timestamp = 5; // Unix seconds +} + +// GreetingRequest asks the LLM to generate a personalised greeting. +// Subject: ai.chat.user.{user_id}.greeting.request +message GreetingRequest { + string user_id = 1; + string username = 2; + string nickname = 3; + bool premium = 4; +} + +// GreetingResponse carries the generated greeting text. +// Subject: ai.chat.user.{user_id}.greeting.response +message GreetingResponse { + string user_id = 1; + string greeting = 2; +} + +// ChatRequest is an incoming chat message routed via NATS. +// Subject: ai.chat.user.{user_id}.message +message ChatRequest { + string request_id = 1; + string user_id = 2; + string username = 3; + string message = 4; + string query = 5; // alternative to message (EffectiveQuery picks first non-empty) + bool premium = 6; + bool enable_rag = 7; + bool enable_reranker = 8; + bool enable_streaming = 9; + int32 top_k = 10; + string collection = 11; + bool enable_tts = 12; + string system_prompt = 13; + string response_subject = 14; +} + +// ChatResponse is the full reply to a ChatRequest. +// Subject: ai.chat.response.{request_id} (or ChatRequest.response_subject) +message ChatResponse { + string user_id = 1; + string response = 2; + string response_text = 3; + bool used_rag = 4; + repeated string rag_sources = 5; + bool success = 6; + bytes audio = 7; + string error = 8; +} + +// ChatStreamChunk is one piece of a streaming LLM response. +// Subject: ai.chat.response.stream.{request_id} +message ChatStreamChunk { + string request_id = 1; + string type = 2; // "chunk" | "done" + string content = 3; + bool done = 4; + int64 timestamp = 5; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Voice Assistant +// ───────────────────────────────────────────────────────────────────────────── + +// VoiceRequest is an incoming voice-to-voice request. +// Subject: ai.voice.request +message VoiceRequest { + string request_id = 1; + bytes audio = 2; + string language = 3; + string collection = 4; +} + +// DocumentSource is a single RAG search-result citation. +message DocumentSource { + string text = 1; + double score = 2; +} + +// VoiceResponse is the reply to a VoiceRequest. +// Subject: ai.voice.response.{request_id} +message VoiceResponse { + string request_id = 1; + string response = 2; + bytes audio = 3; + string transcription = 4; + repeated DocumentSource sources = 5; + string error = 6; +} + +// ───────────────────────────────────────────────────────────────────────────── +// TTS Module +// ───────────────────────────────────────────────────────────────────────────── + +// TTSRequest is a text-to-speech synthesis request. +// Subject: ai.voice.tts.request.{session_id} +message TTSRequest { + string text = 1; + string speaker = 2; + string language = 3; + string speaker_wav_b64 = 4; + bool stream = 5; +} + +// TTSAudioChunk is a streamed audio chunk from TTS synthesis. +// Subject: ai.voice.tts.audio.{session_id} +message TTSAudioChunk { + string session_id = 1; + int32 chunk_index = 2; + int32 total_chunks = 3; + bytes audio = 4; + bool is_last = 5; + int64 timestamp = 6; + int32 sample_rate = 7; +} + +// TTSFullResponse is a non-streamed TTS response (whole audio blob). +// Subject: ai.voice.tts.audio.{session_id} +message TTSFullResponse { + string session_id = 1; + bytes audio = 2; + int64 timestamp = 3; + int32 sample_rate = 4; +} + +// TTSStatus is a TTS processing status update. +// Subject: ai.voice.tts.status.{session_id} +message TTSStatus { + string session_id = 1; + string status = 2; + string message = 3; + int64 timestamp = 4; +} + +// TTSVoiceInfo is summary info about a custom voice. +message TTSVoiceInfo { + string name = 1; + string language = 2; + string model_type = 3; + string created_at = 4; +} + +// TTSVoiceListResponse is the reply to a voice list request. +// Subject: ai.voice.tts.voices.list (request-reply) +message TTSVoiceListResponse { + string default_speaker = 1; + repeated TTSVoiceInfo custom_voices = 2; + int64 last_refresh = 3; + int64 timestamp = 4; +} + +// TTSVoiceRefreshResponse is the reply to a voice refresh request. +// Subject: ai.voice.tts.voices.refresh (request-reply) +message TTSVoiceRefreshResponse { + int32 count = 1; + repeated TTSVoiceInfo custom_voices = 2; + int64 timestamp = 3; +} + +// ───────────────────────────────────────────────────────────────────────────── +// STT Module +// ───────────────────────────────────────────────────────────────────────────── + +// STTStreamMessage is any message on the ai.voice.stream.{session_id} subject. +message STTStreamMessage { + string type = 1; // "start" | "chunk" | "state_change" | "end" + bytes audio = 2; + string state = 3; + string speaker_id = 4; +} + +// STTTranscription is the transcription result published by the STT module. +// Subject: ai.voice.transcription.{session_id} +message STTTranscription { + string session_id = 1; + string transcript = 2; + int32 sequence = 3; + bool is_partial = 4; + bool is_final = 5; + int64 timestamp = 6; + string speaker_id = 7; + bool has_voice_activity = 8; + string state = 9; +} + +// STTInterrupt is published when the STT module detects a user interrupt. +// Subject: ai.voice.transcription.{session_id} +message STTInterrupt { + string session_id = 1; + string type = 2; // "interrupt" + int64 timestamp = 3; + string speaker_id = 4; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Pipeline Bridge +// ───────────────────────────────────────────────────────────────────────────── + +// PipelineTrigger is the request to start a pipeline. +// Subject: ai.pipeline.trigger +message PipelineTrigger { + string request_id = 1; + string pipeline = 2; + // Protobuf Struct could be used here, but a simple string map covers + // all current use-cases and avoids a google/protobuf import. + map parameters = 3; +} + +// PipelineStatus is the response / status update for a pipeline run. +// Subject: ai.pipeline.status.{request_id} +message PipelineStatus { + string request_id = 1; + string status = 2; + string run_id = 3; + string engine = 4; + string pipeline = 5; + string submitted_at = 6; + string error = 7; + repeated string available_pipelines = 8; +}