Let's Build a Customer Support AI Copilot: An Event-Driven Agent with LangGraph, Go, pgvector & Redis Streams [Part 2]

Karan Kashyap
June 23, 2026
Part 2: The Go GraphQL API — Resolvers, Pub/Sub Bridge & Subscription Fan-Out
Package Layout
1services/api/2├── cmd/server/main.go # entrypoint: wires everything, starts HTTP3├── internal/4│ ├── config/config.go # env validation at startup5│ ├── repo/6│ │ ├── store.go # Store interface (contract for testability)7│ │ ├── models.go # DB ↔ Go type mapping8│ │ └── postgres.go # pgx pool implementation9│ ├── service/service.go # business logic: validate + persist + publish10│ ├── resolvers/11│ │ ├── resolver.go # root resolver wiring12│ │ ├── schema.resolvers.go # generated + implemented resolver methods13│ │ ├── mapping.go # repo → gql type converters14│ │ └── errors.go # boundary error handling15│ ├── pubsub/16│ │ ├── events.go # typed event structs + constructors17│ │ ├── redis.go # XADD publish + XREAD subscription bridge18│ │ └── hub.go # in-process subscription fan-out19│ └── obs/tracing.go # OpenTelemetry setup20└── gqlgen.yml # codegen config
Step 1 — Configuration: Fail Fast at Startup
Nothing is worse than a service that silently boots with bad config and fails on the first request. config.Load() reads the environment, collects all missing/invalid values, and returns them together.
1// services/api/internal/config/config.go23package config45type Config struct {6 DatabaseURL string7 PGPoolMaxConns int3289 RedisURL string10 StreamMessages string // API publishes message.created here11 StreamDrafts string // API consumes draft.* here for subscriptions1213 APIHost string14 APIPort int15 CORSAllowedOrigins []string16}1718func Load() (*Config, error) {19 var problems []string20 require := func(key string) string {21 v := strings.TrimSpace(os.Getenv(key))22 if v == "" {23 problems = append(problems, "missing required env "+key)24 }25 return v26 }2728 c := &Config{29 DatabaseURL: require("DATABASE_URL"),30 RedisURL: require("REDIS_URL"),31 StreamMessages: getDefault("STREAM_MESSAGES", "messages"),32 StreamDrafts: getDefault("STREAM_DRAFTS", "drafts"),33 APIHost: getDefault("API_HOST", "0.0.0.0"),34 CORSAllowedOrigins: getList("CORS_ALLOWED_ORIGINS",35 []string{"http://localhost:3000"}),36 }37 c.PGPoolMaxConns = int32(getInt("PG_POOL_MAX_CONNS", 20, &problems))38 c.APIPort = getInt("API_PORT", 8080, &problems)3940 if len(problems) > 0 {41 return nil, fmt.Errorf("invalid config:\n - %s",42 strings.Join(problems, "\n - "))43 }44 return c, nil45}
require() appends to problems instead of returning early — one boot attempt surfaces every misconfiguration at once.
Step 2 — The Repository Interface
The service layer depends on a Store interface, not on *Postgres directly. This means tests get a zero-dependency fake, not a real database.
1// services/api/internal/repo/store.go23package repo45type Store interface {6 // Creates a CUSTOMER message; auto-creates the conversation if ConversationID is nil.7 CreateMessage(ctx context.Context, in CreateMessageInput) (Message, error)89 GetConversation(ctx context.Context, id string) (*Conversation, error)10 GetMessage(ctx context.Context, id string) (*Message, error)11 GetDraft(ctx context.Context, id string) (*Draft, error)1213 // Priority queue: urgency-first, with negative-sentiment tie-breaking.14 // Returns keyset cursor for the next page.15 ListDrafts(ctx context.Context, status string, limit int, cursor string) (16 items []Draft, nextCursor string, err error)1718 UpdateDraftStatus(ctx context.Context, id, status string, edited *string) (*Draft, error)19 AppendAudit(ctx context.Context, e AuditEntry) error20 ListEvalRuns(ctx context.Context, limit int) ([]EvalRun, error)21 DashboardStats(ctx context.Context) (DashboardStats, error)2223 Close()24}
Step 3 — The pgx Implementation
NewPostgres opens a sized connection pool and pings before returning — startup fails loudly rather than on the first query.
1// services/api/internal/repo/postgres.go23func NewPostgres(ctx context.Context, dsn string, maxConns int32) (*Postgres, error) {4 cfg, err := pgxpool.ParseConfig(dsn)5 if err != nil {6 return nil, fmt.Errorf("parse dsn: %w", err)7 }8 cfg.MaxConns = maxConns9 pool, err := pgxpool.NewWithConfig(ctx, cfg)10 if err != nil {11 return nil, fmt.Errorf("open pool: %w", err)12 }13 if err := pool.Ping(ctx); err != nil {14 pool.Close()15 return nil, fmt.Errorf("ping: %w", err)16 }17 return &Postgres{pool: pool}, nil18}
CreateMessage runs inside a transaction — either both the conversation row (if new) and the message row land, or neither does.
1func (p *Postgres) CreateMessage(ctx context.Context, in CreateMessageInput) (Message, error) {2 tx, err := p.pool.Begin(ctx)3 if err != nil {4 return Message{}, fmt.Errorf("begin: %w", err)5 }6 defer tx.Rollback(ctx)78 convID := ""9 if in.ConversationID != nil && *in.ConversationID != "" {10 // Verify the conversation exists; fail clearly if not.11 if err := tx.QueryRow(ctx,12 `SELECT id FROM conversations WHERE id = $1`, *in.ConversationID,13 ).Scan(&convID); err != nil {14 if errors.Is(err, pgx.ErrNoRows) { return Message{}, ErrNotFound }15 return Message{}, fmt.Errorf("lookup conversation: %w", err)16 }17 } else {18 ref := "anonymous"19 if in.CustomerRef != nil && *in.CustomerRef != "" { ref = *in.CustomerRef }20 if err := tx.QueryRow(ctx,21 `INSERT INTO conversations (customer_ref, status)22 VALUES ($1, 'OPEN') RETURNING id`, ref,23 ).Scan(&convID); err != nil {24 return Message{}, fmt.Errorf("insert conversation: %w", err)25 }26 }2728 var m Message29 m.ConversationID = convID30 if err := tx.QueryRow(ctx,31 `INSERT INTO messages (conversation_id, role, body)32 VALUES ($1, 'CUSTOMER', $2) RETURNING id, created_at`,33 convID, in.Body,34 ).Scan(&m.ID, &m.CreatedAt); err != nil {35 return Message{}, fmt.Errorf("insert message: %w", err)36 }37 if err := tx.Commit(ctx); err != nil {38 return Message{}, fmt.Errorf("commit: %w", err)39 }40 return m, nil41}
Priority Queue with Keyset Pagination
The queue surfaces high-urgency, negative-sentiment tickets first. The SQL ranking expression and its Go mirror stay in lockstep — same formula, two representations tested against each other.
1// services/api/internal/repo/postgres.go23// draftRank is a SQL expression: urgency dominates, sentiment breaks ties.4// HIGH always outranks NORMAL regardless of sentiment.5const draftRank = `(CASE urgency WHEN 'HIGH' THEN 6 WHEN 'NORMAL' THEN 4 ELSE 2 END` +6 ` + CASE sentiment WHEN 'NEGATIVE' THEN 1 ELSE 0 END)`78// goRank mirrors draftRank for building the keyset cursor in Go.9func goRank(d Draft) int {10 rank := 211 switch d.Urgency {12 case "HIGH": rank = 613 case "NORMAL": rank = 414 }15 if d.Sentiment == "NEGATIVE" { rank++ }16 return rank17}
The cursor is a rank|timestamp composite string. On the next page request it becomes a WHERE (rank, created_at) < ($cursor_rank, $cursor_time) row-value comparison — no OFFSET, no count queries.
Step 4 — The Service Layer
service.Service is the only place business rules live. It validates input, calls the store, publishes events, and writes audit rows. Resolvers never touch the store directly.
1// services/api/internal/service/service.go23const maxBodyLen = 800045func (s *Service) IngestMessage(ctx context.Context, in repo.CreateMessageInput) (repo.Message, error) {6 ctx, span := obs.Tracer().Start(ctx, "ingest_message")7 defer span.End()89 body := strings.TrimSpace(in.Body)10 if body == "" {11 return repo.Message{}, fmt.Errorf("%w: body is required", ErrInvalidInput)12 }13 if len(body) > maxBodyLen {14 return repo.Message{}, fmt.Errorf("%w: body exceeds %d chars", ErrInvalidInput, maxBodyLen)15 }16 in.Body = body1718 // Persist before publish: the worker can always load the row it's told about.19 m, err := s.store.CreateMessage(ctx, in)20 if err != nil { return repo.Message{}, err }2122 span.SetAttributes(23 attribute.String("conversation.id", m.ConversationID),24 attribute.String("message.id", m.ID),25 )2627 // Carry the OTel trace into the event so the worker continues the same trace.28 tid := traceID(ctx)29 if sc := span.SpanContext(); sc.HasTraceID() { tid = sc.TraceID().String() }3031 ev := pubsub.NewMessageCreated(m.ID, m.ConversationID, m.Body, tid)32 if err := s.pub.PublishMessageCreated(ctx, ev); err != nil {33 // Message persisted but event lost — surface it so the caller can reprocess.34 return m, fmt.Errorf("publish message.created: %w", err)35 }36 return m, nil37}
ApproveReply calls transition(), which reads the draft before and after, writes the audit row, and returns the updated draft. Audit failure is logged but never rolls back the human action — an audit write error must not undo a completed approval.
1func (s *Service) transition(ctx context.Context,2 draftID, status, action string, edited *string,3) (*repo.Draft, error) {4 before, err := s.store.GetDraft(ctx, draftID)5 if err != nil { return nil, err }67 after, err := s.store.UpdateDraftStatus(ctx, draftID, status, edited)8 if err != nil { return nil, err }910 if auditErr := s.store.AppendAudit(ctx, repo.AuditEntry{11 DraftID: draftID, Actor: s.actor, Action: action,12 Before: map[string]any{"status": before.Status},13 After: map[string]any{"status": after.Status},14 }); auditErr != nil {15 // Best-effort: log it, don't undo the transition.16 s.log.WarnContext(ctx, "audit write failed",17 "draft_id", draftID, "action", action, "err", auditErr)18 }19 return after, nil20}
Step 5 — The gqlgen Resolvers
gqlgen generates the interface stubs. We implement them in schema.resolvers.go. Each resolver does exactly one thing: call the service, convert the result to a GQL type.
1// services/api/internal/resolvers/schema.resolvers.go23func (r *mutationResolver) IngestMessage(4 ctx context.Context, input gql.IngestInput,5) (*gql.Message, error) {6 m, err := r.svc.IngestMessage(ctx, repo.CreateMessageInput{7 ConversationID: input.ConversationID,8 CustomerRef: input.CustomerRef,9 Body: input.Body,10 })11 if err != nil {12 return nil, boundaryErr(ctx, r.log, "ingestMessage", err)13 }14 return toGQLMessage(&m), nil15}1617func (r *mutationResolver) ApproveReply(18 ctx context.Context, draftID string, edited *string,19) (*gql.Draft, error) {20 d, err := r.svc.ApproveReply(ctx, draftID, edited)21 if err != nil {22 return nil, boundaryErr(ctx, r.log, "approveReply", err)23 }24 return toGQLDraft(d), nil25}
The DraftUpdates subscription resolver is more interesting — it subscribes to the Hub, then in a goroutine it pulls each event, loads the full draft from the store, and forwards it to the channel the GQL runtime is reading.
1func (r *subscriptionResolver) DraftUpdates(2 ctx context.Context, conversationID *string,3) (<-chan *gql.Draft, error) {4 cid := ""5 if conversationID != nil { cid = *conversationID }67 events, unsub := r.hub.Subscribe(cid)8 out := make(chan *gql.Draft, 16)910 go func() {11 defer unsub()12 defer close(out)13 for {14 select {15 case <-ctx.Done():16 return17 case ev, ok := <-events:18 if !ok { return }19 d, err := r.svc.GetDraft(ctx, ev.DraftID)20 if err != nil {21 r.log.WarnContext(ctx, "draftUpdates: load draft failed",22 "draft_id", ev.DraftID, "err", err)23 continue24 }25 select {26 case out <- toGQLDraft(d):27 case <-ctx.Done():28 return29 }30 }31 }32 }()33 return out, nil34}
Loading the full draft on each subscription event means the UI always receives the same rich Draft shape as a query — no separate subscription-specific type needed.
Step 6 — The Redis Pub/Sub Bridge
The Bus connects the two streams.
Publish side — the API calls PublishMessageCreated after persisting a message:
1// services/api/internal/pubsub/redis.go23func (b *Bus) PublishMessageCreated(ctx context.Context, ev MessageCreated) error {4 payload, err := json.Marshal(ev)5 if err != nil { return fmt.Errorf("marshal event: %w", err) }67 return b.rdb.XAdd(ctx, &redis.XAddArgs{8 Stream: b.streamMessages,9 Values: map[string]any{10 "type": ev.Type,11 "event_id": ev.EventID,12 "payload": payload, // full JSON under "payload"13 },14 }).Err()15}
Consume side — a background goroutine reads new entries from the drafts stream and pushes them to the Hub:
1func (b *Bus) ConsumeDrafts(ctx context.Context) error {2 lastID := "$" // only new messages from the moment the API starts3 for {4 if ctx.Err() != nil { return ctx.Err() }56 res, err := b.rdb.XRead(ctx, &redis.XReadArgs{7 Streams: []string{b.streamDrafts, lastID},8 Block: 5 * time.Second,9 Count: 64,10 }).Result()11 if err != nil {12 if errors.Is(err, redis.Nil) { continue } // block timeout13 if ctx.Err() != nil { return ctx.Err() }14 return fmt.Errorf("xread %s: %w", b.streamDrafts, err)15 }16 for _, stream := range res {17 for _, msg := range stream.Messages {18 lastID = msg.ID19 if ev, ok := decodeDraftEvent(msg.Values); ok {20 b.hub.Publish(ev)21 }22 }23 }24 }25}
"$" as the initial ID means the API only picks up events published after it boots — it doesn't replay the full history on restart. The subscription is for live updates; historical state comes from queries.
Step 7 — The Subscription Hub
Hub is an in-process fan-out: one entry per open WebSocket subscription. It's intentionally simple — sync.RWMutex over a map, a buffered channel per subscriber.
1// services/api/internal/pubsub/hub.go23type Hub struct {4 mu sync.RWMutex5 next int6 subs map[int]*subscriber7}89type subscriber struct {10 conversationID string // "" = receive all11 ch chan DraftEvent12}1314func (h *Hub) Subscribe(conversationID string) (<-chan DraftEvent, func()) {15 h.mu.Lock()16 defer h.mu.Unlock()17 id := h.next18 h.next++19 s := &subscriber{conversationID: conversationID, ch: make(chan DraftEvent, 16)}20 h.subs[id] = s21 return s.ch, func() {22 h.mu.Lock()23 defer h.mu.Unlock()24 if existing, ok := h.subs[id]; ok {25 close(existing.ch)26 delete(h.subs, id)27 }28 }29}3031// Publish is non-blocking: full buffer = event dropped, not blocked.32func (h *Hub) Publish(e DraftEvent) {33 h.mu.RLock()34 defer h.mu.RUnlock()35 for _, s := range h.subs {36 if s.conversationID != "" && s.conversationID != e.ConversationID { continue }37 select {38 case s.ch <- e:39 default: // client too slow — drop and let them re-query40 }41 }42}
The default branch in Publish is critical. A slow client must not stall the Redis consumer loop that serves every other subscriber.
Step 8 — The Entrypoint
main.go wires everything in dependency order, then starts the HTTP server. Shutdown drains gracefully on SIGINT/SIGTERM.
1// services/api/cmd/server/main.go23func run(log *slog.Logger) error {4 cfg, err := config.Load()5 if err != nil { return err }67 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)8 defer stop()910 shutdownTracing, err := obs.Init(ctx, log)11 if err != nil { return err }12 defer shutdownTracing(...)1314 store, err := repo.NewPostgres(ctx, cfg.DatabaseURL, cfg.PGPoolMaxConns)15 if err != nil { return err }16 defer store.Close()1718 hub := pubsub.NewHub()19 bus, err := pubsub.NewBus(ctx, cfg.RedisURL, cfg.StreamMessages, cfg.StreamDrafts, hub)20 if err != nil { return err }21 defer bus.Close()2223 svc := service.New(store, bus, log)24 root := resolvers.New(svc, hub, log)2526 // Bridge drafts stream → subscriptions in background.27 go bus.ConsumeDrafts(ctx)2829 mux := http.NewServeMux()30 mux.Handle("/", playground.Handler("Resolver", "/query"))31 mux.Handle("/query", otelhttp.NewHandler(graphqlHandler(root, ...), "graphql"))32 mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {33 w.WriteHeader(http.StatusOK)34 _, _ = w.Write([]byte("ok"))35 })3637 srv := &http.Server{Addr: cfg.Addr(), Handler: withCORS(..., mux), ReadHeaderTimeout: 10 * time.Second}3839 // Wait for either a server error or a shutdown signal.40 select {41 case err := <-errCh:42 return err43 case <-ctx.Done():44 return srv.Shutdown(context.WithTimeout(context.Background(), 15*time.Second))45 }46}
otelhttp.NewHandler wraps the GraphQL endpoint so every request gets an OTel server span. The trace ID extracted here is the one the service layer injects into the message.created event — the same trace continues in the Python worker.
Step 9 — Observability
OTel is configured by OTEL_TRACES_EXPORTER. In development it defaults to console (stdout). In a full stack it ships OTLP to Jaeger.
1// services/api/internal/obs/tracing.go23func Init(ctx context.Context, log *slog.Logger) (func(context.Context) error, error) {4 mode := os.Getenv("OTEL_TRACES_EXPORTER")5 if mode == "" { mode = "console" }6 if mode == "none" {7 return func(context.Context) error { return nil }, nil8 }910 var exp sdktrace.SpanExporter11 switch mode {12 case "otlp": exp, _ = otlptracehttp.New(ctx) // OTEL_EXPORTER_OTLP_ENDPOINT13 case "console": exp, _ = stdouttrace.New(stdouttrace.WithoutTimestamps())14 }1516 tp := sdktrace.NewTracerProvider(17 sdktrace.WithBatcher(exp),18 sdktrace.WithResource(res),19 )20 otel.SetTracerProvider(tp)21 // W3C trace-context so trace propagates API → event bus → worker → LLM.22 otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(23 propagation.TraceContext{}, propagation.Baggage{},24 ))25 return tp.Shutdown, nil26}
Run Jaeger with:
1docker compose --profile observability up jaeger2# Then set in .env:3# OTEL_TRACES_EXPORTER=otlp4# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
The UI will show one trace spanning the HTTP mutation → service → event publish → worker consume → LLM generate → draft write.
The Tests
Service tests — fake Store + fake Publisher
1// services/api/internal/service/service_test.go23func TestIngestMessagePersistsAndPublishesOnce(t *testing.T) {4 store := newFakeStore()5 pub := &fakePub{}6 svc := New(store, pub, nil)78 m, err := svc.IngestMessage(context.Background(),9 repo.CreateMessageInput{Body: " my order is late "})10 if err != nil { t.Fatalf("IngestMessage: %v", err) }1112 // Exactly one publish: duplicate = worker processes twice; zero = stranded.13 if pub.count != 1 { t.Fatalf("want 1 event, got %d", pub.count) }1415 // Body trimmed before persist.16 if store.created[0].Body != "my order is late" {17 t.Fatalf("body not trimmed: %q", store.created[0].Body)18 }19}2021func TestIngestMessageRejectsBadInput(t *testing.T) {22 cases := map[string]string{23 "empty": " ",24 "too long": strings.Repeat("x", maxBodyLen+1),25 }26 for name, body := range cases {27 t.Run(name, func(t *testing.T) {28 svc := New(newFakeStore(), &fakePub{}, nil)29 _, err := svc.IngestMessage(context.Background(),30 repo.CreateMessageInput{Body: body})31 if !errors.Is(err, ErrInvalidInput) {32 t.Fatalf("expected ErrInvalidInput, got %v", err)33 }34 })35 }36}3738func TestApproveReplyTransitionsAndAudits(t *testing.T) {39 store := newFakeStore()40 store.drafts["d1"] = &repo.Draft{ID: "d1", Status: "SUGGESTED"}41 svc := New(store, &fakePub{}, nil)4243 edit := "Here is your refund status."44 d, err := svc.ApproveReply(context.Background(), "d1", &edit)45 if err != nil { t.Fatalf("ApproveReply: %v", err) }4647 if d.Status != "SENT" || d.Answer != edit {48 t.Fatalf("unexpected draft: %+v", d)49 }50 if store.audits != 1 { t.Fatalf("expected 1 audit row, got %d", store.audits) }51}
No Postgres, no Redis, no network. The fake implements Store and Publisher in ~50 lines.
Hub tests — concurrent fan-out correctness
1// services/api/internal/pubsub/hub_test.go23func TestHubDeliversToMatchingSubscribers(t *testing.T) {4 h := NewHub()5 all, unsubAll := h.Subscribe("") // wildcard6 convA, unsubA := h.Subscribe("conv-A") // scoped7 defer unsubAll(); defer unsubA()89 h.Publish(DraftEvent{DraftID: "d1", ConversationID: "conv-A"})1011 // Wildcard receives it.12 if e, ok := recvWithin(t, all, time.Second); !ok || e.DraftID != "d1" {13 t.Fatalf("wildcard missed event")14 }15 // conv-A scoped receives it.16 if e, ok := recvWithin(t, convA, time.Second); !ok || e.DraftID != "d1" {17 t.Fatalf("conv-A subscriber missed event")18 }1920 // conv-B event must NOT reach conv-A subscriber.21 h.Publish(DraftEvent{DraftID: "d2", ConversationID: "conv-B"})22 if _, ok := recvWithin(t, convA, 200*time.Millisecond); ok {23 t.Fatal("conv-A received conv-B event")24 }25}2627func TestHubPublishDoesNotBlockOnFullBuffer(t *testing.T) {28 h := NewHub()29 _, unsub := h.Subscribe("") // never drained30 defer unsub()3132 done := make(chan struct{})33 go func() {34 for i := 0; i < 100; i++ { h.Publish(DraftEvent{DraftID: "x"}) }35 close(done)36 }()37 select {38 case <-done:39 case <-time.After(2 * time.Second):40 t.Fatal("Publish blocked on a full subscriber buffer")41 }42}
Priority queue tests — SQL ↔ Go parity
1// services/api/internal/repo/priority_test.go23func TestGoRank(t *testing.T) {4 tests := []struct{ urgency, sentiment string; want int }{5 {"HIGH", "NEGATIVE", 7},6 {"HIGH", "NEUTRAL", 6},7 {"NORMAL", "NEGATIVE", 5},8 {"NORMAL", "POSITIVE", 4},9 {"LOW", "NEGATIVE", 3},10 {"LOW", "POSITIVE", 2},11 }12 for _, tt := range tests {13 got := goRank(Draft{Urgency: tt.urgency, Sentiment: tt.sentiment})14 if got != tt.want {15 t.Errorf("goRank(%s,%s) = %d, want %d",16 tt.urgency, tt.sentiment, got, tt.want)17 }18 }19}2021func TestGoRankOrders(t *testing.T) {22 // Urgency strictly dominates sentiment.23 if goRank(Draft{Urgency: "HIGH", Sentiment: "NEUTRAL"}) <=24 goRank(Draft{Urgency: "NORMAL", Sentiment: "NEGATIVE"}) {25 t.Error("HIGH urgency must outrank NORMAL regardless of sentiment")26 }27}
Request Flow, End to End
The UI mutation returns after the XADD — before the AI pipeline runs. The draft arrives later over the WebSocket subscription. The API is never blocked on LLM latency.
Run the Tests
1cd resolver_code/services/api23# All tests4go test ./...56# Verbose, with race detector7go test -race -v ./...89# Just service tests10go test -v ./internal/service/...1112# Just hub tests13go test -v ./internal/pubsub/...1415# Just repo/priority tests16go test -v ./internal/repo/...
![Let's Build a Print-Ready Die-Cut Sticker SaaS from scratch in Golang & Next.js [Part 6]](/_next/image/?url=https%3A%2F%2Fcdn.sanity.io%2Fimages%2F3e1sexdu%2Fproduction%2Feeb1314f51d4c39e5d1e176c2c837de8f33725ca-1600x739.png%3Frect%3D61%2C0%2C1478%2C739%26w%3D800%26h%3D400%26q%3D85%26fit%3Dcrop%26auto%3Dformat&w=3840&q=75)