feat!: replace msgpack with protobuf for all NATS messages
Some checks failed
CI / Lint (push) Failing after 3m2s
CI / Test (push) Successful in 3m44s
CI / Release (push) Has been skipped
CI / Notify Downstream (chat-handler) (push) Has been skipped
CI / Notify Downstream (pipeline-bridge) (push) Has been skipped
CI / Notify Downstream (stt-module) (push) Has been skipped
CI / Notify Downstream (tts-module) (push) Has been skipped
CI / Notify Downstream (voice-assistant) (push) Has been skipped
CI / Notify (push) Successful in 1s

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
This commit is contained in:
2026-02-21 14:58:05 -05:00
parent 3585d81ff5
commit 13ef1df109
12 changed files with 3074 additions and 1293 deletions

View File

@@ -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)
}
}

View File

@@ -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()
}