diff --git a/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator.go b/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator.go index c2341025d2..542816fc21 100644 --- a/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator.go +++ b/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator.go @@ -5680,11 +5680,12 @@ func (p *EvaluatorRunError) Field2DeepEqual(src *string) bool { // 评估器输出数据 type EvaluatorOutputData struct { - EvaluatorResult_ *EvaluatorResult_ `thrift:"evaluator_result,1,optional" frugal:"1,optional,EvaluatorResult_" form:"evaluator_result" json:"evaluator_result,omitempty" query:"evaluator_result"` - EvaluatorUsage *EvaluatorUsage `thrift:"evaluator_usage,2,optional" frugal:"2,optional,EvaluatorUsage" form:"evaluator_usage" json:"evaluator_usage,omitempty" query:"evaluator_usage"` - EvaluatorRunError *EvaluatorRunError `thrift:"evaluator_run_error,3,optional" frugal:"3,optional,EvaluatorRunError" form:"evaluator_run_error" json:"evaluator_run_error,omitempty" query:"evaluator_run_error"` - TimeConsumingMs *int64 `thrift:"time_consuming_ms,4,optional" frugal:"4,optional,i64" json:"time_consuming_ms" form:"time_consuming_ms" query:"time_consuming_ms"` - Stdout *string `thrift:"stdout,11,optional" frugal:"11,optional,string" form:"stdout" json:"stdout,omitempty" query:"stdout"` + EvaluatorResult_ *EvaluatorResult_ `thrift:"evaluator_result,1,optional" frugal:"1,optional,EvaluatorResult_" form:"evaluator_result" json:"evaluator_result,omitempty" query:"evaluator_result"` + EvaluatorUsage *EvaluatorUsage `thrift:"evaluator_usage,2,optional" frugal:"2,optional,EvaluatorUsage" form:"evaluator_usage" json:"evaluator_usage,omitempty" query:"evaluator_usage"` + EvaluatorRunError *EvaluatorRunError `thrift:"evaluator_run_error,3,optional" frugal:"3,optional,EvaluatorRunError" form:"evaluator_run_error" json:"evaluator_run_error,omitempty" query:"evaluator_run_error"` + TimeConsumingMs *int64 `thrift:"time_consuming_ms,4,optional" frugal:"4,optional,i64" json:"time_consuming_ms" form:"time_consuming_ms" query:"time_consuming_ms"` + Stdout *string `thrift:"stdout,11,optional" frugal:"11,optional,string" form:"stdout" json:"stdout,omitempty" query:"stdout"` + ExtraOutput *EvaluatorExtraOutputContent `thrift:"extra_output,12,optional" frugal:"12,optional,EvaluatorExtraOutputContent" form:"extra_output" json:"extra_output,omitempty" query:"extra_output"` } func NewEvaluatorOutputData() *EvaluatorOutputData { @@ -5753,6 +5754,18 @@ func (p *EvaluatorOutputData) GetStdout() (v string) { } return *p.Stdout } + +var EvaluatorOutputData_ExtraOutput_DEFAULT *EvaluatorExtraOutputContent + +func (p *EvaluatorOutputData) GetExtraOutput() (v *EvaluatorExtraOutputContent) { + if p == nil { + return + } + if !p.IsSetExtraOutput() { + return EvaluatorOutputData_ExtraOutput_DEFAULT + } + return p.ExtraOutput +} func (p *EvaluatorOutputData) SetEvaluatorResult_(val *EvaluatorResult_) { p.EvaluatorResult_ = val } @@ -5768,6 +5781,9 @@ func (p *EvaluatorOutputData) SetTimeConsumingMs(val *int64) { func (p *EvaluatorOutputData) SetStdout(val *string) { p.Stdout = val } +func (p *EvaluatorOutputData) SetExtraOutput(val *EvaluatorExtraOutputContent) { + p.ExtraOutput = val +} var fieldIDToName_EvaluatorOutputData = map[int16]string{ 1: "evaluator_result", @@ -5775,6 +5791,7 @@ var fieldIDToName_EvaluatorOutputData = map[int16]string{ 3: "evaluator_run_error", 4: "time_consuming_ms", 11: "stdout", + 12: "extra_output", } func (p *EvaluatorOutputData) IsSetEvaluatorResult_() bool { @@ -5797,6 +5814,10 @@ func (p *EvaluatorOutputData) IsSetStdout() bool { return p.Stdout != nil } +func (p *EvaluatorOutputData) IsSetExtraOutput() bool { + return p.ExtraOutput != nil +} + func (p *EvaluatorOutputData) Read(iprot thrift.TProtocol) (err error) { var fieldTypeId thrift.TType var fieldId int16 @@ -5855,6 +5876,14 @@ func (p *EvaluatorOutputData) Read(iprot thrift.TProtocol) (err error) { } else if err = iprot.Skip(fieldTypeId); err != nil { goto SkipFieldError } + case 12: + if fieldTypeId == thrift.STRUCT { + if err = p.ReadField12(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } default: if err = iprot.Skip(fieldTypeId); err != nil { goto SkipFieldError @@ -5930,6 +5959,14 @@ func (p *EvaluatorOutputData) ReadField11(iprot thrift.TProtocol) error { p.Stdout = _field return nil } +func (p *EvaluatorOutputData) ReadField12(iprot thrift.TProtocol) error { + _field := NewEvaluatorExtraOutputContent() + if err := _field.Read(iprot); err != nil { + return err + } + p.ExtraOutput = _field + return nil +} func (p *EvaluatorOutputData) Write(oprot thrift.TProtocol) (err error) { var fieldId int16 @@ -5957,6 +5994,10 @@ func (p *EvaluatorOutputData) Write(oprot thrift.TProtocol) (err error) { fieldId = 11 goto WriteFieldError } + if err = p.writeField12(oprot); err != nil { + fieldId = 12 + goto WriteFieldError + } } if err = oprot.WriteFieldStop(); err != nil { goto WriteFieldStopError @@ -6065,6 +6106,24 @@ WriteFieldBeginError: WriteFieldEndError: return thrift.PrependError(fmt.Sprintf("%T write field 11 end error: ", p), err) } +func (p *EvaluatorOutputData) writeField12(oprot thrift.TProtocol) (err error) { + if p.IsSetExtraOutput() { + if err = oprot.WriteFieldBegin("extra_output", thrift.STRUCT, 12); err != nil { + goto WriteFieldBeginError + } + if err := p.ExtraOutput.Write(oprot); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 12 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 12 end error: ", p), err) +} func (p *EvaluatorOutputData) String() string { if p == nil { @@ -6095,6 +6154,9 @@ func (p *EvaluatorOutputData) DeepEqual(ano *EvaluatorOutputData) bool { if !p.Field11DeepEqual(ano.Stdout) { return false } + if !p.Field12DeepEqual(ano.ExtraOutput) { + return false + } return true } @@ -6143,6 +6205,271 @@ func (p *EvaluatorOutputData) Field11DeepEqual(src *string) bool { } return true } +func (p *EvaluatorOutputData) Field12DeepEqual(src *EvaluatorExtraOutputContent) bool { + + if !p.ExtraOutput.DeepEqual(src) { + return false + } + return true +} + +type EvaluatorExtraOutputContent struct { + OutputType *string `thrift:"output_type,1,optional" frugal:"1,optional,string" form:"output_type" json:"output_type,omitempty" query:"output_type"` + URL *string `thrift:"url,2,optional" frugal:"2,optional,string" form:"url" json:"url,omitempty" query:"url"` +} + +func NewEvaluatorExtraOutputContent() *EvaluatorExtraOutputContent { + return &EvaluatorExtraOutputContent{} +} + +func (p *EvaluatorExtraOutputContent) InitDefault() { +} + +var EvaluatorExtraOutputContent_OutputType_DEFAULT string + +func (p *EvaluatorExtraOutputContent) GetOutputType() (v string) { + if p == nil { + return + } + if !p.IsSetOutputType() { + return EvaluatorExtraOutputContent_OutputType_DEFAULT + } + return *p.OutputType +} + +var EvaluatorExtraOutputContent_URL_DEFAULT string + +func (p *EvaluatorExtraOutputContent) GetURL() (v string) { + if p == nil { + return + } + if !p.IsSetURL() { + return EvaluatorExtraOutputContent_URL_DEFAULT + } + return *p.URL +} +func (p *EvaluatorExtraOutputContent) SetOutputType(val *string) { + p.OutputType = val +} +func (p *EvaluatorExtraOutputContent) SetURL(val *string) { + p.URL = val +} + +var fieldIDToName_EvaluatorExtraOutputContent = map[int16]string{ + 1: "output_type", + 2: "url", +} + +func (p *EvaluatorExtraOutputContent) IsSetOutputType() bool { + return p.OutputType != nil +} + +func (p *EvaluatorExtraOutputContent) IsSetURL() bool { + return p.URL != nil +} + +func (p *EvaluatorExtraOutputContent) Read(iprot thrift.TProtocol) (err error) { + var fieldTypeId thrift.TType + var fieldId int16 + + if _, err = iprot.ReadStructBegin(); err != nil { + goto ReadStructBeginError + } + + for { + _, fieldTypeId, fieldId, err = iprot.ReadFieldBegin() + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + if err = p.ReadField1(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + case 2: + if fieldTypeId == thrift.STRING { + if err = p.ReadField2(iprot); err != nil { + goto ReadFieldError + } + } else if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + default: + if err = iprot.Skip(fieldTypeId); err != nil { + goto SkipFieldError + } + } + if err = iprot.ReadFieldEnd(); err != nil { + goto ReadFieldEndError + } + } + if err = iprot.ReadStructEnd(); err != nil { + goto ReadStructEndError + } + + return nil +ReadStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err) +ReadFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_EvaluatorExtraOutputContent[fieldId]), err) +SkipFieldError: + return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) + +ReadFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err) +ReadStructEndError: + return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err) +} + +func (p *EvaluatorExtraOutputContent) ReadField1(iprot thrift.TProtocol) error { + + var _field *string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _field = &v + } + p.OutputType = _field + return nil +} +func (p *EvaluatorExtraOutputContent) ReadField2(iprot thrift.TProtocol) error { + + var _field *string + if v, err := iprot.ReadString(); err != nil { + return err + } else { + _field = &v + } + p.URL = _field + return nil +} + +func (p *EvaluatorExtraOutputContent) Write(oprot thrift.TProtocol) (err error) { + var fieldId int16 + if err = oprot.WriteStructBegin("EvaluatorExtraOutputContent"); err != nil { + goto WriteStructBeginError + } + if p != nil { + if err = p.writeField1(oprot); err != nil { + fieldId = 1 + goto WriteFieldError + } + if err = p.writeField2(oprot); err != nil { + fieldId = 2 + goto WriteFieldError + } + } + if err = oprot.WriteFieldStop(); err != nil { + goto WriteFieldStopError + } + if err = oprot.WriteStructEnd(); err != nil { + goto WriteStructEndError + } + return nil +WriteStructBeginError: + return thrift.PrependError(fmt.Sprintf("%T write struct begin error: ", p), err) +WriteFieldError: + return thrift.PrependError(fmt.Sprintf("%T write field %d error: ", p, fieldId), err) +WriteFieldStopError: + return thrift.PrependError(fmt.Sprintf("%T write field stop error: ", p), err) +WriteStructEndError: + return thrift.PrependError(fmt.Sprintf("%T write struct end error: ", p), err) +} + +func (p *EvaluatorExtraOutputContent) writeField1(oprot thrift.TProtocol) (err error) { + if p.IsSetOutputType() { + if err = oprot.WriteFieldBegin("output_type", thrift.STRING, 1); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(*p.OutputType); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 1 end error: ", p), err) +} +func (p *EvaluatorExtraOutputContent) writeField2(oprot thrift.TProtocol) (err error) { + if p.IsSetURL() { + if err = oprot.WriteFieldBegin("url", thrift.STRING, 2); err != nil { + goto WriteFieldBeginError + } + if err := oprot.WriteString(*p.URL); err != nil { + return err + } + if err = oprot.WriteFieldEnd(); err != nil { + goto WriteFieldEndError + } + } + return nil +WriteFieldBeginError: + return thrift.PrependError(fmt.Sprintf("%T write field 2 begin error: ", p), err) +WriteFieldEndError: + return thrift.PrependError(fmt.Sprintf("%T write field 2 end error: ", p), err) +} + +func (p *EvaluatorExtraOutputContent) String() string { + if p == nil { + return "" + } + return fmt.Sprintf("EvaluatorExtraOutputContent(%+v)", *p) + +} + +func (p *EvaluatorExtraOutputContent) DeepEqual(ano *EvaluatorExtraOutputContent) bool { + if p == ano { + return true + } else if p == nil || ano == nil { + return false + } + if !p.Field1DeepEqual(ano.OutputType) { + return false + } + if !p.Field2DeepEqual(ano.URL) { + return false + } + return true +} + +func (p *EvaluatorExtraOutputContent) Field1DeepEqual(src *string) bool { + + if p.OutputType == src { + return true + } else if p.OutputType == nil || src == nil { + return false + } + if strings.Compare(*p.OutputType, *src) != 0 { + return false + } + return true +} +func (p *EvaluatorExtraOutputContent) Field2DeepEqual(src *string) bool { + + if p.URL == src { + return true + } else if p.URL == nil || src == nil { + return false + } + if strings.Compare(*p.URL, *src) != 0 { + return false + } + return true +} // 评估器输入数据 type EvaluatorInputData struct { diff --git a/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator_validator.go b/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator_validator.go index 00c436ad1e..d6a5859832 100644 --- a/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator_validator.go +++ b/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/evaluator_validator.go @@ -174,6 +174,14 @@ func (p *EvaluatorOutputData) IsValid() error { return fmt.Errorf("field EvaluatorRunError not valid, %w", err) } } + if p.ExtraOutput != nil { + if err := p.ExtraOutput.IsValid(); err != nil { + return fmt.Errorf("field ExtraOutput not valid, %w", err) + } + } + return nil +} +func (p *EvaluatorExtraOutputContent) IsValid() error { return nil } func (p *EvaluatorInputData) IsValid() error { diff --git a/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/k-evaluator.go b/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/k-evaluator.go index 5b3daafbb7..f9ec501820 100644 --- a/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/k-evaluator.go +++ b/backend/kitex_gen/coze/loop/evaluation/domain_openapi/evaluator/k-evaluator.go @@ -4156,6 +4156,20 @@ func (p *EvaluatorOutputData) FastRead(buf []byte) (int, error) { goto SkipFieldError } } + case 12: + if fieldTypeId == thrift.STRUCT { + l, err = p.FastReadField12(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = thrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } default: l, err = thrift.Binary.Skip(buf[offset:], fieldTypeId) offset += l @@ -4238,6 +4252,18 @@ func (p *EvaluatorOutputData) FastReadField11(buf []byte) (int, error) { return offset, nil } +func (p *EvaluatorOutputData) FastReadField12(buf []byte) (int, error) { + offset := 0 + _field := NewEvaluatorExtraOutputContent() + if l, err := _field.FastRead(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + } + p.ExtraOutput = _field + return offset, nil +} + func (p *EvaluatorOutputData) FastWrite(buf []byte) int { return p.FastWriteNocopy(buf, nil) } @@ -4250,6 +4276,7 @@ func (p *EvaluatorOutputData) FastWriteNocopy(buf []byte, w thrift.NocopyWriter) offset += p.fastWriteField2(buf[offset:], w) offset += p.fastWriteField3(buf[offset:], w) offset += p.fastWriteField11(buf[offset:], w) + offset += p.fastWriteField12(buf[offset:], w) } offset += thrift.Binary.WriteFieldStop(buf[offset:]) return offset @@ -4263,6 +4290,7 @@ func (p *EvaluatorOutputData) BLength() int { l += p.field3Length() l += p.field4Length() l += p.field11Length() + l += p.field12Length() } l += thrift.Binary.FieldStopLength() return l @@ -4313,6 +4341,15 @@ func (p *EvaluatorOutputData) fastWriteField11(buf []byte, w thrift.NocopyWriter return offset } +func (p *EvaluatorOutputData) fastWriteField12(buf []byte, w thrift.NocopyWriter) int { + offset := 0 + if p.IsSetExtraOutput() { + offset += thrift.Binary.WriteFieldBegin(buf[offset:], thrift.STRUCT, 12) + offset += p.ExtraOutput.FastWriteNocopy(buf[offset:], w) + } + return offset +} + func (p *EvaluatorOutputData) field1Length() int { l := 0 if p.IsSetEvaluatorResult_() { @@ -4358,6 +4395,15 @@ func (p *EvaluatorOutputData) field11Length() int { return l } +func (p *EvaluatorOutputData) field12Length() int { + l := 0 + if p.IsSetExtraOutput() { + l += thrift.Binary.FieldBeginLength() + l += p.ExtraOutput.BLength() + } + return l +} + func (p *EvaluatorOutputData) DeepCopy(s interface{}) error { src, ok := s.(*EvaluatorOutputData) if !ok { @@ -4404,6 +4450,191 @@ func (p *EvaluatorOutputData) DeepCopy(s interface{}) error { p.Stdout = &tmp } + var _extraOutput *EvaluatorExtraOutputContent + if src.ExtraOutput != nil { + _extraOutput = &EvaluatorExtraOutputContent{} + if err := _extraOutput.DeepCopy(src.ExtraOutput); err != nil { + return err + } + } + p.ExtraOutput = _extraOutput + + return nil +} + +func (p *EvaluatorExtraOutputContent) FastRead(buf []byte) (int, error) { + + var err error + var offset int + var l int + var fieldTypeId thrift.TType + var fieldId int16 + for { + fieldTypeId, fieldId, l, err = thrift.Binary.ReadFieldBegin(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldBeginError + } + if fieldTypeId == thrift.STOP { + break + } + switch fieldId { + case 1: + if fieldTypeId == thrift.STRING { + l, err = p.FastReadField1(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = thrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + case 2: + if fieldTypeId == thrift.STRING { + l, err = p.FastReadField2(buf[offset:]) + offset += l + if err != nil { + goto ReadFieldError + } + } else { + l, err = thrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + default: + l, err = thrift.Binary.Skip(buf[offset:], fieldTypeId) + offset += l + if err != nil { + goto SkipFieldError + } + } + } + + return offset, nil +ReadFieldBeginError: + return offset, thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err) +ReadFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_EvaluatorExtraOutputContent[fieldId]), err) +SkipFieldError: + return offset, thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err) +} + +func (p *EvaluatorExtraOutputContent) FastReadField1(buf []byte) (int, error) { + offset := 0 + + var _field *string + if v, l, err := thrift.Binary.ReadString(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + _field = &v + } + p.OutputType = _field + return offset, nil +} + +func (p *EvaluatorExtraOutputContent) FastReadField2(buf []byte) (int, error) { + offset := 0 + + var _field *string + if v, l, err := thrift.Binary.ReadString(buf[offset:]); err != nil { + return offset, err + } else { + offset += l + _field = &v + } + p.URL = _field + return offset, nil +} + +func (p *EvaluatorExtraOutputContent) FastWrite(buf []byte) int { + return p.FastWriteNocopy(buf, nil) +} + +func (p *EvaluatorExtraOutputContent) FastWriteNocopy(buf []byte, w thrift.NocopyWriter) int { + offset := 0 + if p != nil { + offset += p.fastWriteField1(buf[offset:], w) + offset += p.fastWriteField2(buf[offset:], w) + } + offset += thrift.Binary.WriteFieldStop(buf[offset:]) + return offset +} + +func (p *EvaluatorExtraOutputContent) BLength() int { + l := 0 + if p != nil { + l += p.field1Length() + l += p.field2Length() + } + l += thrift.Binary.FieldStopLength() + return l +} + +func (p *EvaluatorExtraOutputContent) fastWriteField1(buf []byte, w thrift.NocopyWriter) int { + offset := 0 + if p.IsSetOutputType() { + offset += thrift.Binary.WriteFieldBegin(buf[offset:], thrift.STRING, 1) + offset += thrift.Binary.WriteStringNocopy(buf[offset:], w, *p.OutputType) + } + return offset +} + +func (p *EvaluatorExtraOutputContent) fastWriteField2(buf []byte, w thrift.NocopyWriter) int { + offset := 0 + if p.IsSetURL() { + offset += thrift.Binary.WriteFieldBegin(buf[offset:], thrift.STRING, 2) + offset += thrift.Binary.WriteStringNocopy(buf[offset:], w, *p.URL) + } + return offset +} + +func (p *EvaluatorExtraOutputContent) field1Length() int { + l := 0 + if p.IsSetOutputType() { + l += thrift.Binary.FieldBeginLength() + l += thrift.Binary.StringLengthNocopy(*p.OutputType) + } + return l +} + +func (p *EvaluatorExtraOutputContent) field2Length() int { + l := 0 + if p.IsSetURL() { + l += thrift.Binary.FieldBeginLength() + l += thrift.Binary.StringLengthNocopy(*p.URL) + } + return l +} + +func (p *EvaluatorExtraOutputContent) DeepCopy(s interface{}) error { + src, ok := s.(*EvaluatorExtraOutputContent) + if !ok { + return fmt.Errorf("%T's type not matched %T", s, p) + } + + if src.OutputType != nil { + var tmp string + if *src.OutputType != "" { + tmp = kutils.StringDeepCopy(*src.OutputType) + } + p.OutputType = &tmp + } + + if src.URL != nil { + var tmp string + if *src.URL != "" { + tmp = kutils.StringDeepCopy(*src.URL) + } + p.URL = &tmp + } + return nil } diff --git a/backend/modules/evaluation/application/convertor/evaluator/openapi.go b/backend/modules/evaluation/application/convertor/evaluator/openapi.go index 49b8ea51f0..5848e6ffa5 100644 --- a/backend/modules/evaluation/application/convertor/evaluator/openapi.go +++ b/backend/modules/evaluation/application/convertor/evaluator/openapi.go @@ -309,10 +309,23 @@ func OpenAPIEvaluatorOutputDataDO2DTO(do *entity.EvaluatorOutputData) *openapiEv EvaluatorRunError: OpenAPIEvaluatorRunErrorDO2DTO(do.EvaluatorRunError), TimeConsumingMs: gptr.Of(do.TimeConsumingMS), Stdout: gptr.Of(do.Stdout), + ExtraOutput: OpenAPIEvaluatorExtraOutputContentDO2DTO(do.ExtraOutput), } return dto } +func OpenAPIEvaluatorExtraOutputContentDO2DTO(do *entity.EvaluatorExtraOutputContent) *openapiEvaluator.EvaluatorExtraOutputContent { + if do == nil { + return nil + } + dto := &openapiEvaluator.EvaluatorExtraOutputContent{} + if do.OutputType != nil { + dto.OutputType = gptr.Of(string(*do.OutputType)) + } + dto.URL = do.URL + return dto +} + func OpenAPIEvaluatorResultDO2DTO(do *entity.EvaluatorResult) *openapiEvaluator.EvaluatorResult_ { if do == nil { return nil diff --git a/backend/modules/evaluation/application/convertor/evaluator/openapi_test.go b/backend/modules/evaluation/application/convertor/evaluator/openapi_test.go index f114eecdb0..ceee0950fb 100644 --- a/backend/modules/evaluation/application/convertor/evaluator/openapi_test.go +++ b/backend/modules/evaluation/application/convertor/evaluator/openapi_test.go @@ -209,6 +209,34 @@ func TestOpenAPIEvaluatorOutputDataDO2DTO(t *testing.T) { assert.Equal(t, "output", *dto.Stdout) assert.Equal(t, float64(5), *dto.EvaluatorResult_.Score) }) + + t.Run("with extra_output", func(t *testing.T) { + outputType := entity.EvaluatorExtraOutputTypeHTML + url := "https://tos.example.com/signed-url" + do := &entity.EvaluatorOutputData{ + TimeConsumingMS: 200, + ExtraOutput: &entity.EvaluatorExtraOutputContent{ + OutputType: &outputType, + URI: gptr.Of("tos-cn-i-xxx/space/123/extra_output/index.html"), + URL: &url, + }, + } + dto := OpenAPIEvaluatorOutputDataDO2DTO(do) + assert.NotNil(t, dto) + assert.NotNil(t, dto.ExtraOutput) + assert.Equal(t, "html", *dto.ExtraOutput.OutputType) + assert.Equal(t, "https://tos.example.com/signed-url", *dto.ExtraOutput.URL) + }) + + t.Run("with nil extra_output", func(t *testing.T) { + do := &entity.EvaluatorOutputData{ + TimeConsumingMS: 100, + ExtraOutput: nil, + } + dto := OpenAPIEvaluatorOutputDataDO2DTO(do) + assert.NotNil(t, dto) + assert.Nil(t, dto.ExtraOutput) + }) } func TestOpenAPIEvaluatorResultDO2DTO(t *testing.T) { @@ -856,3 +884,141 @@ func TestOpenAPIEvaluatorDTO2DO_AgentWithVersion(t *testing.T) { assert.Equal(t, entity.AgentType_Vibe, do.AgentEvaluatorVersion.AgentConfig.AgentType) assert.Equal(t, "v1", do.GetVersion()) } + +func TestOpenAPIEvaluatorExtraOutputContentDO2DTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, OpenAPIEvaluatorExtraOutputContentDO2DTO(nil)) + }) + + t.Run("with output type and url", func(t *testing.T) { + outputType := entity.EvaluatorExtraOutputTypeHTML + url := "https://example.com/signed" + do := &entity.EvaluatorExtraOutputContent{ + OutputType: &outputType, + URL: &url, + URI: gptr.Of("tos-bucket/path"), + } + dto := OpenAPIEvaluatorExtraOutputContentDO2DTO(do) + assert.NotNil(t, dto) + assert.Equal(t, "html", *dto.OutputType) + assert.Equal(t, "https://example.com/signed", *dto.URL) + }) + + t.Run("without output type", func(t *testing.T) { + url := "https://example.com/signed" + do := &entity.EvaluatorExtraOutputContent{ + URL: &url, + } + dto := OpenAPIEvaluatorExtraOutputContentDO2DTO(do) + assert.NotNil(t, dto) + assert.Nil(t, dto.OutputType) + assert.Equal(t, "https://example.com/signed", *dto.URL) + }) +} + +func TestOpenapiAccessProtocolFromEntity(t *testing.T) { + t.Run("rpc_old maps to rpc", func(t *testing.T) { + result := openapiAccessProtocolFromEntity(entity.EvaluatorAccessProtocolRPCOld) + assert.NotNil(t, result) + assert.Equal(t, openapiEvaluator.EvaluatorAccessProtocolRPC, *result) + }) + + t.Run("faas_http_old maps to faas_http", func(t *testing.T) { + result := openapiAccessProtocolFromEntity(entity.EvaluatorAccessProtocolFaasHTTPOld) + assert.NotNil(t, result) + assert.Equal(t, openapiEvaluator.EvaluatorAccessProtocolFaasHTTP, *result) + }) + + t.Run("rpc stays rpc", func(t *testing.T) { + result := openapiAccessProtocolFromEntity(entity.EvaluatorAccessProtocolRPC) + assert.NotNil(t, result) + assert.Equal(t, openapiEvaluator.EvaluatorAccessProtocolRPC, *result) + }) + + t.Run("faas_http stays faas_http", func(t *testing.T) { + result := openapiAccessProtocolFromEntity(entity.EvaluatorAccessProtocolFaasHTTP) + assert.NotNil(t, result) + assert.Equal(t, openapiEvaluator.EvaluatorAccessProtocolFaasHTTP, *result) + }) + + t.Run("empty returns nil", func(t *testing.T) { + result := openapiAccessProtocolFromEntity("") + assert.Nil(t, result) + }) + + t.Run("unknown protocol returned as-is", func(t *testing.T) { + result := openapiAccessProtocolFromEntity("custom_proto") + assert.NotNil(t, result) + assert.Equal(t, openapiEvaluator.EvaluatorAccessProtocol("custom_proto"), *result) + }) +} + +func TestOpenAPIEvaluatorHTTPInfoDO2DTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, OpenAPIEvaluatorHTTPInfoDO2DTO(nil)) + }) + + t.Run("with method", func(t *testing.T) { + method := entity.EvaluatorHTTPMethodPost + do := &entity.EvaluatorHTTPInfo{ + Method: &method, + Path: gptr.Of("/api/v1/eval"), + } + dto := OpenAPIEvaluatorHTTPInfoDO2DTO(do) + assert.NotNil(t, dto) + assert.Equal(t, openapiEvaluator.EvaluatorHTTPMethod("post"), *dto.Method) + assert.Equal(t, "/api/v1/eval", *dto.Path) + }) + + t.Run("without method", func(t *testing.T) { + do := &entity.EvaluatorHTTPInfo{ + Path: gptr.Of("/api/v1/eval"), + } + dto := OpenAPIEvaluatorHTTPInfoDO2DTO(do) + assert.NotNil(t, dto) + assert.Nil(t, dto.Method) + }) +} + +func TestOpenAPIEvaluatorHTTPInfoDTO2DO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, OpenAPIEvaluatorHTTPInfoDTO2DO(nil)) + }) + + t.Run("with method", func(t *testing.T) { + method := openapiEvaluator.EvaluatorHTTPMethod("post") + dto := &openapiEvaluator.EvaluatorHTTPInfo{ + Method: &method, + Path: gptr.Of("/api/v1/eval"), + } + do := OpenAPIEvaluatorHTTPInfoDTO2DO(dto) + assert.NotNil(t, do) + assert.Equal(t, entity.EvaluatorHTTPMethodPost, *do.Method) + assert.Equal(t, "/api/v1/eval", *do.Path) + }) + + t.Run("without method", func(t *testing.T) { + dto := &openapiEvaluator.EvaluatorHTTPInfo{ + Path: gptr.Of("/api/v1/eval"), + } + do := OpenAPIEvaluatorHTTPInfoDTO2DO(dto) + assert.NotNil(t, do) + assert.Nil(t, do.Method) + }) +} + +func TestOpenAPIEvaluatorRunConfigDO2DTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, OpenAPIEvaluatorRunConfigDO2DTO(nil)) + }) + + t.Run("with env", func(t *testing.T) { + env := "production" + do := &entity.EvaluatorRunConfig{ + Env: &env, + } + dto := OpenAPIEvaluatorRunConfigDO2DTO(do) + assert.NotNil(t, dto) + assert.Equal(t, "production", *dto.Env) + }) +} diff --git a/backend/modules/evaluation/application/convertor/experiment/aggr_result_test.go b/backend/modules/evaluation/application/convertor/experiment/aggr_result_test.go new file mode 100644 index 0000000000..1cc5856fd8 --- /dev/null +++ b/backend/modules/evaluation/application/convertor/experiment/aggr_result_test.go @@ -0,0 +1,246 @@ +// Copyright (c) 2025 coze-dev Authors +// SPDX-License-Identifier: Apache-2.0 + +package experiment + +import ( + "testing" + "time" + + "github.com/bytedance/gg/gptr" + "github.com/stretchr/testify/assert" + + "github.com/coze-dev/coze-loop/backend/modules/evaluation/domain/entity" +) + +func TestExptAggregateResultDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, ExptAggregateResultDOToDTO(nil)) + }) + + t.Run("full input", func(t *testing.T) { + updateTime := time.Unix(1000, 0) + data := &entity.ExptAggregateResult{ + ExperimentID: 100, + Status: 1, + UpdateTime: &updateTime, + EvaluatorResults: map[int64]*entity.EvaluatorAggregateResult{ + 10: { + EvaluatorVersionID: 10, + AggregatorResults: []*entity.AggregatorResult{ + {AggregatorType: entity.Average, Data: &entity.AggregateData{Value: gptr.Of(0.95)}}, + }, + Name: gptr.Of("eval1"), + Version: gptr.Of("v1"), + }, + }, + AnnotationResults: map[int64]*entity.AnnotationAggregateResult{ + 20: { + TagKeyID: 20, + AggregatorResults: []*entity.AggregatorResult{ + {AggregatorType: entity.Sum, Data: &entity.AggregateData{Value: gptr.Of(5.0)}}, + }, + Name: gptr.Of("tag1"), + }, + }, + WeightedResults: []*entity.AggregatorResult{ + {AggregatorType: entity.Average, Data: &entity.AggregateData{Value: gptr.Of(0.88)}}, + }, + } + + dto := ExptAggregateResultDOToDTO(data) + assert.NotNil(t, dto) + assert.Equal(t, int64(100), dto.ExperimentID) + assert.Equal(t, int64(1000), *dto.UpdateTime) + assert.Len(t, dto.EvaluatorResults, 1) + assert.NotNil(t, dto.EvaluatorResults[10]) + assert.Len(t, dto.AnnotationResults, 1) + assert.NotNil(t, dto.AnnotationResults[20]) + assert.Len(t, dto.WeightedResults, 1) + }) + + t.Run("without update time", func(t *testing.T) { + data := &entity.ExptAggregateResult{ + ExperimentID: 200, + EvaluatorResults: map[int64]*entity.EvaluatorAggregateResult{}, + AnnotationResults: map[int64]*entity.AnnotationAggregateResult{}, + } + dto := ExptAggregateResultDOToDTO(data) + assert.NotNil(t, dto) + assert.Nil(t, dto.UpdateTime) + }) +} + +func TestEvaluatorResultsDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, EvaluatorResultsDOToDTO(nil)) + }) + + t.Run("normal input", func(t *testing.T) { + result := &entity.EvaluatorAggregateResult{ + EvaluatorVersionID: 10, + AggregatorResults: []*entity.AggregatorResult{ + {AggregatorType: entity.Average, Data: &entity.AggregateData{Value: gptr.Of(0.5)}}, + }, + Name: gptr.Of("eval"), + Version: gptr.Of("v1"), + } + dto := EvaluatorResultsDOToDTO(result) + assert.NotNil(t, dto) + assert.Equal(t, int64(10), dto.EvaluatorVersionID) + assert.Equal(t, "eval", *dto.Name) + assert.Equal(t, "v1", *dto.Version) + assert.Len(t, dto.AggregatorResults, 1) + }) +} + +func TestAnnotationResultDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, AnnotationResultDOToDTO(nil)) + }) + + t.Run("normal input", func(t *testing.T) { + result := &entity.AnnotationAggregateResult{ + TagKeyID: 20, + AggregatorResults: []*entity.AggregatorResult{ + {AggregatorType: entity.Sum, Data: &entity.AggregateData{Value: gptr.Of(3.0)}}, + }, + Name: gptr.Of("tag"), + } + dto := AnnotationResultDOToDTO(result) + assert.NotNil(t, dto) + assert.Equal(t, int64(20), dto.TagKeyID) + assert.Equal(t, "tag", *dto.Name) + }) +} + +func TestAggregatorResultDOsToDTOs(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, AggregatorResultDOsToDTOs(nil)) + }) + + t.Run("empty input", func(t *testing.T) { + assert.Nil(t, AggregatorResultDOsToDTOs([]*entity.AggregatorResult{})) + }) + + t.Run("normal input", func(t *testing.T) { + results := []*entity.AggregatorResult{ + {AggregatorType: entity.Average, Data: &entity.AggregateData{Value: gptr.Of(1.0)}}, + nil, + } + dtos := AggregatorResultDOsToDTOs(results) + assert.Len(t, dtos, 2) + assert.NotNil(t, dtos[0]) + assert.Nil(t, dtos[1]) + }) +} + +func TestAggregatorResultDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, AggregatorResultDOToDTO(nil)) + }) + + t.Run("normal input", func(t *testing.T) { + result := &entity.AggregatorResult{ + AggregatorType: entity.Average, + Data: &entity.AggregateData{Value: gptr.Of(0.75)}, + } + dto := AggregatorResultDOToDTO(result) + assert.NotNil(t, dto) + assert.Equal(t, 0.75, *dto.Data.Value) + }) +} + +func TestAggregateDataDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, AggregateDataDOToDTO(nil)) + }) + + t.Run("with value only", func(t *testing.T) { + data := &entity.AggregateData{ + Value: gptr.Of(0.123456), + } + dto := AggregateDataDOToDTO(data) + assert.NotNil(t, dto) + assert.Equal(t, 0.12, *dto.Value) + }) + + t.Run("with score distribution", func(t *testing.T) { + data := &entity.AggregateData{ + ScoreDistribution: &entity.ScoreDistributionData{ + ScoreDistributionItems: []*entity.ScoreDistributionItem{ + {Score: "1.0", Count: 5, Percentage: 0.5}, + nil, + {Score: "2.0", Count: 5, Percentage: 0.5}, + }, + }, + } + dto := AggregateDataDOToDTO(data) + assert.NotNil(t, dto) + assert.NotNil(t, dto.ScoreDistribution) + assert.Len(t, dto.ScoreDistribution.ScoreDistributionItems, 2) + }) + + t.Run("with option distribution", func(t *testing.T) { + data := &entity.AggregateData{ + OptionDistribution: &entity.OptionDistributionData{ + OptionDistributionItems: []*entity.OptionDistributionItem{ + {Option: "A", Count: 3, Percentage: 0.3}, + nil, + {Option: "B", Count: 7, Percentage: 0.7}, + }, + }, + } + dto := AggregateDataDOToDTO(data) + assert.NotNil(t, dto) + assert.NotNil(t, dto.OptionDistribution) + assert.Len(t, dto.OptionDistribution.OptionDistributionItems, 2) + }) + + t.Run("nil value", func(t *testing.T) { + data := &entity.AggregateData{} + dto := AggregateDataDOToDTO(data) + assert.NotNil(t, dto) + assert.Nil(t, dto.Value) + }) +} + +func TestScoreDistributionItemsDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, ScoreDistributionItemsDOToDTO(nil)) + }) + + t.Run("empty input", func(t *testing.T) { + assert.Nil(t, ScoreDistributionItemsDOToDTO([]*entity.ScoreDistributionItem{})) + }) + + t.Run("normal input with nil", func(t *testing.T) { + items := []*entity.ScoreDistributionItem{ + {Score: "1.0", Count: 10, Percentage: 0.5}, + nil, + } + dtos := ScoreDistributionItemsDOToDTO(items) + assert.Len(t, dtos, 1) + assert.Equal(t, "1.0", dtos[0].Score) + }) +} + +func TestOptionDistributionItemsDOToDTO(t *testing.T) { + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, OptionDistributionItemsDOToDTO(nil)) + }) + + t.Run("empty input", func(t *testing.T) { + assert.Nil(t, OptionDistributionItemsDOToDTO([]*entity.OptionDistributionItem{})) + }) + + t.Run("normal input with nil", func(t *testing.T) { + items := []*entity.OptionDistributionItem{ + {Option: "yes", Count: 5, Percentage: 0.5}, + nil, + } + dtos := OptionDistributionItemsDOToDTO(items) + assert.Len(t, dtos, 1) + assert.Equal(t, "yes", dtos[0].Option) + }) +} diff --git a/backend/modules/evaluation/application/convertor/experiment/expt_result_test.go b/backend/modules/evaluation/application/convertor/experiment/expt_result_test.go index 13a412f6d4..664d35acbc 100644 --- a/backend/modules/evaluation/application/convertor/experiment/expt_result_test.go +++ b/backend/modules/evaluation/application/convertor/experiment/expt_result_test.go @@ -410,3 +410,72 @@ func TestExportRecordDO2DTO(t *testing.T) { assert.Equal(t, from.EndAt.Unix(), *got.EndTime) }) } + +func TestItemResultsDO2DTOs(t *testing.T) { + t.Parallel() + + from := []*entity.ItemResult{ + { + ItemID: 1, + TurnResults: []*entity.TurnResult{ + { + TurnID: 10, + TurnIndex: gptr.Of(int64(0)), + ExperimentResults: []*entity.ExperimentResult{ + { + ExperimentID: 100, + Payload: &entity.ExperimentTurnPayload{ + TurnID: 10, + }, + }, + }, + }, + }, + SystemInfo: &entity.ItemSystemInfo{ + RunState: 1, + LogID: gptr.Of("log-1"), + }, + ItemIndex: gptr.Of(int64(0)), + Ext: map[string]string{"k": "v"}, + }, + { + ItemID: 2, + ItemIndex: gptr.Of(int64(1)), + }, + } + + got := ItemResultsDO2DTOs(from) + assert.Len(t, got, 2) + assert.Equal(t, int64(1), got[0].ItemID) + assert.Len(t, got[0].TurnResults, 1) + assert.Equal(t, int64(10), got[0].TurnResults[0].TurnID) + assert.Equal(t, "v", got[0].Ext["k"]) + assert.Equal(t, int64(2), got[1].ItemID) +} + +func TestTurnEvaluatorOutputDO2DTO_WithRecords(t *testing.T) { + t.Parallel() + + score := 0.85 + from := &entity.TurnEvaluatorOutput{ + EvaluatorRecords: map[int64]*entity.EvaluatorRecord{ + 1: {ID: 100, EvaluatorVersionID: 1}, + }, + WeightedScore: &score, + } + + got := TurnEvaluatorOutputDO2DTO(from) + assert.NotNil(t, got) + assert.Len(t, got.EvaluatorRecords, 1) + assert.NotNil(t, got.EvaluatorRecords[1]) + assert.Equal(t, &score, got.WeightedScore) +} + +func TestConvRetryMode(t *testing.T) { + t.Parallel() + + assert.Equal(t, entity.EvaluationModeFailRetry, ConvRetryMode(domain_expt.ExptRetryMode_RetryFailure)) + assert.Equal(t, entity.EvaluationModeRetryAll, ConvRetryMode(domain_expt.ExptRetryMode_RetryAll)) + assert.Equal(t, entity.EvaluationModeRetryItems, ConvRetryMode(domain_expt.ExptRetryMode_RetryTargetItems)) + assert.Equal(t, entity.EvaluationModeUnknown, ConvRetryMode(domain_expt.ExptRetryMode(999))) +} diff --git a/backend/modules/evaluation/application/convertor/experiment/openapi.go b/backend/modules/evaluation/application/convertor/experiment/openapi.go index 4f2dcf1663..6e5fda0f13 100644 --- a/backend/modules/evaluation/application/convertor/experiment/openapi.go +++ b/backend/modules/evaluation/application/convertor/experiment/openapi.go @@ -1195,6 +1195,12 @@ func openAPIEvaluatorOutputDataDO2DTO(data *entity.EvaluatorOutputData) *openapi if data.TimeConsumingMS > 0 { res.TimeConsumingMs = gptr.Of(data.TimeConsumingMS) } + if data.Stdout != "" { + res.Stdout = gptr.Of(data.Stdout) + } + if data.ExtraOutput != nil { + res.ExtraOutput = evaluator_convertor.OpenAPIEvaluatorExtraOutputContentDO2DTO(data.ExtraOutput) + } if res.EvaluatorResult_ == nil && res.EvaluatorUsage == nil && res.EvaluatorRunError == nil && res.TimeConsumingMs == nil { return nil } diff --git a/backend/modules/evaluation/application/convertor/experiment/openapi_test.go b/backend/modules/evaluation/application/convertor/experiment/openapi_test.go index d01b9f2fb8..01f3a528c6 100755 --- a/backend/modules/evaluation/application/convertor/experiment/openapi_test.go +++ b/backend/modules/evaluation/application/convertor/experiment/openapi_test.go @@ -3372,3 +3372,394 @@ func TestOpenAPINotificationConfDTO2Domain_EnglishValueInput(t *testing.T) { assert.Nil(t, got) }) } + +func TestOpenAPIEvaluatorOutputDataDO2DTO_ExtraFields(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPIEvaluatorOutputDataDO2DTO(nil)) + }) + + t.Run("all nil fields returns nil", func(t *testing.T) { + data := &entity.EvaluatorOutputData{} + assert.Nil(t, openAPIEvaluatorOutputDataDO2DTO(data)) + }) + + t.Run("with stdout", func(t *testing.T) { + data := &entity.EvaluatorOutputData{ + TimeConsumingMS: 100, + Stdout: "hello output", + } + dto := openAPIEvaluatorOutputDataDO2DTO(data) + assert.NotNil(t, dto) + assert.Equal(t, "hello output", *dto.Stdout) + assert.Equal(t, int64(100), *dto.TimeConsumingMs) + }) + + t.Run("with extra_output", func(t *testing.T) { + outputType := entity.EvaluatorExtraOutputTypeHTML + url := "https://tos.example.com/signed" + data := &entity.EvaluatorOutputData{ + TimeConsumingMS: 50, + ExtraOutput: &entity.EvaluatorExtraOutputContent{ + OutputType: &outputType, + URL: &url, + }, + } + dto := openAPIEvaluatorOutputDataDO2DTO(data) + assert.NotNil(t, dto) + assert.NotNil(t, dto.ExtraOutput) + assert.Equal(t, "html", *dto.ExtraOutput.OutputType) + assert.Equal(t, "https://tos.example.com/signed", *dto.ExtraOutput.URL) + }) + + t.Run("stdout and extra_output only (no result/usage/error/time)", func(t *testing.T) { + outputType := entity.EvaluatorExtraOutputTypeHTML + url := "https://tos.example.com/signed" + data := &entity.EvaluatorOutputData{ + Stdout: "output", + ExtraOutput: &entity.EvaluatorExtraOutputContent{ + OutputType: &outputType, + URL: &url, + }, + } + // TimeConsumingMS=0, no result/usage/error → returns nil due to the guard condition + dto := openAPIEvaluatorOutputDataDO2DTO(data) + assert.Nil(t, dto) + }) +} + +func TestOpenAPIEvaluatorResultDO2DTO_Branches(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPIEvaluatorResultDO2DTO(nil)) + }) + + t.Run("score and reasoning without correction", func(t *testing.T) { + result := &entity.EvaluatorResult{ + Score: gptr.Of(4.5), + Reasoning: "good quality", + } + dto := openAPIEvaluatorResultDO2DTO(result) + assert.NotNil(t, dto) + assert.Equal(t, 4.5, *dto.Score) + assert.Equal(t, "good quality", *dto.Reasoning) + }) + + t.Run("with correction score overrides", func(t *testing.T) { + result := &entity.EvaluatorResult{ + Score: gptr.Of(3.0), + Reasoning: "original", + Correction: &entity.Correction{ + Score: gptr.Of(4.0), + Explain: "corrected", + }, + } + dto := openAPIEvaluatorResultDO2DTO(result) + assert.NotNil(t, dto) + assert.Equal(t, 4.0, *dto.Score) + assert.Equal(t, "corrected", *dto.Reasoning) + }) + + t.Run("correction without score falls back to original", func(t *testing.T) { + result := &entity.EvaluatorResult{ + Score: gptr.Of(3.0), + Reasoning: "original", + Correction: &entity.Correction{ + Explain: "corrected reason", + }, + } + dto := openAPIEvaluatorResultDO2DTO(result) + assert.NotNil(t, dto) + assert.Equal(t, 3.0, *dto.Score) + assert.Equal(t, "corrected reason", *dto.Reasoning) + }) + + t.Run("correction without explain falls back to original reasoning", func(t *testing.T) { + result := &entity.EvaluatorResult{ + Score: gptr.Of(3.0), + Reasoning: "original reason", + Correction: &entity.Correction{ + Score: gptr.Of(5.0), + }, + } + dto := openAPIEvaluatorResultDO2DTO(result) + assert.NotNil(t, dto) + assert.Equal(t, 5.0, *dto.Score) + assert.Equal(t, "original reason", *dto.Reasoning) + }) + + t.Run("all nil returns nil", func(t *testing.T) { + result := &entity.EvaluatorResult{} + assert.Nil(t, openAPIEvaluatorResultDO2DTO(result)) + }) +} + +func TestConvertEntityEvaluatorStatusToOpenAPI(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + status := convertEntityEvaluatorStatusToOpenAPI(entity.EvaluatorRunStatusSuccess) + assert.NotNil(t, status) + assert.Equal(t, openapiEvaluator.EvaluatorRunStatusSuccess, *status) + }) + + t.Run("fail", func(t *testing.T) { + status := convertEntityEvaluatorStatusToOpenAPI(entity.EvaluatorRunStatusFail) + assert.NotNil(t, status) + assert.Equal(t, openapiEvaluator.EvaluatorRunStatusFailed, *status) + }) + + t.Run("unknown returns nil", func(t *testing.T) { + status := convertEntityEvaluatorStatusToOpenAPI(entity.EvaluatorRunStatusUnknown) + assert.Nil(t, status) + }) + + t.Run("default returns processing", func(t *testing.T) { + status := convertEntityEvaluatorStatusToOpenAPI(entity.EvaluatorRunStatus(999)) + assert.NotNil(t, status) + assert.Equal(t, openapiEvaluator.EvaluatorRunStatusProcessing, *status) + }) +} + +func TestConvertEntityTargetRunStatusToOpenAPI(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, convertEntityTargetRunStatusToOpenAPI(nil)) + }) + + t.Run("success", func(t *testing.T) { + s := entity.EvalTargetRunStatusSuccess + status := convertEntityTargetRunStatusToOpenAPI(&s) + assert.NotNil(t, status) + assert.Equal(t, openapiEvalTarget.EvalTargetRunStatusSuccess, *status) + }) + + t.Run("fail", func(t *testing.T) { + s := entity.EvalTargetRunStatusFail + status := convertEntityTargetRunStatusToOpenAPI(&s) + assert.NotNil(t, status) + assert.Equal(t, openapiEvalTarget.EvalTargetRunStatusFail, *status) + }) + + t.Run("unknown returns nil", func(t *testing.T) { + s := entity.EvalTargetRunStatus(999) + status := convertEntityTargetRunStatusToOpenAPI(&s) + assert.Nil(t, status) + }) +} + +func TestOpenAPIEvaluatorUsageDO2DTO_Experiment(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPIEvaluatorUsageDO2DTO(nil)) + }) + + t.Run("both zero returns nil", func(t *testing.T) { + usage := &entity.EvaluatorUsage{} + assert.Nil(t, openAPIEvaluatorUsageDO2DTO(usage)) + }) + + t.Run("only input tokens", func(t *testing.T) { + usage := &entity.EvaluatorUsage{InputTokens: 10} + dto := openAPIEvaluatorUsageDO2DTO(usage) + assert.NotNil(t, dto) + assert.Equal(t, int64(10), *dto.InputTokens) + assert.Nil(t, dto.OutputTokens) + }) + + t.Run("only output tokens", func(t *testing.T) { + usage := &entity.EvaluatorUsage{OutputTokens: 20} + dto := openAPIEvaluatorUsageDO2DTO(usage) + assert.NotNil(t, dto) + assert.Nil(t, dto.InputTokens) + assert.Equal(t, int64(20), *dto.OutputTokens) + }) +} + +func TestOpenAPIEvaluatorRunErrorDO2DTO_Experiment(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPIEvaluatorRunErrorDO2DTO(nil)) + }) + + t.Run("both zero returns nil", func(t *testing.T) { + err := &entity.EvaluatorRunError{} + assert.Nil(t, openAPIEvaluatorRunErrorDO2DTO(err)) + }) + + t.Run("only code", func(t *testing.T) { + err := &entity.EvaluatorRunError{Code: 500} + dto := openAPIEvaluatorRunErrorDO2DTO(err) + assert.NotNil(t, dto) + assert.Equal(t, int32(500), *dto.Code) + assert.Nil(t, dto.Message) + }) + + t.Run("only message", func(t *testing.T) { + err := &entity.EvaluatorRunError{Message: "timeout"} + dto := openAPIEvaluatorRunErrorDO2DTO(err) + assert.NotNil(t, dto) + assert.Nil(t, dto.Code) + assert.Equal(t, "timeout", *dto.Message) + }) +} + +func TestOpenAPITargetUsageDO2DTO(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPITargetUsageDO2DTO(nil)) + }) + + t.Run("normal input", func(t *testing.T) { + usage := &entity.EvalTargetUsage{ + InputTokens: 100, + OutputTokens: 200, + } + dto := openAPITargetUsageDO2DTO(usage) + assert.NotNil(t, dto) + assert.Equal(t, int64(100), dto.InputTokens) + assert.Equal(t, int64(200), dto.OutputTokens) + }) +} + +func TestOpenAPITargetRunErrorDO2DTO(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPITargetRunErrorDO2DTO(nil)) + }) + + t.Run("both zero returns nil", func(t *testing.T) { + err := &entity.EvalTargetRunError{} + assert.Nil(t, openAPITargetRunErrorDO2DTO(err)) + }) + + t.Run("with code and message", func(t *testing.T) { + err := &entity.EvalTargetRunError{Code: 500, Message: "internal error"} + dto := openAPITargetRunErrorDO2DTO(err) + assert.NotNil(t, dto) + assert.Equal(t, int32(500), *dto.Code) + assert.Equal(t, "internal error", *dto.Message) + }) +} + +func TestOpenAPIEvaluatorRecordsMapDO2DTO(t *testing.T) { + t.Parallel() + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, openAPIEvaluatorRecordsMapDO2DTO(nil)) + }) + + t.Run("empty map", func(t *testing.T) { + assert.Nil(t, openAPIEvaluatorRecordsMapDO2DTO(map[int64]*entity.EvaluatorRecord{})) + }) + + t.Run("map with nil values only", func(t *testing.T) { + records := map[int64]*entity.EvaluatorRecord{ + 1: nil, + 2: nil, + } + assert.Nil(t, openAPIEvaluatorRecordsMapDO2DTO(records)) + }) + + t.Run("normal map", func(t *testing.T) { + records := map[int64]*entity.EvaluatorRecord{ + 1: { + ID: 1, EvaluatorVersionID: 10, Status: entity.EvaluatorRunStatusSuccess, + EvaluatorOutputData: &entity.EvaluatorOutputData{TimeConsumingMS: 50}, + }, + 2: nil, + } + dtos := openAPIEvaluatorRecordsMapDO2DTO(records) + assert.Len(t, dtos, 1) + assert.Equal(t, int64(1), *dtos[0].ID) + }) +} + +func TestDomainFieldTypeToOpenAPI(t *testing.T) { + t.Parallel() + + cases := []struct { + input domainExpt.FieldType + want string + }{ + {domainExpt.FieldType_EvaluatorScore, "evaluator_score"}, + {domainExpt.FieldType_CreatorBy, "creator_by"}, + {domainExpt.FieldType_UpdatedBy, "updated_by"}, + {domainExpt.FieldType_ExptStatus, "expt_status"}, + {domainExpt.FieldType_TurnRunState, "turn_run_state"}, + {domainExpt.FieldType_TargetID, "target_id"}, + {domainExpt.FieldType_EvalSetID, "eval_set_id"}, + {domainExpt.FieldType_EvaluatorID, "evaluator_id"}, + {domainExpt.FieldType_TargetType, "target_type"}, + {domainExpt.FieldType_SourceTarget, "source_target"}, + {domainExpt.FieldType_EvaluatorVersionID, "evaluator_version_id"}, + {domainExpt.FieldType_TargetVersionID, "target_version_id"}, + {domainExpt.FieldType_EvalSetVersionID, "eval_set_version_id"}, + {domainExpt.FieldType_ExptType, "expt_type"}, + {domainExpt.FieldType_SourceType, "source_type"}, + {domainExpt.FieldType_SourceID, "source_id"}, + {domainExpt.FieldType_KeywordSearch, "keyword_search"}, + {domainExpt.FieldType_EvalSetColumn, "eval_set_column"}, + {domainExpt.FieldType_Annotation, "annotation"}, + {domainExpt.FieldType_ActualOutput, "actual_output"}, + {domainExpt.FieldType_EvaluatorScoreCorrected, "evaluator_score_corrected"}, + {domainExpt.FieldType_Evaluator, "evaluator"}, + {domainExpt.FieldType_ItemID, "item_id"}, + {domainExpt.FieldType_ItemRunState, "item_run_state"}, + {domainExpt.FieldType_AnnotationScore, "annotation_score"}, + {domainExpt.FieldType_AnnotationText, "annotation_text"}, + {domainExpt.FieldType_AnnotationCategorical, "annotation_categorical"}, + {domainExpt.FieldType_TotalLatency, "total_latency"}, + {domainExpt.FieldType_InputTokens, "input_tokens"}, + {domainExpt.FieldType_OutputTokens, "output_tokens"}, + {domainExpt.FieldType_TotalTokens, "total_tokens"}, + {domainExpt.FieldType_ExperimentTemplateID, "experiment_template_id"}, + {domainExpt.FieldType_EvaluatorWeightedScore, "evaluator_weighted_score"}, + } + for _, tt := range cases { + assert.Equal(t, tt.want, domainFieldTypeToOpenAPI(tt.input)) + } + // unknown falls back to numeric + assert.Equal(t, "9999", domainFieldTypeToOpenAPI(domainExpt.FieldType(9999))) +} + +func TestDomainFilterOperatorToOpenAPI(t *testing.T) { + t.Parallel() + + cases := []struct { + input domainExpt.FilterOperatorType + want string + }{ + {domainExpt.FilterOperatorType_Equal, "equal"}, + {domainExpt.FilterOperatorType_NotEqual, "not_equal"}, + {domainExpt.FilterOperatorType_Greater, "greater"}, + {domainExpt.FilterOperatorType_GreaterOrEqual, "greater_or_equal"}, + {domainExpt.FilterOperatorType_Less, "less"}, + {domainExpt.FilterOperatorType_LessOrEqual, "less_or_equal"}, + {domainExpt.FilterOperatorType_In, "in"}, + {domainExpt.FilterOperatorType_NotIn, "not_in"}, + {domainExpt.FilterOperatorType_Like, "like"}, + {domainExpt.FilterOperatorType_NotLike, "not_like"}, + {domainExpt.FilterOperatorType_IsNull, "is_null"}, + {domainExpt.FilterOperatorType_IsNotNull, "is_not_null"}, + } + for _, tt := range cases { + assert.Equal(t, tt.want, domainFilterOperatorToOpenAPI(tt.input)) + } + assert.Equal(t, "9999", domainFilterOperatorToOpenAPI(domainExpt.FilterOperatorType(9999))) +} + +func TestDomainFilterLogicOpToOpenAPI(t *testing.T) { + t.Parallel() + + assert.Equal(t, "and", domainFilterLogicOpToOpenAPI(domainExpt.FilterLogicOp_And)) + assert.Equal(t, "or", domainFilterLogicOpToOpenAPI(domainExpt.FilterLogicOp_Or)) + assert.Equal(t, "999", domainFilterLogicOpToOpenAPI(domainExpt.FilterLogicOp(999))) +} diff --git a/backend/modules/evaluation/application/eval_openapi_app.go b/backend/modules/evaluation/application/eval_openapi_app.go index 720ace8b68..d6470e53d8 100644 --- a/backend/modules/evaluation/application/eval_openapi_app.go +++ b/backend/modules/evaluation/application/eval_openapi_app.go @@ -64,6 +64,7 @@ type EvalOpenAPIApplication struct { evaluatorRecordService service.EvaluatorRecordService exptTemplateManager service.IExptTemplateManager configer component.IConfiger + fileProvider rpc.IFileProvider } func NewEvalOpenAPIApplication(asyncRepo repo.IEvalAsyncRepo, publisher events.ExptEventPublisher, @@ -83,6 +84,7 @@ func NewEvalOpenAPIApplication(asyncRepo repo.IEvalAsyncRepo, publisher events.E evaluatorRecordService service.EvaluatorRecordService, exptTemplateManager service.IExptTemplateManager, configer component.IConfiger, + fileProvider rpc.IFileProvider, ) IEvalOpenAPIApplication { return &EvalOpenAPIApplication{ asyncRepo: asyncRepo, @@ -103,6 +105,7 @@ func NewEvalOpenAPIApplication(asyncRepo repo.IEvalAsyncRepo, publisher events.E evaluatorRecordService: evaluatorRecordService, exptTemplateManager: exptTemplateManager, configer: configer, + fileProvider: fileProvider, } } @@ -1240,6 +1243,28 @@ func (e *EvalOpenAPIApplication) ListExperimentResultOApi(ctx context.Context, r return nil, err } + for _, item := range result.ItemResults { + for _, turn := range item.TurnResults { + for _, exptResult := range turn.ExperimentResults { + if exptResult.Payload == nil || exptResult.Payload.EvaluatorOutput == nil { + continue + } + for _, record := range exptResult.Payload.EvaluatorOutput.EvaluatorRecords { + if record == nil || record.EvaluatorOutputData == nil { + logs.CtxInfo(ctx, "[ListExperimentResultOApi] record or outputData is nil, itemID=%v", item.ItemID) + continue + } + logs.CtxInfo(ctx, "[ListExperimentResultOApi] before fillExtraOutputURLs: itemID=%v, evaluatorVersionID=%v, hasStdout=%v, hasExtraOutput=%v, extraOutput=%v", + item.ItemID, record.EvaluatorVersionID, record.EvaluatorOutputData.Stdout != "", record.EvaluatorOutputData.ExtraOutput != nil, json.Jsonify(record.EvaluatorOutputData.ExtraOutput)) + } + } + } + } + + if err := e.fillExtraOutputURLs(ctx, result.ItemResults); err != nil { + logs.CtxError(ctx, "[ListExperimentResultOApi] fillExtraOutputURLs fail, err: %v", err) + } + res := &openapi.ListExperimentResultOApiResponse{ Data: &openapi.ListExperimentResultOpenAPIData{ ColumnEvalSetFields: experiment_convertor.OpenAPIColumnEvalSetFieldsDO2DTOs(result.ColumnEvalSetFields), @@ -2487,3 +2512,51 @@ func (e *EvalOpenAPIApplication) ReportEvaluatorInvokeResult_(ctx context.Contex return &openapi.ReportEvaluatorInvokeResultResponse{BaseResp: base.NewBaseResp()}, nil } + +func (e *EvalOpenAPIApplication) fillExtraOutputURLs(ctx context.Context, itemResults []*entity.ItemResult) error { + if e.fileProvider == nil { + logs.CtxWarn(ctx, "[fillExtraOutputURLs] fileProvider is nil, skip") + return nil + } + uris := make([]string, 0) + for _, item := range itemResults { + for _, turn := range item.TurnResults { + for _, exptResult := range turn.ExperimentResults { + if exptResult.Payload == nil || exptResult.Payload.EvaluatorOutput == nil { + continue + } + for _, record := range exptResult.Payload.EvaluatorOutput.EvaluatorRecords { + if record != nil && record.EvaluatorOutputData != nil && record.EvaluatorOutputData.ExtraOutput != nil && record.EvaluatorOutputData.ExtraOutput.URI != nil && *record.EvaluatorOutputData.ExtraOutput.URI != "" { + uris = append(uris, *record.EvaluatorOutputData.ExtraOutput.URI) + } + } + } + } + } + logs.CtxInfo(ctx, "[fillExtraOutputURLs] collected %d uris: %v", len(uris), uris) + if len(uris) == 0 { + return nil + } + urlMap, err := e.fileProvider.MGetFileURL(ctx, uris) + if err != nil { + return err + } + for _, item := range itemResults { + for _, turn := range item.TurnResults { + for _, exptResult := range turn.ExperimentResults { + if exptResult.Payload == nil || exptResult.Payload.EvaluatorOutput == nil { + continue + } + for _, record := range exptResult.Payload.EvaluatorOutput.EvaluatorRecords { + if record != nil && record.EvaluatorOutputData != nil && record.EvaluatorOutputData.ExtraOutput != nil && record.EvaluatorOutputData.ExtraOutput.URI != nil { + uri := *record.EvaluatorOutputData.ExtraOutput.URI + if url, ok := urlMap[uri]; ok { + record.EvaluatorOutputData.ExtraOutput.URL = &url + } + } + } + } + } + } + return nil +} diff --git a/backend/modules/evaluation/application/eval_openapi_app_test.go b/backend/modules/evaluation/application/eval_openapi_app_test.go index 9df271f392..e7fc06719d 100755 --- a/backend/modules/evaluation/application/eval_openapi_app_test.go +++ b/backend/modules/evaluation/application/eval_openapi_app_test.go @@ -5703,7 +5703,8 @@ func TestEvalOpenAPIApplication_ReportEvaluatorInvokeResult(t *testing.T) { } assert.True(t, ev.AsyncEvaluatorReportTrigger) return nil - }) + }, + ) }, }, } @@ -6866,3 +6867,242 @@ func TestMapOpenAPIExptRetryMode(t *testing.T) { }) } } + +func TestEvalOpenAPIApplication_fillExtraOutputURLs(t *testing.T) { + t.Parallel() + + // helper to build an itemResults slice carrying a single record with the given uri. + buildItemResults := func(uri *string) []*entity.ItemResult { + return []*entity.ItemResult{ + { + ItemID: 1, + TurnResults: []*entity.TurnResult{ + { + TurnID: 1, + ExperimentResults: []*entity.ExperimentResult{ + { + ExperimentID: 1, + Payload: &entity.ExperimentTurnPayload{ + EvaluatorOutput: &entity.TurnEvaluatorOutput{ + EvaluatorRecords: map[int64]*entity.EvaluatorRecord{ + 10: { + ID: 10, + EvaluatorOutputData: &entity.EvaluatorOutputData{ + ExtraOutput: &entity.EvaluatorExtraOutputContent{ + URI: uri, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + } + + tests := []struct { + name string + fileNil bool + itemResults []*entity.ItemResult + setup func(fp *rpcmocks.MockIFileProvider) + wantErr bool + // assert mutates expectations on the itemResults after the call + assert func(t *testing.T, itemResults []*entity.ItemResult) + }{ + { + name: "fileProvider nil - returns nil without mutation", + fileNil: true, + itemResults: buildItemResults(gptr.Of("u/1")), + setup: func(_ *rpcmocks.MockIFileProvider) {}, + wantErr: false, + assert: func(t *testing.T, itemResults []*entity.ItemResult) { + rec := itemResults[0].TurnResults[0].ExperimentResults[0].Payload.EvaluatorOutput.EvaluatorRecords[10] + assert.Nil(t, rec.EvaluatorOutputData.ExtraOutput.URL) + }, + }, + { + name: "no uris collected - returns nil, MGetFileURL not called", + fileNil: false, + // empty uri + a payload with nil EvaluatorOutput + nil record branches + itemResults: []*entity.ItemResult{ + { + TurnResults: []*entity.TurnResult{ + { + ExperimentResults: []*entity.ExperimentResult{ + {Payload: nil}, + {Payload: &entity.ExperimentTurnPayload{EvaluatorOutput: nil}}, + { + Payload: &entity.ExperimentTurnPayload{ + EvaluatorOutput: &entity.TurnEvaluatorOutput{ + EvaluatorRecords: map[int64]*entity.EvaluatorRecord{ + 1: nil, + 2: {EvaluatorOutputData: nil}, + 3: {EvaluatorOutputData: &entity.EvaluatorOutputData{ExtraOutput: nil}}, + 4: {EvaluatorOutputData: &entity.EvaluatorOutputData{ExtraOutput: &entity.EvaluatorExtraOutputContent{URI: nil}}}, + 5: {EvaluatorOutputData: &entity.EvaluatorOutputData{ExtraOutput: &entity.EvaluatorExtraOutputContent{URI: gptr.Of("")}}}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + setup: func(_ *rpcmocks.MockIFileProvider) {}, + wantErr: false, + assert: func(_ *testing.T, _ []*entity.ItemResult) {}, + }, + { + name: "MGetFileURL returns error - error propagated", + fileNil: false, + itemResults: buildItemResults(gptr.Of("u/err")), + setup: func(fp *rpcmocks.MockIFileProvider) { + fp.EXPECT().MGetFileURL(gomock.Any(), []string{"u/err"}).Return(nil, errors.New("mget failed")) + }, + wantErr: true, + assert: func(t *testing.T, itemResults []*entity.ItemResult) { + rec := itemResults[0].TurnResults[0].ExperimentResults[0].Payload.EvaluatorOutput.EvaluatorRecords[10] + assert.Nil(t, rec.EvaluatorOutputData.ExtraOutput.URL) + }, + }, + { + name: "success - signed URL written back", + fileNil: false, + itemResults: buildItemResults(gptr.Of("u/ok")), + setup: func(fp *rpcmocks.MockIFileProvider) { + fp.EXPECT().MGetFileURL(gomock.Any(), []string{"u/ok"}).Return(map[string]string{"u/ok": "https://signed/u/ok"}, nil) + }, + wantErr: false, + assert: func(t *testing.T, itemResults []*entity.ItemResult) { + rec := itemResults[0].TurnResults[0].ExperimentResults[0].Payload.EvaluatorOutput.EvaluatorRecords[10] + if assert.NotNil(t, rec.EvaluatorOutputData.ExtraOutput.URL) { + assert.Equal(t, "https://signed/u/ok", *rec.EvaluatorOutputData.ExtraOutput.URL) + } + }, + }, + { + name: "success - uri missing in urlMap leaves URL nil", + fileNil: false, + itemResults: buildItemResults(gptr.Of("u/miss")), + setup: func(fp *rpcmocks.MockIFileProvider) { + fp.EXPECT().MGetFileURL(gomock.Any(), []string{"u/miss"}).Return(map[string]string{"u/other": "https://signed/other"}, nil) + }, + wantErr: false, + assert: func(t *testing.T, itemResults []*entity.ItemResult) { + rec := itemResults[0].TurnResults[0].ExperimentResults[0].Payload.EvaluatorOutput.EvaluatorRecords[10] + assert.Nil(t, rec.EvaluatorOutputData.ExtraOutput.URL) + }, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + app := &EvalOpenAPIApplication{} + if !tc.fileNil { + fp := rpcmocks.NewMockIFileProvider(ctrl) + tc.setup(fp) + app.fileProvider = fp + } + + err := app.fillExtraOutputURLs(context.Background(), tc.itemResults) + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + tc.assert(t, tc.itemResults) + }) + } +} + +func TestEvalOpenAPIApplication_ListExperimentResultOApi_PopulatedResult(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + auth := rpcmocks.NewMockIAuthProvider(ctrl) + resultSvc := servicemocks.NewMockExptResultService(ctrl) + fp := rpcmocks.NewMockIFileProvider(ctrl) + metric := &fakeOpenAPIMetric{} + + app := &EvalOpenAPIApplication{ + auth: auth, + resultSvc: resultSvc, + fileProvider: fp, + metric: metric, + } + + // itemResults carrying records that exercise the logging loop (nil outputData branch + + // extra-output branch) and feed fillExtraOutputURLs with a real provider. + itemResults := []*entity.ItemResult{ + { + ItemID: 7, + TurnResults: []*entity.TurnResult{ + { + TurnID: 1, + ExperimentResults: []*entity.ExperimentResult{ + {Payload: nil}, + {Payload: &entity.ExperimentTurnPayload{EvaluatorOutput: nil}}, + { + Payload: &entity.ExperimentTurnPayload{ + EvaluatorOutput: &entity.TurnEvaluatorOutput{ + EvaluatorRecords: map[int64]*entity.EvaluatorRecord{ + 1: nil, + 2: {EvaluatorOutputData: nil}, + 3: { + EvaluatorVersionID: 99, + EvaluatorOutputData: &entity.EvaluatorOutputData{ + ExtraOutput: &entity.EvaluatorExtraOutputContent{URI: gptr.Of("k1")}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + auth.EXPECT().Authorization(gomock.Any(), gomock.Any()).Return(nil) + resultSvc.EXPECT().MGetExperimentResult(gomock.Any(), gomock.Any()).Return(&entity.MGetExperimentReportResult{ + Total: 1, + ItemResults: itemResults, + ExptColumnsEvalTarget: []*entity.ExptColumnEvalTarget{ + {ExptID: 100, Columns: []*entity.ColumnEvalTarget{{Name: "out"}}}, + }, + }, nil) + fp.EXPECT().MGetFileURL(gomock.Any(), []string{"k1"}).Return(map[string]string{"k1": "https://signed/k1"}, nil) + + resp, err := app.ListExperimentResultOApi(context.Background(), &openapi.ListExperimentResultOApiRequest{ + WorkspaceID: gptr.Of(int64(1)), + ExperimentID: gptr.Of(int64(100)), + PageNum: gptr.Of(int32(1)), + PageSize: gptr.Of(int32(20)), + }) + + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + assert.Equal(t, int64(1), *resp.Data.Total) + assert.NotNil(t, resp.Data.ColumnEvalTargets) + // URL signed back into the record + url := itemResults[0].TurnResults[0].ExperimentResults[2].Payload.EvaluatorOutput.EvaluatorRecords[3].EvaluatorOutputData.ExtraOutput.URL + if assert.NotNil(t, url) { + assert.Equal(t, "https://signed/k1", *url) + } +} diff --git a/backend/modules/evaluation/application/experiment_app.go b/backend/modules/evaluation/application/experiment_app.go index 6d0d0403ae..bb63ab9a44 100644 --- a/backend/modules/evaluation/application/experiment_app.go +++ b/backend/modules/evaluation/application/experiment_app.go @@ -1053,6 +1053,11 @@ func (e *experimentApplication) UpdateExperiment(ctx context.Context, req *expt. return nil, err } + // 水平越权校验:仅当实验确实归属当前 workspace 时才允许更新,避免被改写 space_id 搬到其他 workspace + if got.SpaceID != req.GetWorkspaceID() { + return nil, errorx.NewByCode(errno.CommonBadRequestCode, errorx.WithExtraMsg(fmt.Sprintf("expt %d not found in space %d", req.GetExptID(), req.GetWorkspaceID()))) + } + if got.Name != req.GetName() { pass, err := e.manager.CheckName(ctx, req.GetName(), req.GetWorkspaceID(), session) if err != nil { diff --git a/backend/modules/evaluation/application/experiment_app_test.go b/backend/modules/evaluation/application/experiment_app_test.go index d70d9df3b0..fe7ac5ce90 100644 --- a/backend/modules/evaluation/application/experiment_app_test.go +++ b/backend/modules/evaluation/application/experiment_app_test.go @@ -207,7 +207,8 @@ func TestExperimentApplication_CreateExperiment(t *testing.T) { // 10001 (initial) + 10101 (resolved) + 20200 (resolved) assert.ElementsMatch(t, []int64{10001, 10101, 20200}, param.EvaluatorVersionIds) return validExpt, nil - }) + }, + ) }, postCheck: func(t *testing.T, req *exptpb.CreateExperimentRequest) { assert.Equal(t, []int64{10001, 10101, 20200}, req.EvaluatorVersionIds) @@ -1918,6 +1919,30 @@ func TestExperimentApplication_UpdateExperiment(t *testing.T) { }, wantErr: true, }, + { + name: "workspace mismatch with experiment space", + req: &exptpb.UpdateExperimentRequest{ + ExptID: validExptID, + WorkspaceID: validWorkspaceID, + Name: gptr.Of("updated_experiment"), + Desc: gptr.Of("updated description"), + }, + mockSetup: func() { + mismatchedExpt := &entity.Experiment{ + ID: validExptID, + SpaceID: validWorkspaceID + 1, + Name: "test_experiment_other_space", + Status: entity.ExptStatus_Pending, + CreatedBy: validUserID, + } + // 返回归属于其他 workspace 的实验,应在写库前被越权校验拦截 + mockManager.EXPECT(). + Get(gomock.Any(), validExptID, validWorkspaceID, &entity.Session{}). + Return(mismatchedExpt, nil) + // 不应再调用 Update:mockManager.Update 未设置 EXPECT,被调用即失败 + }, + wantErr: true, + }, } for _, tt := range tests { @@ -2965,7 +2990,8 @@ func TestExperimentApplication_KillExperiment(t *testing.T) { // 异步终止运行失败:允许后台调用 mockManager.EXPECT().CompleteRun(gomock.Any(), validExptID, validRunID, validWorkspaceID, gomock.Any(), gomock.Any()).Return( - errorx.NewByCode(errno.CommonInternalErrorCode)).AnyTimes() + errorx.NewByCode(errno.CommonInternalErrorCode), + ).AnyTimes() }, wantResp: &exptpb.KillExperimentResponse{BaseResp: base.NewBaseResp()}, wantErr: false, @@ -2997,7 +3023,8 @@ func TestExperimentApplication_KillExperiment(t *testing.T) { // 异步终止 mockManager.EXPECT().CompleteRun(gomock.Any(), validExptID, validRunID, validWorkspaceID, gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mockManager.EXPECT().CompleteExpt(gomock.Any(), validExptID, gomock.Any(), validWorkspaceID, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return( - errorx.NewByCode(errno.CommonInternalErrorCode)).AnyTimes() + errorx.NewByCode(errno.CommonInternalErrorCode), + ).AnyTimes() }, wantResp: &exptpb.KillExperimentResponse{BaseResp: base.NewBaseResp()}, wantErr: false, @@ -4126,7 +4153,8 @@ func TestExperimentApplication_BatchGetExperimentAggrResult_(t *testing.T) { }, }, }, - }, nil) + }, nil, + ) }, wantResp: &exptpb.BatchGetExperimentAggrResultResponse{ @@ -7547,3 +7575,161 @@ func TestExperimentApplication_SubmitExptFromTemplate(t *testing.T) { }) } } + +func TestExperimentApplication_UpdateExperiment_MoreBranches(t *testing.T) { + workspaceID := int64(123) + exptID := int64(456) + userID := "789" + + baseExpt := func() *entity.Experiment { + return &entity.Experiment{ + ID: exptID, + SpaceID: workspaceID, + Name: "orig_name", + Status: entity.ExptStatus_Pending, + CreatedBy: userID, + } + } + + tests := []struct { + name string + req *exptpb.UpdateExperimentRequest + mockSetup func(mgr *servicemocks.MockIExptManager, auth *rpcmocks.MockIAuthProvider, ui *userinfomocks.MockUserInfoService) + wantErr bool + }{ + { + name: "manager.Get error", + req: &exptpb.UpdateExperimentRequest{ + ExptID: exptID, + WorkspaceID: workspaceID, + Name: gptr.Of("n"), + }, + mockSetup: func(mgr *servicemocks.MockIExptManager, _ *rpcmocks.MockIAuthProvider, _ *userinfomocks.MockUserInfoService) { + mgr.EXPECT().Get(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(nil, errors.New("get failed")) + }, + wantErr: true, + }, + { + name: "CheckName error", + req: &exptpb.UpdateExperimentRequest{ + ExptID: exptID, + WorkspaceID: workspaceID, + Name: gptr.Of("new_name"), + }, + mockSetup: func(mgr *servicemocks.MockIExptManager, _ *rpcmocks.MockIAuthProvider, _ *userinfomocks.MockUserInfoService) { + mgr.EXPECT().Get(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(baseExpt(), nil) + mgr.EXPECT().CheckName(gomock.Any(), "new_name", workspaceID, gomock.Any()).Return(false, errors.New("check failed")) + }, + wantErr: true, + }, + { + name: "invalid notification_conf", + req: func() *exptpb.UpdateExperimentRequest { + return &exptpb.UpdateExperimentRequest{ + ExptID: exptID, + WorkspaceID: workspaceID, + Name: gptr.Of("orig_name"), // same name -> skip CheckName + NotificationConf: &expt.ExptNotificationConf{ + Filter: &expt.Filters{ + FilterConditions: []*expt.FilterCondition{ + { + Operator: expt.FilterOperatorType(999), // invalid -> conversion error + Value: "v", + }, + }, + }, + }, + } + }(), + mockSetup: func(mgr *servicemocks.MockIExptManager, auth *rpcmocks.MockIAuthProvider, _ *userinfomocks.MockUserInfoService) { + mgr.EXPECT().Get(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(baseExpt(), nil) + auth.EXPECT().AuthorizationWithoutSPI(gomock.Any(), gomock.Any()).Return(nil) + }, + wantErr: true, + }, + { + name: "manager.Update error", + req: &exptpb.UpdateExperimentRequest{ + ExptID: exptID, + WorkspaceID: workspaceID, + Name: gptr.Of("orig_name"), + }, + mockSetup: func(mgr *servicemocks.MockIExptManager, auth *rpcmocks.MockIAuthProvider, _ *userinfomocks.MockUserInfoService) { + mgr.EXPECT().Get(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(baseExpt(), nil) + auth.EXPECT().AuthorizationWithoutSPI(gomock.Any(), gomock.Any()).Return(nil) + mgr.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("update failed")) + }, + wantErr: true, + }, + { + name: "GetDetail error", + req: &exptpb.UpdateExperimentRequest{ + ExptID: exptID, + WorkspaceID: workspaceID, + Name: gptr.Of("orig_name"), + }, + mockSetup: func(mgr *servicemocks.MockIExptManager, auth *rpcmocks.MockIAuthProvider, _ *userinfomocks.MockUserInfoService) { + mgr.EXPECT().Get(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(baseExpt(), nil) + auth.EXPECT().AuthorizationWithoutSPI(gomock.Any(), gomock.Any()).Return(nil) + mgr.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + mgr.EXPECT().GetDetail(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(nil, errors.New("detail failed")) + }, + wantErr: true, + }, + { + name: "success with notification_conf", + req: &exptpb.UpdateExperimentRequest{ + ExptID: exptID, + WorkspaceID: workspaceID, + Name: gptr.Of("orig_name"), + NotificationConf: &expt.ExptNotificationConf{ + Webhook: &expt.WebhookNotificationConf{ + Enable: true, + Urls: gptr.Of("https://hook"), + }, + }, + }, + mockSetup: func(mgr *servicemocks.MockIExptManager, auth *rpcmocks.MockIAuthProvider, ui *userinfomocks.MockUserInfoService) { + mgr.EXPECT().Get(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(baseExpt(), nil) + auth.EXPECT().AuthorizationWithoutSPI(gomock.Any(), gomock.Any()).Return(nil) + mgr.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, updated *entity.Experiment, _ *entity.Session) error { + assert.NotNil(t, updated.NotificationConf) + return nil + }, + ) + mgr.EXPECT().GetDetail(gomock.Any(), exptID, workspaceID, gomock.Any()).Return(baseExpt(), nil) + ui.EXPECT().PackUserInfo(gomock.Any(), gomock.Any()).AnyTimes() + }, + wantErr: false, + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mgr := servicemocks.NewMockIExptManager(ctrl) + auth := rpcmocks.NewMockIAuthProvider(ctrl) + ui := userinfomocks.NewMockUserInfoService(ctrl) + + app := &experimentApplication{ + manager: mgr, + auth: auth, + userInfoService: ui, + } + + tc.mockSetup(mgr, auth, ui) + + resp, err := app.UpdateExperiment(context.Background(), tc.req) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, resp) + } + }) + } +} diff --git a/backend/modules/evaluation/application/wire_gen.go b/backend/modules/evaluation/application/wire_gen.go index 80d81f0e43..3b6ce6155e 100644 --- a/backend/modules/evaluation/application/wire_gen.go +++ b/backend/modules/evaluation/application/wire_gen.go @@ -420,7 +420,7 @@ func InitEvalOpenAPIApplication(ctx context.Context, configFactory conf.IConfigL webhookDispatcher := service.NewWebhookDispatcher(exptEventPublisher, noopWebhookSecretProvider, iExptStatsRepo) exptLifecycleEventHandler := service.NewExptLifecycleEventHandler(iExperimentRepo, iNotifyRPCAdapter, iUserProvider, webhookDispatcher) iExperimentApplication := NewExperimentApplication(exptAggrResultService, exptResultService, iExptManager, exptSchedulerEvent, exptItemEvalEvent, idgen2, iConfiger, iAuthProvider, userInfoService, iEvalTargetService, evaluationSetItemService, iExptAnnotateService, iTagRPCAdapter, iExptResultExportService, iExptInsightAnalysisService, evaluatorService, iExptTemplateManager, iFileProvider, exptLifecycleEventHandler) - evalOpenAPIService := NewEvalOpenAPIApplication(iEvalAsyncRepo, exptEventPublisher, iEvalTargetService, iAuthProvider, iEvaluationSetService, evaluationSetVersionService, evaluationSetItemService, evaluationSetSchemaService, openAPIEvaluationMetrics, userInfoService, iExperimentApplication, iExptManager, exptResultService, exptAggrResultService, evaluatorService, evaluatorRecordService, iExptTemplateManager, iConfiger) + evalOpenAPIService := NewEvalOpenAPIApplication(iEvalAsyncRepo, exptEventPublisher, iEvalTargetService, iAuthProvider, iEvaluationSetService, evaluationSetVersionService, evaluationSetItemService, evaluationSetSchemaService, openAPIEvaluationMetrics, userInfoService, iExperimentApplication, iExptManager, exptResultService, exptAggrResultService, evaluatorService, evaluatorRecordService, iExptTemplateManager, iConfiger, iFileProvider) return evalOpenAPIService, nil } diff --git a/backend/modules/evaluation/domain/service/expt_result_aggr_impl.go b/backend/modules/evaluation/domain/service/expt_result_aggr_impl.go index 6f5159a6b6..bb778d34ee 100644 --- a/backend/modules/evaluation/domain/service/expt_result_aggr_impl.go +++ b/backend/modules/evaluation/domain/service/expt_result_aggr_impl.go @@ -501,6 +501,13 @@ func (e *ExptAggrResultServiceImpl) BatchGetExptAggrResultByExperimentIDs(ctx co return nil, err } + validExptIDs := make([]int64, 0, len(expts)) + for _, expt := range expts { + if expt.SpaceID == spaceID { + validExptIDs = append(validExptIDs, expt.ID) + } + } + versionedTargetIDMap := gslice.ToMap(expts, func(t *entity.Experiment) (int64, entity.VersionedTargetID) { return t.ID, entity.VersionedTargetID{ TargetID: t.TargetID, @@ -508,7 +515,7 @@ func (e *ExptAggrResultServiceImpl) BatchGetExptAggrResultByExperimentIDs(ctx co } }) - aggrResults, err := e.exptAggrResultRepo.BatchGetExptAggrResultByExperimentIDs(ctx, exptIDs) + aggrResults, err := e.exptAggrResultRepo.BatchGetExptAggrResultByExperimentIDs(ctx, validExptIDs) if err != nil { return nil, err } @@ -522,7 +529,7 @@ func (e *ExptAggrResultServiceImpl) BatchGetExptAggrResultByExperimentIDs(ctx co expt2AggrResults[aggrResult.ExperimentID] = append(expt2AggrResults[aggrResult.ExperimentID], aggrResult) } - evaluatorRef, err := e.experimentRepo.GetEvaluatorRefByExptIDs(ctx, exptIDs, spaceID) + evaluatorRef, err := e.experimentRepo.GetEvaluatorRefByExptIDs(ctx, validExptIDs, spaceID) if err != nil { return nil, err } @@ -542,7 +549,7 @@ func (e *ExptAggrResultServiceImpl) BatchGetExptAggrResultByExperimentIDs(ctx co return nil, err } - tagInfoMap, err := e.batchGetTagInfoByExperimentIDs(ctx, spaceID, exptIDs) + tagInfoMap, err := e.batchGetTagInfoByExperimentIDs(ctx, spaceID, validExptIDs) if err != nil { return nil, err } diff --git a/backend/modules/evaluation/domain/service/expt_result_aggr_impl_test.go b/backend/modules/evaluation/domain/service/expt_result_aggr_impl_test.go index 819d9ad154..e88d56f180 100644 --- a/backend/modules/evaluation/domain/service/expt_result_aggr_impl_test.go +++ b/backend/modules/evaluation/domain/service/expt_result_aggr_impl_test.go @@ -377,7 +377,7 @@ func TestExptAggrResultServiceImpl_BatchGetExptAggrResultByExperimentIDs(t *test mockTagRPCAdapter *rpcmocks.MockITagRPCAdapter, mockAnnotateRepo *repoMocks.MockIExptAnnotateRepo, ) { // Mock experiments - mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{1}).Return([]*entity.Experiment{{ID: 1, TargetID: 10, TargetVersionID: 20}}, nil) + mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{1}).Return([]*entity.Experiment{{ID: 1, SpaceID: 100, TargetID: 10, TargetVersionID: 20}}, nil) // Mock aggregation results aggrResult := &entity.AggregateResult{ @@ -447,7 +447,8 @@ func TestExptAggrResultServiceImpl_BatchGetExptAggrResultByExperimentIDs(t *test map[int64]*entity.TagInfo{1: { TagKeyId: 1, TagKeyName: "123", - }}, nil) + }}, nil, + ) // Mock annotate refs mockAnnotateRepo.EXPECT().BatchGetExptTurnAnnotateRecordRefs(gomock.Any(), gomock.Any(), gomock.Any()).Return( @@ -458,7 +459,8 @@ func TestExptAggrResultServiceImpl_BatchGetExptAggrResultByExperimentIDs(t *test ExptID: 1, AnnotateRecordID: 1, }, - }, nil) + }, nil, + ) }, want: []*entity.ExptAggregateResult{ { @@ -513,6 +515,81 @@ func TestExptAggrResultServiceImpl_BatchGetExptAggrResultByExperimentIDs(t *test }, wantErr: false, }, + { + // 水平越权过滤专项用例:同一批 exptIDs 中混入一条跨 space 的实验(SpaceID != 入参 spaceID), + // 断言所有下游查询(BatchGetExptAggrResultByExperimentIDs / GetEvaluatorRefByExptIDs / + // batchGetTagInfoByExperimentIDs->BatchGetExptTurnAnnotateRecordRefs)只对 valid 的 exptID 发起, + // 越权的 exptID 被 expt.SpaceID == spaceID 过滤掉,最终结果不包含越权实验。 + name: "Filter out cross-space experiments to prevent horizontal privilege escalation", + spaceID: 100, + exptIDs: []int64{1, 2}, + setup: func(mockExptAggrResultRepo *repoMocks.MockIExptAggrResultRepo, mockExperimentRepo *repoMocks.MockIExperimentRepo, mockEvaluatorService *svcMocks.MockEvaluatorService, + mockTagRPCAdapter *rpcmocks.MockITagRPCAdapter, mockAnnotateRepo *repoMocks.MockIExptAnnotateRepo, + ) { + // expt 1 属于入参 space(100) -> valid;expt 2 属于其他 space(200) -> 越权,应被过滤 + mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{1, 2}).Return([]*entity.Experiment{ + {ID: 1, SpaceID: 100, TargetID: 10, TargetVersionID: 20}, + {ID: 2, SpaceID: 200, TargetID: 11, TargetVersionID: 21}, + }, nil) + + aggrResult := &entity.AggregateResult{ + AggregatorResults: []*entity.AggregatorResult{ + { + AggregatorType: entity.Average, + Data: &entity.AggregateData{ + DataType: entity.Double, + Value: gptr.Of(0.8), + }, + }, + }, + } + aggrResultBytes, _ := json.Marshal(aggrResult) + // 关键断言:聚合结果查询只对 valid exptID(1) 发起,不含越权 exptID(2) + mockExptAggrResultRepo.EXPECT(). + BatchGetExptAggrResultByExperimentIDs(gomock.Any(), []int64{1}). + Return([]*entity.ExptAggrResult{ + { + ExperimentID: 1, + FieldType: int32(entity.FieldType_TargetLatency), + FieldKey: entity.AggrResultFieldKey_TargetLatency, + AggrResult: aggrResultBytes, + UpdateAt: gptr.Of(time.Unix(1000, 0)), + }, + }, nil) + + // 关键断言:评估器引用查询只对 valid exptID(1) 发起 + mockExperimentRepo.EXPECT(). + GetEvaluatorRefByExptIDs(gomock.Any(), []int64{1}, int64(100)). + Return([]*entity.ExptEvaluatorRef{}, nil) + mockEvaluatorService.EXPECT(). + BatchGetEvaluatorVersion(gomock.Any(), gomock.Nil(), []int64{}, true). + Return([]*entity.Evaluator{}, nil) + + // 关键断言:标签信息查询的 annotate refs 只对 valid exptID(1) 发起 + mockAnnotateRepo.EXPECT(). + BatchGetExptTurnAnnotateRecordRefs(gomock.Any(), []int64{1}, int64(100)). + Return([]*entity.ExptTurnAnnotateRecordRef{}, nil) + mockTagRPCAdapter.EXPECT(). + BatchGetTagInfo(gomock.Any(), int64(100), []int64{}). + Return(map[int64]*entity.TagInfo{}, nil) + }, + want: []*entity.ExptAggregateResult{ + { + ExperimentID: 1, + EvaluatorResults: map[int64]*entity.EvaluatorAggregateResult{}, + AnnotationResults: map[int64]*entity.AnnotationAggregateResult{}, + TargetResults: &entity.EvalTargetMtrAggrResult{ + TargetID: 10, + TargetVersionID: 20, + LatencyAggrResults: []*entity.AggregatorResult{ + {AggregatorType: entity.Average, Data: &entity.AggregateData{DataType: entity.Double, Value: gptr.Of(0.8)}}, + }, + }, + UpdateTime: gptr.Of(time.Unix(1000, 0)), + }, + }, + wantErr: false, + }, { name: "Batch get aggregation results successfully with all target metrics", spaceID: 100, @@ -520,7 +597,7 @@ func TestExptAggrResultServiceImpl_BatchGetExptAggrResultByExperimentIDs(t *test setup: func(mockExptAggrResultRepo *repoMocks.MockIExptAggrResultRepo, mockExperimentRepo *repoMocks.MockIExperimentRepo, mockEvaluatorService *svcMocks.MockEvaluatorService, mockTagRPCAdapter *rpcmocks.MockITagRPCAdapter, mockAnnotateRepo *repoMocks.MockIExptAnnotateRepo, ) { - mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{2}).Return([]*entity.Experiment{{ID: 2, TargetID: 10, TargetVersionID: 20}}, nil) + mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{2}).Return([]*entity.Experiment{{ID: 2, SpaceID: 100, TargetID: 10, TargetVersionID: 20}}, nil) aggrResult := &entity.AggregateResult{ AggregatorResults: []*entity.AggregatorResult{ @@ -580,7 +657,7 @@ func TestExptAggrResultServiceImpl_BatchGetExptAggrResultByExperimentIDs(t *test setup: func(mockExptAggrResultRepo *repoMocks.MockIExptAggrResultRepo, mockExperimentRepo *repoMocks.MockIExperimentRepo, mockEvaluatorService *svcMocks.MockEvaluatorService, mockTagRPCAdapter *rpcmocks.MockITagRPCAdapter, mockAnnotateRepo *repoMocks.MockIExptAnnotateRepo, ) { - mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{1}).Return([]*entity.Experiment{{ID: 1}}, nil) + mockExperimentRepo.EXPECT().MGetBasicByID(gomock.Any(), []int64{1}).Return([]*entity.Experiment{{ID: 1, SpaceID: 100}}, nil) mockExptAggrResultRepo.EXPECT(). BatchGetExptAggrResultByExperimentIDs(gomock.Any(), []int64{1}). Return(nil, errorx.NewByCode(500, errorx.WithExtraMsg("db error"))) diff --git a/idl/thrift/coze/loop/evaluation/domain_openapi/evaluator.thrift b/idl/thrift/coze/loop/evaluation/domain_openapi/evaluator.thrift index a25098a52c..c1b877589a 100644 --- a/idl/thrift/coze/loop/evaluation/domain_openapi/evaluator.thrift +++ b/idl/thrift/coze/loop/evaluation/domain_openapi/evaluator.thrift @@ -154,6 +154,12 @@ struct EvaluatorOutputData { 3: optional EvaluatorRunError evaluator_run_error 4: optional i64 time_consuming_ms (api.js_conv = 'true', go.tag = 'json:"time_consuming_ms"') 11: optional string stdout + 12: optional EvaluatorExtraOutputContent extra_output +} + +struct EvaluatorExtraOutputContent { + 1: optional string output_type + 2: optional string url } // 评估器输入数据