Module: Parse::Agent::ResultFormatter

Extended by:
ResultFormatter
Included in:
ResultFormatter
Defined in:
lib/parse/agent/result_formatter.rb

Overview

The ResultFormatter transforms Parse API responses into LLM-friendly formats that are easy to understand and process.

It provides consistent structure, human-readable type descriptions, and truncates large results to fit context windows.

Constant Summary collapse

MAX_RESULTS_DISPLAY =

Maximum number of results to include in output

50
DROPPED_OBJECT_KEYS =

Keys stripped from every simplified data object before it reaches the LLM. The raw ACL map (per-role / per-user read/write bits) is operationally useless to a model reasoning over row data — the agent's effective read/write authority is enforced server-side regardless of what ACL a row carries — so surfacing it is pure token overhead plus a minor disclosure of role/user identifiers. Applied recursively (nested included records too).

%w[ACL].freeze
TYPE_NAMES =

Parse field type mappings for human-readable output

{
  "String" => "string",
  "Number" => "number",
  "Boolean" => "boolean",
  "Date" => "date/time",
  "Object" => "object (JSON)",
  "Array" => "array",
  "GeoPoint" => "geo location",
  "File" => "file",
  "Pointer" => "pointer (reference)",
  "Relation" => "relation (many-to-many)",
  "Bytes" => "binary data",
  "Polygon" => "polygon (geo shape)",
  "ACL" => "access control list",
}.freeze

Instance Method Summary collapse

Instance Method Details

#format_object(class_name, object, truncated_include_fields: nil) ⇒ Hash

Format a single object

Parameters:

  • class_name (String)

    the class name

  • object (Hash)

    the object data

  • truncated_include_fields (Hash, nil) (defaults to: nil)

    map of pointer-name => source: when keys-on-include auto-projection narrowed any joined record.

Returns:

  • (Hash)

    formatted object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/parse/agent/result_formatter.rb', line 221

def format_object(class_name, object, truncated_include_fields: nil)
  envelope = {
    class_name: class_name,
    object_id: object["objectId"],
    created_at: object["createdAt"],
    updated_at: object["updatedAt"],
    object: simplify_object(object),
  }
  if truncated_include_fields && !truncated_include_fields.empty?
    envelope[:truncated_include_fields] =
      truncated_include_fields.transform_values { |meta| meta[:dropped] }
  end
  envelope
end

#format_query_results(class_name, results, limit:, skip:, where: nil, keys: nil, order: nil, include: nil, truncated_include_fields: nil) ⇒ Hash

Format query results

Parameters:

  • class_name (String)

    the class that was queried

  • results (Array<Hash>)

    array of result objects

  • limit (Integer)

    the limit that was requested

  • skip (Integer)

    the skip offset

  • where (Hash, nil) (defaults to: nil)

    query constraints from the original call

  • keys (Array<String>, nil) (defaults to: nil)

    field projection from the original call

  • order (String, nil) (defaults to: nil)

    sort field from the original call

  • include (Array<String>, nil) (defaults to: nil)

    pointer includes from the original call

Returns:

  • (Hash)

    formatted results



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/parse/agent/result_formatter.rb', line 162

def format_query_results(class_name, results, limit:, skip:,
                         where: nil, keys: nil, order: nil, include: nil,
                         truncated_include_fields: nil)
  total = results.size
  truncated = total > MAX_RESULTS_DISPLAY
  has_more = total >= limit

  displayed_results = if truncated
      results.first(MAX_RESULTS_DISPLAY)
    else
      results
    end

  next_call = if has_more
      next_args = {
        class_name: class_name,
        limit: limit,
        skip: skip + limit,
        where: where,
        keys: keys,
        order: order,
        include: include,
      }.compact
      { tool: "query_class", arguments: next_args }
    end

  # Surface keys-on-include auto-projection metadata so the LLM
  # can see which joins were narrowed and re-ask with explicit
  # dotted paths (`keys: ["user.iconImage"]`) if it needs fields
  # that were dropped. Suppress the key when nothing was auto-
  # projected — keeps the envelope minimal for the common case.
  truncated_includes_payload =
    if truncated_include_fields && !truncated_include_fields.empty?
      truncated_include_fields.transform_values { |meta| meta[:dropped] }.compact
    end

  {
    class_name: class_name,
    result_count: total,
    pagination: {
      limit: limit,
      skip: skip,
      has_more: has_more,
    },
    truncated: truncated,
    truncated_note: truncated ? "Showing first #{MAX_RESULTS_DISPLAY} of #{total} results" : nil,
    truncated_include_fields: truncated_includes_payload,
    next_call: next_call,
    results: displayed_results.map { |obj| simplify_object(obj) },
  }.compact
