Module: Parse::Agent::Prompts
- Defined in:
- lib/parse/agent/prompts.rb
Overview
Standalone prompt catalog and renderer for the MCP prompts layer.
This module can be loaded independently of the WEBrick MCPServer. All references to Parse::Agent::PARSE_CONVENTIONS and Parse::Agent::RelationGraph are resolved at call-time (inside lambda bodies), so the file remains loadable standalone as long as those constants exist by the time render() is invoked.
Extension API
Third-party apps may register custom prompts:
Parse::Agent::Prompts.register(
name: "my_prompt",
description: "Does something useful",
arguments: [{ "name" => "id", "description" => "Object ID", "required" => true }],
renderer: ->(args) { "Do the thing with #{args['id']}" }
)
A renderer lambda may return either:
- A String — used directly as the message text; description defaults to
"Parse analytics prompt: <name>".
- A Hash with :description and :text keys — both are used verbatim in the
MCP response.
Registering a name that matches a builtin replaces the builtin in responses. Call reset_registry! to restore builtins-only state (useful in tests).
Defined Under Namespace
Modules: Validators
Constant Summary collapse
- BUILTIN_PROMPTS =
Built-in prompt catalog (string keys so list/render work in pure Ruby).
[ { "name" => "parse_conventions", "description" => "Generic Parse platform conventions (objectId, createdAt, pointer/date shapes, _User, ACL). Fetch once and prepend to your system message.", "arguments" => [], }, { "name" => "parse_relations", "description" => "Compact ASCII diagram of class relationships derived from belongs_to and has_many :through => :relation. Pass `classes` for a subset slice (both endpoints must be in the set).", "arguments" => [ { "name" => "classes", "description" => "Optional comma-separated subset, e.g. \"_User,Post,Company\"", "required" => false }, ], }, { "name" => "explore_database", "description" => "Survey all Parse classes: list them, count each, and summarize what each appears to store", "arguments" => [], }, { "name" => "class_overview", "description" => "Describe a class in detail: schema, total count, and a few sample objects", "arguments" => [ { "name" => "class_name", "description" => "Parse class name", "required" => true }, ], }, { "name" => "count_by", "description" => "Count objects in a class grouped by a field (e.g. users by team, projects by status)", "arguments" => [ { "name" => "class_name", "description" => "Parse class to count", "required" => true }, { "name" => "group_by", "description" => "Field to group by", "required" => true }, ], }, { "name" => "recent_activity", "description" => "Show the most recently created objects in a class (answers \"when was the last X created\")", "arguments" => [ { "name" => "class_name", "description" => "Parse class name", "required" => true }, { "name" => "limit", "description" => "Number of objects to return (default 10)", "required" => false }, ], }, { "name" => "find_relationship", "description" => "Find objects in one class related to a given object in another (e.g. members of a team)", "arguments" => [ { "name" => "parent_class", "description" => "Class of the parent object (e.g. Team)", "required" => true }, { "name" => "parent_id", "description" => "objectId of the parent", "required" => true }, { "name" => "child_class", "description" => "Class to query (e.g. _User)", "required" => true }, { "name" => "pointer_field", "description" => "Field on child_class that points to parent (e.g. team)", "required" => true }, ], }, { "name" => "created_in_range", "description" => "Count and sample objects created within a date range", "arguments" => [ { "name" => "class_name", "description" => "Parse class name", "required" => true }, { "name" => "since", "description" => "ISO8601 lower bound (inclusive)", "required" => true }, { "name" => "until", "description" => "ISO8601 upper bound (exclusive); omit for now", "required" => false }, ], }, ].freeze
- BUILTIN_RENDERERS =
Builtin renderers — each lambda takes the args Hash and returns a String. References to Parse::Agent constants are resolved at call-time.
{ "parse_conventions" => ->(args) { Parse::Agent::PARSE_CONVENTIONS }, "parse_relations" => ->(args) { subset = args["classes"].to_s.split(",").map(&:strip).reject(&:empty?) subset.each { |c| Validators.validate_identifier!(c, "classes entry") } subset = nil if subset.empty? edges = Parse::Agent::RelationGraph.build(classes: subset) diagram = Parse::Agent::RelationGraph.to_ascii(edges) slice_note = subset ? " (subset: #{subset.join(", ")})" : "" empty_subset_hint = (subset && edges.empty?) ? " No edges matched the requested subset — check the class names for casing and spelling (e.g. `_User`, not `_user`)." : "" "Class relationships in this Parse database#{slice_note}.#{empty_subset_hint} " \ "Owning-field names are camelCase exactly as stored in Parse. " \ "Read each line as: <one side> ─<cardinality>→ <many side> (owning field). " \ "Use the owning field name with `query_class where:` to filter by that pointer, or with `include:` to expand it.\n\n#{diagram}" }, "explore_database" => ->(args) { "Survey the Parse database. Call get_all_schemas to list every class, then call count_objects on each to get totals. " \ "Skip `_`-prefixed system classes other than `_User` and `_Role` (they may be empty, huge, or return errors). " \ "Group remaining classes by likely purpose (users/auth, content, app-specific) and summarize what the database is for." }, "class_overview" => ->(args) { cn = Validators.validate_identifier!(args["class_name"], "class_name") "Describe the #{cn} class. Call get_schema for #{cn}, count_objects to get the total, and get_sample_objects (limit: 3). Summarize fields, what the class represents, and notable values in the samples." }, "count_by" => ->(args) { cn = Validators.validate_identifier!(args["class_name"], "class_name") gb = Validators.validate_identifier!(args["group_by"], "group_by") pipeline = [ { "$group" => { "_id" => "$#{gb}", "count" => { "$sum" => 1 } } }, { "$sort" => { "count" => -1 } }, { "$limit" => 25 }, ] "Count #{cn} objects grouped by #{gb}. Use aggregate with class_name=\"#{cn}\" and pipeline #{pipeline.to_json}. " \ "If #{gb} is a pointer field, Parse returns each `_id` as the literal string \"ClassName$objectId\" (e.g. \"Team$abc123\") — strip the \"ClassName$\" prefix to recover the objectId, then optionally call get_object on a few to label them. " \ "Report the top groups, call out any null/missing values, and give the total." }, "recent_activity" => ->(args) { cn = Validators.validate_identifier!(args["class_name"], "class_name") limit = (args["limit"] || 10).to_i limit = 10 if limit <= 0 limit = 100 if limit > 100 "Show the #{limit} most recently created #{cn} objects. Use query_class with class_name=\"#{cn}\", order=\"-createdAt\", limit=#{limit}. Report the createdAt of the latest one prominently and highlight notable fields." }, "find_relationship" => ->(args) { pc = Validators.validate_identifier!(args["parent_class"], "parent_class") pid = Validators.validate_object_id!(args["parent_id"], "parent_id") cc = Validators.validate_identifier!(args["child_class"], "child_class") pf = Validators.validate_identifier!(args["pointer_field"], "pointer_field") where = { pf => { "__type" => "Pointer", "className" => pc, "objectId" => pid } } "Find #{cc} objects whose #{pf} field points to #{pc} #{pid}. " \ "First call count_objects with class_name=\"#{cc}\" and where=#{where.to_json}. " \ "Then call query_class with the same constraint, limit 20, to show a sample. " \ "Note: #{pf} must match the field name as stored (camelCase as defined in the schema). Report the count first." }, "created_in_range" => ->(args) { cn = Validators.validate_identifier!(args["class_name"], "class_name") since = Validators.validate_iso8601!(args["since"], "since") upper = Validators.validate_iso8601!(args["until"], "until", required: false) date_constraint = { "$gte" => { "__type" => "Date", "iso" => since } } date_constraint["$lt"] = { "__type" => "Date", "iso" => upper } if upper where = { "createdAt" => date_constraint } "Count #{cn} objects created since #{since}#{upper ? " and before #{upper}" : ""}. " \ "Use count_objects with class_name=\"#{cn}\" and where=#{where.to_json}. " \ "Then call query_class with the same where, order=\"-createdAt\", limit=10 for a sample. Report the count and the date range of the sample." }, }.freeze
Class Method Summary collapse
-
.list ⇒ Array<Hash>
Returns the full list of prompt definitions for the MCP prompts/list response.
- .notify_subscribers ⇒ Object private
-
.register(name:, description:, arguments: [], renderer:) ⇒ Object
Register a custom prompt.
-
.render(name, args = {}) ⇒ Hash
Renders a prompt by name and returns the MCP prompts/get response shape.
-
.reset_registry! ⇒ Object
Clears the custom registry, restoring builtins-only state.
-
.reset_subscribers! ⇒ Object
Remove all subscribers.
-
.subscribe { ... } ⇒ Proc
Subscribe to registry-changed events.
Class Method Details
.list ⇒ Array<Hash>
Returns the full list of prompt definitions for the MCP prompts/list response. Registered prompts override builtins with the same name.
245 246 247 248 249 250 251 252 |
# File 'lib/parse/agent/prompts.rb', line 245 def list merged = {} BUILTIN_PROMPTS.each { |p| merged[p["name"]] = p } REGISTRY_MUTEX.synchronize do @registry.each { |name, entry| merged[name] = entry[:prompt] } end merged.values end |
.notify_subscribers ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
338 339 340 341 342 343 344 345 346 347 |
# File 'lib/parse/agent/prompts.rb', line 338 def notify_subscribers snapshot = REGISTRY_MUTEX.synchronize { @subscribers.dup } snapshot.each do |callback| begin callback.call rescue StandardError => e warn "[Parse::Agent::Prompts] subscriber raised: #{e.class}: #{e.}" end end end |
.register(name:, description:, arguments: [], renderer:) ⇒ Object
Register a custom prompt. Thread-safe. Idempotent on same name (replaces).
295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/parse/agent/prompts.rb', line 295 def register(name:, description:, arguments: [], renderer:) prompt = { "name" => name.to_s, "description" => description.to_s, "arguments" => arguments, } REGISTRY_MUTEX.synchronize do @registry[name.to_s] = { prompt: prompt, renderer: renderer } end notify_subscribers nil end |
.render(name, args = {}) ⇒ Hash
Renders a prompt by name and returns the MCP prompts/get response shape.
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/parse/agent/prompts.rb', line 260 def render(name, args = {}) renderer = nil REGISTRY_MUTEX.synchronize { renderer = @registry[name]&.fetch(:renderer, nil) } renderer ||= BUILTIN_RENDERERS[name] raise Parse::Agent::ValidationError, "Unknown prompt: #{name}" if renderer.nil? result = renderer.call(args) if result.is_a?(Hash) description = (result[:description] || result["description"]).to_s text = (result[:text] || result["text"]).to_s else description = "Parse analytics prompt: #{name}" text = result.to_s end { "description" => description, "messages" => [ { "role" => "user", "content" => { "type" => "text", "text" => text }, }, ], } end |
.reset_registry! ⇒ Object
Clears the custom registry, restoring builtins-only state. Intended for use in test suites.
310 311 312 313 314 |
# File 'lib/parse/agent/prompts.rb', line 310 def reset_registry! REGISTRY_MUTEX.synchronize { @registry.clear } notify_subscribers nil end |
.reset_subscribers! ⇒ Object
Remove all subscribers. Intended for test suites.
332 333 334 335 |
# File 'lib/parse/agent/prompts.rb', line 332 def reset_subscribers! REGISTRY_MUTEX.synchronize { @subscribers.clear } nil end |
.subscribe { ... } ⇒ Proc
Subscribe to registry-changed events. The block is invoked with no arguments after every register or reset_registry! call. Returns a Proc that, when called, deregisters the subscriber. Used by Parse::Agent::MCPRackApp::SSEBody to drive MCP ‘notifications/prompts/list_changed` broadcasts.
324 325 326 327 328 329 |
# File 'lib/parse/agent/prompts.rb', line 324 def subscribe(&block) raise ArgumentError, "block required" unless block REGISTRY_MUTEX.synchronize { @subscribers << block } -> { REGISTRY_MUTEX.synchronize { @subscribers.delete(block) } } end |