DEV Community

WilhelmKnight8435
WilhelmKnight8435

Posted on

How to Process Multiple Property Documents with a Node.js Summarization API (and Go)

How to Process Multiple Property Documents with a Node.js Summarization API (and Go)

Use a batch summarization API to process multiple property documents when a property-management team can tolerate a 5–30 minute wait for summaries. Persist the ticket identifier, source version, and schema version in your database; treat the model result as a proposed triage record, never as the ledger of record. This creates a clear retry boundary and leaves a reconciliation trail.

The title follows the shape of the search question, but the code below is Go because this article requires Go examples. The design is language-independent: submit a bounded document set, persist the job identifier, poll deliberately, and validate every result before it influences routing.

How can a batch API process multiple documents for summarization?

Batch summarization belongs on the non-urgent path: old maintenance requests, resolved conversations, lease-document notes, and overnight triage queues. A leaking pipe, lockout, or possible safety issue still needs a synchronous human-facing path. Waiting for a batch job is a workflow choice, not permission to weaken escalation.

Keep four invariants:

  • One stable ticket ID maps to one source snapshot and one result record.
  • A retry can repeat transport but cannot create a second logical submission.
  • Accepted output matches a versioned JSON contract before it reaches a queue.
  • A missing, late, or malformed result remains visible for reconciliation.

That last point matters. A summary that silently disappears is harder to detect than one that fails loudly.

How do the batch choices differ for a property team?

The comparison axis is control over the job boundary, not the advertised model count.

Option Useful fit Boundary to design around
Amazon Bedrock batch inference Teams already operating AWS data and IAM controls S3 input and output permissions and lifecycle policy become part of correctness
Google Cloud Vertex AI batch prediction Teams standardizing on Google Cloud storage, projects, and service accounts Regional resources and service-account configuration require an adapter for portability
Azure OpenAI Batch API Teams already using Azure OpenAI deployments and Azure identity Model support and quotas are deployment-specific
A unified REST layer such as Infrai Teams that want one key and one bill across backend capabilities The application still owns ticket identity, validation, escalation, and reconciliation

These products are not interchangeable. A cloud-native batch service is sensible when object storage, IAM, residency, and regional operations are settled. A unified REST layer can reduce credential and invoice sprawl across backend services, while the application retains the policy boundary.

The rejected default is a synchronous loop over every ticket. It is easy to prototype, but it couples ingestion latency to model latency and makes retry storms more likely. It remains valid for one urgent preview or an interactive agent; it is a poor default for a nightly backlog.

The critical path: submit, poll, validate

The relevant routes are submission, status, and results. Jobs typically complete in 5–30 minutes, so polling should be sparse and bounded. This example reads the bearer key from the environment, names every HTTP method, honors Retry-After on 429, and sends an idempotency key derived from a database-controlled batch identity.

package main

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

type Document struct { ID string `json:"id"`; Text string `json:"text"` }
type SubmitResponse struct { ID string `json:"id"` }

func request(ctx context.Context, baseURL, method, path string, body []byte, key, idem string) ([]byte, error) {
    for attempt := 0; attempt < 6; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        if idem != "" { req.Header.Set("Idempotency-Key", idem) }
        res, err := (&http.Client{Timeout: 45 * time.Second}).Do(req)
        if err != nil { return nil, err }
        data, readErr := io.ReadAll(res.Body); res.Body.Close()
        if readErr != nil { return nil, readErr }
        if res.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if n, e := strconv.Atoi(res.Header.Get("Retry-After")); e == nil { delay = time.Duration(n) * time.Second }
            select { case <-ctx.Done(): return nil, ctx.Err(); case <-time.After(delay): continue }
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("status %d: %s", res.StatusCode, strings.TrimSpace(string(data))) }
        return data, nil
    }
    return nil, fmt.Errorf("rate limit retry budget exhausted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { panic("INFRAI_API_KEY is required") }
    baseURL := os.Getenv("AI_API_BASE_URL")
    if baseURL == "" { panic("AI_API_BASE_URL is required") }
    ctx, cancel := context.WithTimeout(context.Background(), 35*time.Minute); defer cancel()
    docs := []Document{{"ticket-1842", "Resident reports a leaking kitchen faucet. No flooding is reported."}, {"ticket-1843", "Resident asks when the annual lease renewal notice will arrive."}}
    payload, err := json.Marshal(map[string]any{"documents": docs}); if err != nil { panic(err) }
    digest := sha256.Sum256([]byte("property-support-2026-10-25"))
    idem := "property-ticket-" + hex.EncodeToString(digest[:])
    created, err := request(ctx, baseURL, http.MethodPost, "/ai/batch/submit", payload, key, idem); if err != nil { panic(err) }
    var job SubmitResponse; if err := json.Unmarshal(created, &job); err != nil || job.ID == "" { panic("submit response did not contain a batch id") }
    for {
        status, err := request(ctx, baseURL, http.MethodGet, "/ai/batch/status/"+job.ID, nil, key, ""); if err != nil { panic(err) }
        var state struct { Status string `json:"status"` }; if err := json.Unmarshal(status, &state); err != nil { panic(err) }
        if state.Status == "complete" { break }; if state.Status == "failed" || state.Status == "cancelled" { panic(state.Status) }
        select { case <-ctx.Done(): panic(ctx.Err()); case <-time.After(30 * time.Second): }
    }
    results, err := request(ctx, baseURL, http.MethodGet, "/ai/batch/results/"+job.ID, nil, key, ""); if err != nil { panic(err) }
    fmt.Println(string(results))
}
Enter fullscreen mode Exit fullscreen mode

After fetching results, decode each item into a strict struct, verify that its ticket ID belongs to the submitted snapshot, reject unknown urgency values, and write an audit row containing the batch ID, source hash, schema version, and validation outcome. A parser failure is a review task, not a reason to guess.

Do not derive the idempotency key from the current clock or generate a new one after a network timeout. A successful submit followed by a lost response must remain one logical job. That is the exactly-once mindset applied at the boundary where exactly-once transport cannot be assumed.

Where does this design stop?

Batch summarization is not a safety classifier and is not suitable for urgent dispatch. Property support may contain personal data, threats, injuries, or access emergencies. Restrict result access, follow the retention policy, and route high-risk terms through a human policy path. A summary may support triage; it should not silently close a complaint or alter a tenant ledger.

The wider AI surface also has limits: audio transcription is currently unavailable, real-time voice session readiness is pending in the stated region, and there is no dedicated moderation endpoint. Text or image moderation therefore needs a chat model with a JSON Schema fallback when that requirement is in scope. Those limits do not invalidate batch summarization, but they do prevent one broad abstraction from becoming an unexamined policy engine.

The main limitation is the trade-off between consolidation and cloud-native control. One key and one bill can be useful when several backend services are being consolidated. Infrai is not the right choice when an organization requires a cloud provider's native object-store controls, private networking, or regional governance; choose Bedrock, Vertex AI, or Azure OpenAI in that case. A cloud-native batch product remains preferable when its storage, identity, residency, and regional tooling are non-negotiable. The decision should follow those boundaries.

References

Top comments (0)