end

#format_schema(schema) ⇒ Hash

Format a single schema for detailed display

Parameters:

  • schema (Hash)

    schema object from Parse (enriched with metadata)

Returns:

  • (Hash)

    formatted schema details



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/parse/agent/result_formatter.rb', line 93

def format_schema(schema)
  class_name = schema["className"]
  fields = schema["fields"] || {}
  indexes = schema["indexes"] || {}
  clp = schema["classLevelPermissions"] || {}
  agent_methods = schema["agent_methods"] || []

  result = {
    class_name: class_name,
    type: class_type(class_name),
  }

  # Include class description if present
  result[:description] = schema["description"] if schema["description"]

  # Include analytics usage hint if present (separate from description)
  result[:usage] = schema["usage"] if schema["usage"]

  result[:fields] = format_fields_detailed(fields)
  result[:indexes] = format_indexes(indexes)
  result[:permissions] = format_clp(clp)

  # Include agent methods if any
  result[:agent_methods] = agent_methods if agent_methods.any?

  # Include the canonical "valid state" filter when declared. Lets
  # callers that opt out of the default `apply_canonical_filter`
  # behavior reproduce the predicate manually in their where:.
  if schema["canonical_filter"].is_a?(Hash) && schema["canonical_filter"].any?
    result[:canonical_filter] = schema["canonical_filter"]
  end

  # Echo the wire-format agent_fields allowlist when declared. The
  # allowlist already filters `result[:fields]` by omission, but the
  # explicit list answers "what may I write in `keys:` for this
  # class" without forcing the consumer to scan the fields array.
  # Storage-form columns (`_p_*`) and other Parse-internal
  # underscored columns are never addressable through agent tools.
  if schema["agent_fields"].is_a?(Array) && schema["agent_fields"].any?
    result[:agent_fields] = schema["agent_fields"]
  end

  # Echo the narrower join projection (wire-format) when declared.
  # Tells consumers "when this class is included on another class's
  # query, these are the fields you'll see."
  if schema["agent_join_fields"].is_a?(Array) && schema["agent_join_fields"].any?
    result[:agent_join_fields] = schema["agent_join_fields"]
  end

  # Include relationship edges if any (set by MetadataRegistry)
  if schema["relations"].is_a?(Hash) &&
     (schema["relations"]["outgoing"].to_a.any? || schema["relations"]["incoming"].to_a.any?)
    result[:relations] = schema["relations"]
  end

  result
end

#format_schemas(schemas) ⇒ Hash

Format multiple schemas for display (compact summary) Returns class names grouped by type for efficient token usage. Use get_schema for detailed field info on specific classes.

Parameters:

  • schemas (Array<Hash>)

    array of schema objects from Parse (enriched with metadata)

Returns:

  • (Hash)

    formatted schema summary



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/parse/agent/result_formatter.rb', line 50

def format_schemas(schemas)
  built_in = []
  custom = []

  schemas.each do |schema|
    class_name = schema["className"]
    fields = schema["fields"] || {}
    agent_methods = schema["agent_methods"] || []

    # Subtract the four system fields (objectId, createdAt, updatedAt,
    # ACL) when reporting a "user-meaningful" count, but never let the
    # subtraction go negative — the allowlist filter in enriched_schema
    # may have already trimmed system fields out.
    info = {
      name: class_name,
      fields: [fields.size - 4, 0].max,
    }

    # Include description if present (compact)
    info[:desc] = schema["description"] if schema["description"]

    # Include agent methods count if any
    info[:methods] = agent_methods.size if agent_methods.any?

    if class_name.start_with?("_")
      built_in << info
    else
      custom << info
    end
  end

  {
    total: schemas.size,
    note: "Use get_schema(class_name) for detailed field info",
    built_in: built_in,
    custom: custom,
  }
end

#simplify_object(obj) ⇒ Object

Simplify an object for display (resolve __type fields). Strips the raw ACL map (see DROPPED_OBJECT_KEYS). Public so the query/get/ atlas tool envelopes can route their rows through the same normalization query_class already uses.



398
399
400
401
402
403
404
405
406
# File 'lib/parse/agent/result_formatter.rb', line 398

def simplify_object(obj)
  return obj unless obj.is_a?(Hash)

  obj.each_with_object({}) do |(key, value), acc|
    next if DROPPED_OBJECT_KEYS.include?(key.to_s)

    acc[key] = simplify_value(value)
  end
end