diff --git a/common/go/go.mod b/common/go/go.mod index 01fd533..49feeb2 100644 --- a/common/go/go.mod +++ b/common/go/go.mod @@ -2,7 +2,7 @@ module github.com/curioswitch/cookchat/common go 1.25.0 -require google.golang.org/genai v1.24.0 +require google.golang.org/genai v1.25.0 require ( cloud.google.com/go v0.122.0 // indirect diff --git a/common/go/go.sum b/common/go/go.sum index 6deb7bc..07af01c 100644 --- a/common/go/go.sum +++ b/common/go/go.sum @@ -57,8 +57,8 @@ golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genai v1.24.0 h1:j5lt+Qr7W0+OBxwwEPe4DQ+ygEqpvZuSBvYoHIuUjhg= -google.golang.org/genai v1.24.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M= +google.golang.org/genai v1.25.0 h1:Cpyh2nmEoOS1eM3mT9XKuA/qWTEDoktfP2gsN3EduPE= +google.golang.org/genai v1.25.0/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= diff --git a/crawler/server/go.mod b/crawler/server/go.mod index b1390ca..5a18556 100644 --- a/crawler/server/go.mod +++ b/crawler/server/go.mod @@ -12,7 +12,7 @@ require ( github.com/curioswitch/go-curiostack v0.0.0-20250909080149-d127817587e7 github.com/gocolly/colly/v2 v2.2.0 github.com/wandb/parallel v0.2.2 - google.golang.org/genai v1.24.0 + google.golang.org/genai v1.25.0 google.golang.org/grpc v1.75.1 ) diff --git a/crawler/server/go.sum b/crawler/server/go.sum index 59ca494..d486509 100644 --- a/crawler/server/go.sum +++ b/crawler/server/go.sum @@ -293,8 +293,8 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genai v1.24.0 h1:j5lt+Qr7W0+OBxwwEPe4DQ+ygEqpvZuSBvYoHIuUjhg= -google.golang.org/genai v1.24.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M= +google.golang.org/genai v1.25.0 h1:Cpyh2nmEoOS1eM3mT9XKuA/qWTEDoktfP2gsN3EduPE= +google.golang.org/genai v1.25.0/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= diff --git a/frontend/api/build/descriptors/descriptorset.pb b/frontend/api/build/descriptors/descriptorset.pb deleted file mode 100644 index f68caf2..0000000 Binary files a/frontend/api/build/descriptors/descriptorset.pb and /dev/null differ diff --git a/frontend/api/build/go.mod b/frontend/api/build/go.mod index 719a165..608035c 100644 --- a/frontend/api/build/go.mod +++ b/frontend/api/build/go.mod @@ -5,19 +5,24 @@ go 1.25.0 require ( github.com/curioswitch/go-build v0.6.1 github.com/curioswitch/go-curiostack v0.0.0-20250909080149-d127817587e7 + github.com/curioswitch/go-docs-handler v0.1.5 + github.com/curioswitch/go-docs-handler/plugins/proto v0.1.5 + github.com/goyek/goyek/v2 v2.3.0 github.com/goyek/x v0.3.0 + google.golang.org/protobuf v1.36.9 ) require ( github.com/fatih/color v1.18.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goyek/goyek/v2 v2.3.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/parsers/yaml v1.1.0 // indirect github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect github.com/knadh/koanf/providers/rawbytes v1.0.0 // indirect github.com/knadh/koanf/v2 v2.2.2 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect diff --git a/frontend/api/build/go.sum b/frontend/api/build/go.sum index 2ba1e6e..4626d3b 100644 --- a/frontend/api/build/go.sum +++ b/frontend/api/build/go.sum @@ -3,12 +3,18 @@ github.com/curioswitch/go-build v0.6.1 h1:6rIt0hQkWVoVARaXjOEwAU/07wTEA0KkHglscA github.com/curioswitch/go-build v0.6.1/go.mod h1:6YV8tVSmfwamIWTkmKkMUzgoI+SnZNOUQI2CXFs5Rv8= github.com/curioswitch/go-curiostack v0.0.0-20250909080149-d127817587e7 h1:No3mNL0Hcb4CxDT9hcsntqYqJIoSOMfVPhfiSmQ/uFA= github.com/curioswitch/go-curiostack v0.0.0-20250909080149-d127817587e7/go.mod h1:+334ClkBgQj5vbKd+9qtGAbEhOPPRvor2l8v0ZMtjhM= +github.com/curioswitch/go-docs-handler v0.1.5 h1:MIdGAlcpiQLi1grnPwc/CVBuSOu374b7S3W2GKU0oiU= +github.com/curioswitch/go-docs-handler v0.1.5/go.mod h1:gSxE5Z46knEcLh6EtrwlmsApqcAp2bQUJl3csQiePWI= +github.com/curioswitch/go-docs-handler/plugins/proto v0.1.5 h1:OjIICuxWhb+3+0GJ/wsbwk4H/596lXucVBO7lcQoEh8= +github.com/curioswitch/go-docs-handler/plugins/proto v0.1.5/go.mod h1:Cka/I8hexDKVFwu/T01m44w5TPrCDu/ip1bTkOFEJmk= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/goyek/goyek/v2 v2.3.0 h1:ZQlQZp+JOkbZXuPNw6SbeQxYMRKB0Gm3nbC2K9IY1r4= github.com/goyek/goyek/v2 v2.3.0/go.mod h1:VKzmleKfV5LB9f20Xg5umuPC4XfSdDXwPNesfr1RsLU= github.com/goyek/x v0.3.0 h1:JEgcLmqnxAPFR7/IqPMBoSRUcvykNS3o7YCVrC1VKHQ= @@ -30,6 +36,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -53,6 +61,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/frontend/api/build/jsonschema.go b/frontend/api/build/jsonschema.go new file mode 100644 index 0000000..f39296c --- /dev/null +++ b/frontend/api/build/jsonschema.go @@ -0,0 +1,230 @@ +// Copyright (c) CurioSwitch (choko@curioswitch.org) +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + "encoding/json" + "strings" + + "github.com/curioswitch/go-docs-handler/specification" +) + +// Modified go-docs-handler jsonschema generation to generate for types and +// remove $refs. + +type additionalPropertiesSchema struct { + field jsonSchemaField +} + +func (s additionalPropertiesSchema) MarshalJSON() ([]byte, error) { + return json.Marshal(s.field) +} + +type additionalPropertiesBool struct { + value bool +} + +func (s additionalPropertiesBool) MarshalJSON() ([]byte, error) { + return json.Marshal(s.value) +} + +type jsonSchemaField struct { + Description string `json:"description"` + Type string `json:"type,omitempty"` + Enum []string `json:"enum,omitempty"` + Properties map[string]jsonSchemaField `json:"properties,omitempty"` + AdditionalProperties json.Marshaler `json:"additionalProperties,omitempty"` + Items *jsonSchemaField `json:"items,omitempty"` +} + +type jsonSchema struct { + ID string `json:"-"` + Title string `json:"title"` + Description string `json:"description"` + Properties map[string]jsonSchemaField `json:"properties"` + AdditionalProperties bool `json:"additionalProperties"` + Type string `json:"type"` +} + +func generateJSONSchema(spec *specification.Specification) []jsonSchema { + g := &jSONSchemaGenerator{ + typeNameToEnum: map[string]specification.Enum{}, + typeNameToStruct: map[string]specification.Struct{}, + } + + for _, enum := range spec.Enums { + g.typeNameToEnum[enum.Name] = enum + } + for _, s := range spec.Structs { + g.typeNameToStruct[s.Name] = s + } + + return g.generate(spec) +} + +type jSONSchemaGenerator struct { + typeNameToEnum map[string]specification.Enum + typeNameToStruct map[string]specification.Struct +} + +func (g *jSONSchemaGenerator) generate(spec *specification.Specification) []jsonSchema { + schemas := make([]jsonSchema, len(spec.Structs)) + for i, s := range spec.Structs { + schemas[i] = g.generateStructSchema(s) + } + return schemas +} + +func (g *jSONSchemaGenerator) generateStructSchema(s specification.Struct) jsonSchema { + schema := jsonSchema{ + ID: s.Name, + Description: strings.TrimSpace(s.DescriptionInfo.DocString), + AdditionalProperties: false, + Type: "object", + } + + schema.Properties = g.generateProperties(s.Fields, map[string]string{}, "#") + + return schema +} + +func (g *jSONSchemaGenerator) generateField(field specification.Field, visited map[string]string, path string) jsonSchemaField { + schema := jsonSchemaField{ + Description: strings.TrimSpace(field.DescriptionInfo.DocString), + } + + schema.Type = getSchemaType(field.TypeSignature) + if field.TypeSignature.Type() == specification.TypeSignatureTypeEnum { + schema.Enum = g.getEnumType(field.TypeSignature) + } + + currentPath := path + if len(field.Name) > 0 { + currentPath += "/" + field.Name + } + + // We can only have references to struct types, not primitives + switch schema.Type { + case "array", "object": + visited[field.TypeSignature.Signature()] = currentPath + } + + switch { + case field.TypeSignature.Type() == specification.TypeSignatureTypeMap: + aProps := g.generateMapAdditionalProperties(field, visited, currentPath) + schema.AdditionalProperties = additionalPropertiesSchema{field: aProps} + case field.TypeSignature.Type() == specification.TypeSignatureTypeIterable: + items := g.generateArrayItems(field, visited, currentPath) + schema.Items = &items + case schema.Type == "object": + props, found := g.generateStructProperties(field, visited, currentPath) + if found { + schema.AdditionalProperties = additionalPropertiesBool{value: false} + schema.Properties = props + } else { + // When we have a struct but the definition cannot not be found, we go ahead + // and allow all additional properties since it may still be more useful than + // being completely unusable. + schema.AdditionalProperties = additionalPropertiesBool{value: true} + } + } + + return schema +} + +func (g *jSONSchemaGenerator) generateProperties(fields []specification.Field, visited map[string]string, path string) map[string]jsonSchemaField { + properties := make(map[string]jsonSchemaField) + + for _, field := range fields { + switch field.Location { + case specification.FieldLocationBody, specification.FieldLocationUnspecified: + properties[field.Name] = g.generateField(field, visited, path+"/properties") + } + } + + return properties +} + +func (g *jSONSchemaGenerator) generateMapAdditionalProperties(field specification.Field, visited map[string]string, path string) jsonSchemaField { + type mt interface { + KeyType() specification.TypeSignature + ValueType() specification.TypeSignature + } + + valueType := field.TypeSignature.(mt).ValueType() + valueFieldInfo := specification.Field{ + Location: specification.FieldLocationBody, + TypeSignature: valueType, + } + + return g.generateField(valueFieldInfo, visited, path+"/additionalProperties") +} + +func (g *jSONSchemaGenerator) generateArrayItems(field specification.Field, visited map[string]string, path string) jsonSchemaField { + type at interface { + ItemType() specification.TypeSignature + } + + itemType := field.TypeSignature.(at).ItemType() + itemField := specification.Field{ + Location: specification.FieldLocationBody, + TypeSignature: itemType, + } + + return g.generateField(itemField, visited, path+"/items") +} + +func (g *jSONSchemaGenerator) generateStructProperties( + field specification.Field, visited map[string]string, path string, +) (map[string]jsonSchemaField, bool) { + if s, ok := g.typeNameToStruct[field.TypeSignature.Signature()]; ok { + return g.generateProperties(s.Fields, visited, path), true + } + return map[string]jsonSchemaField{}, false +} + +func (g *jSONSchemaGenerator) getEnumType(t specification.TypeSignature) []string { + var res []string + + if e, ok := g.typeNameToEnum[t.Signature()]; ok { + for _, value := range e.Values { + res = append(res, value.Name) + } + } + + return res +} + +func getSchemaType(t specification.TypeSignature) string { + switch t.Type() { + case specification.TypeSignatureTypeEnum: + return "string" + case specification.TypeSignatureTypeIterable: + return "array" + case specification.TypeSignatureTypeMap: + return "object" + case specification.TypeSignatureTypeBase: + return getBaseType(t.Signature()) + default: + return "object" + } +} + +func getBaseType(signature string) string { + switch signature { + case "bool", "boolean": + return "boolean" + case "short", "number", "float", "double": + return "number" + case "i", "i8", "i16", "i32", "i64", "integer", "int", + "l32", "l64", "long", "long32", "long64", "int32", "int64", + "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", + "sfixed32", "sfixed64": + return "integer" + case "binary", "byte", "bytes", "string": + return "string" + default: + return "object" + } +} diff --git a/frontend/api/build/main.go b/frontend/api/build/main.go index fb84111..7e8bbb8 100644 --- a/frontend/api/build/main.go +++ b/frontend/api/build/main.go @@ -4,13 +4,86 @@ package main import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "github.com/curioswitch/go-build" "github.com/curioswitch/go-curiostack/tasks" + protodocs "github.com/curioswitch/go-docs-handler/plugins/proto" + "github.com/goyek/goyek/v2" "github.com/goyek/x/boot" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" ) func main() { tasks.DefineAPI() + + var generateProto *goyek.DefinedTask + for _, t := range goyek.Tasks() { + if t.Name() == "generate-proto" { + generateProto = t + break + } + } + + build.RegisterGenerateTask(goyek.Define(goyek.Task{ + Name: "generate-jsonschema", + Deps: []*goyek.DefinedTask{generateProto}, + Action: func(a *goyek.A) { + dspb, err := os.ReadFile(filepath.Join("descriptors", "descriptorset.pb")) + if err != nil { + a.Fatalf("reading descriptor set: %v", err) + } + + a.Logf("dspb length: %d", len(dspb)) + + var ds descriptorpb.FileDescriptorSet + if err := proto.Unmarshal(dspb, &ds); err != nil { + a.Fatalf("unmarshalling descriptor set: %v", err) + } + files, err := protodesc.NewFiles(&ds) + if err != nil { + a.Fatalf("creating file descriptors: %v", err) + } + files.RangeFiles(func(fd protoreflect.FileDescriptor) bool { + if strings.HasPrefix(string(fd.Package()), "google.protobuf") { + return true + } + _ = protoregistry.GlobalFiles.RegisterFile(fd) + return true + }) + + spec, err := protodocs.NewPlugin("frontendapi.FrontendService", protodocs.WithSerializedDescriptors(dspb)).GenerateSpecification() + if err != nil { + a.Fatalf("creating plugin: %v", err) + } + + schemas := generateJSONSchema(spec) + schemasMap := map[string]jsonSchema{} + for _, s := range schemas { + schemasMap[s.ID] = s + } + b, err := json.MarshalIndent(schemasMap, "", " ") + if err != nil { + a.Fatalf("marshaling jsonschema: %v", err) + } + + if err := os.MkdirAll("jsonschema", 0o755); err != nil { //nolint:gosec // common for build artifacts + a.Fatalf("creating jsonschema dir: %v", err) + } + + if err := os.WriteFile(filepath.Join("jsonschema", "jsonschema.json"), b, 0o644); err != nil { //nolint:gosec // common for build artifacts + a.Fatalf("writing jsonschema.json: %v", err) + } + }, + })) + build.DefineTasks() boot.Main() } diff --git a/frontend/api/jsonschema/jsonschema.json b/frontend/api/jsonschema/jsonschema.json new file mode 100644 index 0000000..dbc07d0 --- /dev/null +++ b/frontend/api/jsonschema/jsonschema.json @@ -0,0 +1,1246 @@ +{ + "frontendapi.AddBookmarkRequest": { + "title": "", + "description": "A request for FrontendService.AddBookmark.", + "properties": { + "recipe_id": { + "description": "The ID of the recipe to add a bookmark for.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.AddBookmarkResponse": { + "title": "", + "description": "A response for FrontendService.AddBookmark.", + "properties": {}, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.AddRecipeRequest": { + "title": "", + "description": "", + "properties": { + "additional_ingredients": { + "description": "Additional ingredients grouped into sections.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "ingredients": { + "description": "The ingredients in the section.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the ingredient section.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "description": { + "description": "The description of the recipe.", + "type": "string" + }, + "ingredients": { + "description": "The main ingredients of the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "language": { + "description": "The language of the recipe.", + "type": "string", + "enum": [ + "LANGUAGE_UNSPECIFIED", + "LANGUAGE_ENGLISH", + "LANGUAGE_JAPANESE" + ] + }, + "main_image_data_url": { + "description": "The main image of the recipe, as a data URL.", + "type": "string" + }, + "serving_size": { + "description": "The serving size of the recipe as free-form text.", + "type": "string" + }, + "steps": { + "description": "The steps to prepare the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_data_url": { + "description": "An image for the step, as a data URL.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.AddRecipeRequest.AddRecipeStep": { + "title": "", + "description": "", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_data_url": { + "description": "An image for the step, as a data URL.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.AddRecipeResponse": { + "title": "", + "description": "", + "properties": { + "recipe_id": { + "description": "The ID of the newly added recipe.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GeneratePlanRequest": { + "title": "", + "description": "A request for FrontendService.GeneratePlan.", + "properties": { + "genres": { + "description": "Genres to prioritize during planning.", + "type": "array", + "items": { + "description": "", + "type": "string", + "enum": [ + "RECIPE_GENRE_UNSPECIFIED", + "RECIPE_GENRE_JAPANESE", + "RECIPE_GENRE_CHINESE", + "RECIPE_GENRE_WESTERN", + "RECIPE_GENRE_KOREAN", + "RECIPE_GENRE_ITALIAN", + "RECIPE_GENRE_ETHNIC" + ] + } + }, + "ingredients": { + "description": "Ingredients to include in the plan.", + "type": "array", + "items": { + "description": "", + "type": "string" + } + }, + "num_days": { + "description": "The number of days to plan for.", + "type": "integer" + }, + "recipe_ids": { + "description": "Recipe IDs to use as main dishes.", + "type": "array", + "items": { + "description": "", + "type": "string" + } + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GeneratePlanResponse": { + "title": "", + "description": "A response for FrontendService.GeneratePlan.", + "properties": {}, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GenerateRecipeRequest": { + "title": "", + "description": "A request for FrontendService.GenerateRecipe.", + "properties": { + "prompt": { + "description": "The prompt to generate a recipe for.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GenerateRecipeResponse": { + "title": "", + "description": "A response for FrontendService.GenerateRecipe.", + "properties": { + "add_recipe_request": { + "description": "A request to add a recipe, prepopulated with generated content.", + "type": "object", + "properties": { + "additional_ingredients": { + "description": "Additional ingredients grouped into sections.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "ingredients": { + "description": "The ingredients in the section.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the ingredient section.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "description": { + "description": "The description of the recipe.", + "type": "string" + }, + "ingredients": { + "description": "The main ingredients of the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "language": { + "description": "The language of the recipe.", + "type": "string", + "enum": [ + "LANGUAGE_UNSPECIFIED", + "LANGUAGE_ENGLISH", + "LANGUAGE_JAPANESE" + ] + }, + "main_image_data_url": { + "description": "The main image of the recipe, as a data URL.", + "type": "string" + }, + "serving_size": { + "description": "The serving size of the recipe as free-form text.", + "type": "string" + }, + "steps": { + "description": "The steps to prepare the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_data_url": { + "description": "An image for the step, as a data URL.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GetPlanRequest": { + "title": "", + "description": "A request for FrontendService.GetPlan.", + "properties": { + "date": { + "description": "The date of the plan. The timestamp will begin on the date in UTC.", + "type": "object", + "properties": { + "nanos": { + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive.", + "type": "integer" + }, + "seconds": { + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive.", + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GetPlanResponse": { + "title": "", + "description": "A response for FrontendService.GetPlan.", + "properties": { + "plan": { + "description": "", + "type": "object", + "properties": { + "date": { + "description": "The date of the plan. The timestamp will begin on the date in UTC.", + "type": "object", + "properties": { + "nanos": { + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive.", + "type": "integer" + }, + "seconds": { + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive.", + "type": "integer" + } + }, + "additionalProperties": false + }, + "notes": { + "description": "A list of notes to help cook the plan.", + "type": "array", + "items": { + "description": "", + "type": "string" + } + }, + "recipes": { + "description": "The recipes for the plan.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "id": { + "description": "The ID of the recipe.", + "type": "string" + }, + "image_url": { + "description": "The image URL of the recipe.", + "type": "string" + }, + "summary": { + "description": "The summary of the recipe.", + "type": "string" + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "step_groups": { + "description": "The step groups for the plan.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "label": { + "description": "The label of the group.", + "type": "string" + }, + "note": { + "description": "Useful note for the group.", + "type": "string" + }, + "steps": { + "description": "The actual steps.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_url": { + "description": "An image for the step.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GetPlansRequest": { + "title": "", + "description": "", + "properties": {}, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GetPlansResponse": { + "title": "", + "description": "", + "properties": { + "plans": { + "description": "The users current plans.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "date": { + "description": "The date of the plan. The timestamp will begin on the date in UTC.", + "type": "object", + "properties": { + "nanos": { + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive.", + "type": "integer" + }, + "seconds": { + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive.", + "type": "integer" + } + }, + "additionalProperties": false + }, + "recipes": { + "description": "The recipes for the plan.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "id": { + "description": "The ID of the recipe.", + "type": "string" + }, + "image_url": { + "description": "The image URL of the recipe.", + "type": "string" + }, + "summary": { + "description": "The summary of the recipe.", + "type": "string" + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GetRecipeRequest": { + "title": "", + "description": "A request for FrontendService.GetRecipe.", + "properties": { + "recipe_id": { + "description": "The ID of the recipe to get.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.GetRecipeResponse": { + "title": "", + "description": "A response for FrontendService.GetRecipe.", + "properties": { + "is_bookmarked": { + "description": "Whether the recipe is bookmarked by the user.", + "type": "boolean" + }, + "llm_prompt": { + "description": "The LLM prompt used to interact with the recipe.\n Only returned for users with debugging access.", + "type": "string" + }, + "recipe": { + "description": "The requested recipe.", + "type": "object", + "properties": { + "additional_ingredients": { + "description": "Additional ingredients grouped into sections.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "ingredients": { + "description": "The ingredients in the section.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the ingredient section.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "description": { + "description": "The description of the recipe.", + "type": "string" + }, + "id": { + "description": "The unique identifier of the recipe within cookchat.", + "type": "string" + }, + "image_url": { + "description": "The URL for the main image of the recipe.", + "type": "string" + }, + "ingredients": { + "description": "The main ingredients of the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "language": { + "description": "The language of the recipe.", + "type": "string", + "enum": [ + "LANGUAGE_UNSPECIFIED", + "LANGUAGE_ENGLISH", + "LANGUAGE_JAPANESE" + ] + }, + "notes": { + "description": "Additional notes or comments about the recipe.", + "type": "string" + }, + "serving_size": { + "description": "The serving size of the recipe as free-form text.", + "type": "string" + }, + "source": { + "description": "The source of the recipe.", + "type": "string", + "enum": [ + "RECIPE_SOURCE_UNSPECIFIED", + "RECIPE_SOURCE_COOKPAD" + ] + }, + "steps": { + "description": "The steps to prepare the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_url": { + "description": "An image for the step.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.IngredientSection": { + "title": "", + "description": "A section of ingredients in a recipe.", + "properties": { + "ingredients": { + "description": "The ingredients in the section.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the ingredient section.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.ListRecipesRequest": { + "title": "", + "description": "A request for FrontendService.ListRecipes.", + "properties": { + "bookmarks": { + "description": "Whether to only return bookmarked recipes.", + "type": "boolean" + }, + "pagination": { + "description": "The pagination token for the next page of recipes.\n If unset, the first page is returned.", + "type": "object", + "properties": { + "last_id": { + "description": "", + "type": "string" + }, + "last_timestamp_nanos": { + "description": "", + "type": "integer" + } + }, + "additionalProperties": false + }, + "query": { + "description": "A text query to filter by.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.ListRecipesResponse": { + "title": "", + "description": "A response for FrontendService.ListRecipes.", + "properties": { + "pagination": { + "description": "The pagination token for the next page of recipes.", + "type": "object", + "properties": { + "last_id": { + "description": "", + "type": "string" + }, + "last_timestamp_nanos": { + "description": "", + "type": "integer" + } + }, + "additionalProperties": false + }, + "recipes": { + "description": "The recipes.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "id": { + "description": "The ID of the recipe.", + "type": "string" + }, + "image_url": { + "description": "The image URL of the recipe.", + "type": "string" + }, + "summary": { + "description": "The summary of the recipe.", + "type": "string" + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.Pagination": { + "title": "", + "description": "A token returned to retrieve a subsequent page of items.", + "properties": { + "last_id": { + "description": "", + "type": "string" + }, + "last_timestamp_nanos": { + "description": "", + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.Plan": { + "title": "", + "description": "A cooking plan.", + "properties": { + "date": { + "description": "The date of the plan. The timestamp will begin on the date in UTC.", + "type": "object", + "properties": { + "nanos": { + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive.", + "type": "integer" + }, + "seconds": { + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive.", + "type": "integer" + } + }, + "additionalProperties": false + }, + "notes": { + "description": "A list of notes to help cook the plan.", + "type": "array", + "items": { + "description": "", + "type": "string" + } + }, + "recipes": { + "description": "The recipes for the plan.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "id": { + "description": "The ID of the recipe.", + "type": "string" + }, + "image_url": { + "description": "The image URL of the recipe.", + "type": "string" + }, + "summary": { + "description": "The summary of the recipe.", + "type": "string" + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "step_groups": { + "description": "The step groups for the plan.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "label": { + "description": "The label of the group.", + "type": "string" + }, + "note": { + "description": "Useful note for the group.", + "type": "string" + }, + "steps": { + "description": "The actual steps.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_url": { + "description": "An image for the step.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.PlanSnippet": { + "title": "", + "description": "A snippet of a plan, without executiond details.", + "properties": { + "date": { + "description": "The date of the plan. The timestamp will begin on the date in UTC.", + "type": "object", + "properties": { + "nanos": { + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive.", + "type": "integer" + }, + "seconds": { + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive.", + "type": "integer" + } + }, + "additionalProperties": false + }, + "recipes": { + "description": "The recipes for the plan.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "id": { + "description": "The ID of the recipe.", + "type": "string" + }, + "image_url": { + "description": "The image URL of the recipe.", + "type": "string" + }, + "summary": { + "description": "The summary of the recipe.", + "type": "string" + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.Recipe": { + "title": "", + "description": "Full details of a recipe.", + "properties": { + "additional_ingredients": { + "description": "Additional ingredients grouped into sections.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "ingredients": { + "description": "The ingredients in the section.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the ingredient section.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "description": { + "description": "The description of the recipe.", + "type": "string" + }, + "id": { + "description": "The unique identifier of the recipe within cookchat.", + "type": "string" + }, + "image_url": { + "description": "The URL for the main image of the recipe.", + "type": "string" + }, + "ingredients": { + "description": "The main ingredients of the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "language": { + "description": "The language of the recipe.", + "type": "string", + "enum": [ + "LANGUAGE_UNSPECIFIED", + "LANGUAGE_ENGLISH", + "LANGUAGE_JAPANESE" + ] + }, + "notes": { + "description": "Additional notes or comments about the recipe.", + "type": "string" + }, + "serving_size": { + "description": "The serving size of the recipe as free-form text.", + "type": "string" + }, + "source": { + "description": "The source of the recipe.", + "type": "string", + "enum": [ + "RECIPE_SOURCE_UNSPECIFIED", + "RECIPE_SOURCE_COOKPAD" + ] + }, + "steps": { + "description": "The steps to prepare the recipe.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_url": { + "description": "An image for the step.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.RecipeIngredient": { + "title": "", + "description": "An ingredient in a recipe.", + "properties": { + "name": { + "description": "The name of the ingredient.", + "type": "string" + }, + "quantity": { + "description": "The quantity of the ingredient as free-form text.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.RecipeSnippet": { + "title": "", + "description": "A snippet of a recipe for list views.", + "properties": { + "id": { + "description": "The ID of the recipe.", + "type": "string" + }, + "image_url": { + "description": "The image URL of the recipe.", + "type": "string" + }, + "summary": { + "description": "The summary of the recipe.", + "type": "string" + }, + "title": { + "description": "The title of the recipe.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.RecipeStep": { + "title": "", + "description": "A step in a recipe.", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_url": { + "description": "An image for the step.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.RemoveBookmarkRequest": { + "title": "", + "description": "A request for FrontendService.RemoveBookmark.", + "properties": { + "recipe_id": { + "description": "The ID of the recipe to remove a bookmark for.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.RemoveBookmarkResponse": { + "title": "", + "description": "A response for FrontendService.RemoveBookmark.", + "properties": {}, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.StartChatRequest": { + "title": "", + "description": "The recipe to chat about.", + "properties": { + "llm_prompt": { + "description": "The prompt to use with the chat.\n Only recognized for users with debugging access.", + "type": "string" + }, + "model_provider": { + "description": "The model provider to use for the chat.", + "type": "string", + "enum": [ + "MODEL_PROVIDER_UNSPECIFIED", + "MODEL_PROVIDER_GOOGLE_GENAI", + "MODEL_PROVIDER_OPENAI" + ] + }, + "recipe_id": { + "description": "The ID of a cookchat recipe.", + "type": "string" + }, + "recipe_text": { + "description": "Free-form text of the recipe.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.StartChatResponse": { + "title": "", + "description": "A response to start a chat session.", + "properties": { + "chat_api_key": { + "description": "The ephemeral API key to use to start the chat.", + "type": "string" + }, + "chat_instructions": { + "description": "Instructions for the chat session.", + "type": "string" + }, + "chat_model": { + "description": "The chat model to use for the session.", + "type": "string" + }, + "start_message": { + "description": "The message to send to start the voice chat.", + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, + "frontendapi.StepGroup": { + "title": "", + "description": "A group of steps within a plan that can be executed together.", + "properties": { + "label": { + "description": "The label of the group.", + "type": "string" + }, + "note": { + "description": "Useful note for the group.", + "type": "string" + }, + "steps": { + "description": "The actual steps.", + "type": "array", + "items": { + "description": "", + "type": "object", + "properties": { + "description": { + "description": "The description of the step.", + "type": "string" + }, + "image_url": { + "description": "An image for the step.", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "type": "object" + }, + "google.protobuf.Timestamp": { + "title": "", + "description": "A Timestamp represents a point in time independent of any time zone or local\n calendar, encoded as a count of seconds and fractions of seconds at\n nanosecond resolution. The count is relative to an epoch at UTC midnight on\n January 1, 1970, in the proleptic Gregorian calendar which extends the\n Gregorian calendar backwards to year one.\n\n All minutes are 60 seconds long. Leap seconds are \"smeared\" so that no leap\n second table is needed for interpretation, using a [24-hour linear\n smear](https://developers.google.com/time/smear).\n\n The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By\n restricting to that range, we ensure that we can convert to and from [RFC\n 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.\n\n # Examples\n\n Example 1: Compute Timestamp from POSIX `time()`.\n\n Timestamp timestamp;\n timestamp.set_seconds(time(NULL));\n timestamp.set_nanos(0);\n\n Example 2: Compute Timestamp from POSIX `gettimeofday()`.\n\n struct timeval tv;\n gettimeofday(\u0026tv, NULL);\n\n Timestamp timestamp;\n timestamp.set_seconds(tv.tv_sec);\n timestamp.set_nanos(tv.tv_usec * 1000);\n\n Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.\n\n FILETIME ft;\n GetSystemTimeAsFileTime(\u0026ft);\n UINT64 ticks = (((UINT64)ft.dwHighDateTime) \u003c\u003c 32) | ft.dwLowDateTime;\n\n // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z\n // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.\n Timestamp timestamp;\n timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));\n timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));\n\n Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.\n\n long millis = System.currentTimeMillis();\n\n Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)\n .setNanos((int) ((millis % 1000) * 1000000)).build();\n\n Example 5: Compute Timestamp from Java `Instant.now()`.\n\n Instant now = Instant.now();\n\n Timestamp timestamp =\n Timestamp.newBuilder().setSeconds(now.getEpochSecond())\n .setNanos(now.getNano()).build();\n\n Example 6: Compute Timestamp from current time in Python.\n\n timestamp = Timestamp()\n timestamp.GetCurrentTime()\n\n # JSON Mapping\n\n In JSON format, the Timestamp type is encoded as a string in the\n [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the\n format is \"{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z\"\n where {year} is always expressed using four digits while {month}, {day},\n {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional\n seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),\n are optional. The \"Z\" suffix indicates the timezone (\"UTC\"); the timezone\n is required. A proto3 JSON serializer should always use UTC (as indicated by\n \"Z\") when printing the Timestamp type and a proto3 JSON parser should be\n able to accept both UTC and other timezones (as indicated by an offset).\n\n For example, \"2017-01-15T01:30:15.01Z\" encodes 15.01 seconds past\n 01:30 UTC on January 15, 2017.\n\n In JavaScript, one can convert a Date object to this format using the\n standard\n [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)\n method. In Python, a standard `datetime.datetime` object can be converted\n to this format using\n [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with\n the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use\n the Joda Time's [`ISODateTimeFormat.dateTime()`](\n http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()\n ) to obtain a formatter capable of generating timestamps in this format.", + "properties": { + "nanos": { + "description": "Non-negative fractions of a second at nanosecond resolution. Negative\n second values with fractions must still have non-negative nanos values\n that count forward in time. Must be from 0 to 999,999,999\n inclusive.", + "type": "integer" + }, + "seconds": { + "description": "Represents seconds of UTC time since Unix epoch\n 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to\n 9999-12-31T23:59:59Z inclusive.", + "type": "integer" + } + }, + "additionalProperties": false, + "type": "object" + } +} \ No newline at end of file diff --git a/frontend/server/go.mod b/frontend/server/go.mod index bff9d38..4d66363 100644 --- a/frontend/server/go.mod +++ b/frontend/server/go.mod @@ -16,7 +16,7 @@ require ( github.com/openai/openai-go/v2 v2.7.0 golang.org/x/sync v0.17.0 google.golang.org/api v0.249.0 - google.golang.org/genai v1.24.0 + google.golang.org/genai v1.25.0 google.golang.org/protobuf v1.36.9 ) diff --git a/frontend/server/go.sum b/frontend/server/go.sum index f914b07..8849107 100644 --- a/frontend/server/go.sum +++ b/frontend/server/go.sum @@ -228,8 +228,8 @@ google.golang.org/api v0.249.0 h1:0VrsWAKzIZi058aeq+I86uIXbNhm9GxSHpbmZ92a38w= google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ= google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw= google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI= -google.golang.org/genai v1.24.0 h1:j5lt+Qr7W0+OBxwwEPe4DQ+ygEqpvZuSBvYoHIuUjhg= -google.golang.org/genai v1.24.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M= +google.golang.org/genai v1.25.0 h1:Cpyh2nmEoOS1eM3mT9XKuA/qWTEDoktfP2gsN3EduPE= +google.golang.org/genai v1.25.0/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs= google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=