Repository navigation
feat(map-ai): generate SVG edit proposals for Concierge (PPT-2832) - #453
Conversation
PPT-2833. AI map editing reuses the signage AI provider rows and job table with a text/vision adapter, so the pieces both adapter families need move out of ImageGen::Adapter: credentials, credential and vendor_message become Utils::VendorCredentials, and the job long poll and same-domain job lookup become Utils::AIJobs so the map AI controller can include them rather than copy them. SignageAI's behaviour is unchanged. The spec helpers setup_signage_ai, signage_group and await_signage_ai_job move into spec/spec_helpers so a second controller spec can use them without requiring the signage spec file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PPT-2833. The AI map editor spends vendor calls on behalf of whoever may save the level's map, so the zone update gate is extracted into Utils::GroupPermissions#zone_updatable?: support or admin, the legacy org-zone ACL, or a support-subsystem Update on the zone. The broad signage subsystem grant that Zones#update also honours is left out of it. Zones#check_update_permissions keeps its behaviour exactly; its private record-form legacy check now delegates to the shared zone_legacy_access?. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PPT-2833. A text/vision call against a signage AI provider row for map editing. The editor posts the context it built from the level's SVG (element index, shortlisted subtrees, annotations, a marked up snapshot by upload id) and the model answers with a closed vocabulary of edit operations that the editor applies itself; nothing here reads or writes the map. - Llm::Schema defines the vocabulary once and renders it in the OpenAI strict dialect; the TypeScript side in libs/map-editor mirrors it field for field. - Llm::Prompt orders the request so the stable front (rules, op reference, index, subtrees) is identical across the turns of one edit and the instruction comes last, with byte caps per section and the parent chain's instruction, ops and resolution for a refine. - Llm::Ops parses the answer (markdown fence tolerated) and checks each op against the posted index; failing ops are kept with server_issue so the editor can show them unchecked with the reason. - Adapters::OpenAIChat posts chat completions with response_format json_schema strict, max_completion_tokens and reasoning_effort (dropped and resent on a 400 naming it), for OpenAI and both Azure URL styles; refusal and content_filter read as Moderated, 429 as Busy, IO::TimeoutError as Error::ImageGen::Timeout. No temperature: the gpt-5 family rejects it. - Llm::Runner is the one-call version of the image runner: Running, markup fetch, prompt, one vendor call under one Llm slot (sized by MAP_AI_MAX_CALLS, apart from the image slots), result written with bump_version, cost_units = total tokens, invalid_response for an answer that is not JSON or was cut off. - Error::ImageGen::InvalidResponse (502) and the MAP_AI_* constants. - The markup snapshot tag ai-map-markup, reaped by the existing sweep. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PPT-2833. /api/engine/v2/maps/ai: capabilities (providers that can edit maps, the default, the map quota, the limits), edit (202, the job), jobs/:id long poll, jobs index (kind, mine, zone_id), cancel, resolve (what the person did with the answer, written into result.resolution), usage (support, map edits only). The edit gate is "may update this zone": support or admin, the legacy org-zone ACL, or a support-subsystem Update, without the broad signage grant. A non-support caller sees 404 for a zone outside the authority's org zone tree. Quotas are counted by kind against map_user_per_day and map_domain_per_month (defaults 40 and 1000). The context is posted inline under MAP_AI_MAX_CONTEXT_BYTES; the row keeps its size and sha256, never the body. The markup upload must be the caller's own png, jpeg or webp under the image size cap, and is tagged for the sweep when it was uploaded for this request. Provider rows gain text_model (create, update, clear) and POST /:id/test with kind=text runs a five-token chat completion under an Llm slot. The signage jobs index takes a kind filter defaulting to the image kinds, so map jobs stay out of "recent generations". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PPT-2833. Chat completions stubs keyed on the /v1/chat/completions path so the image stub (which matches every api.openai.com POST, and WebMock uses the first registered match) cannot swallow them: a verbatim answer with the requests it received, a refusal, a timeout, prose, and a 400 naming reasoning_effort followed by the answer. map_ai_spec covers capabilities (no provider, disabled, Vertex only, on with the text model and the map quota), the edit path end to end with what reached the vendor, the markup snapshot as an image part in the right position, fenced JSON, prose and a cut-off answer as invalid_response, a flagged unknown anchor, refusal, timeout, the reasoning_effort retry, the map quota by kind, the read-only scope, the concierge org-zone ACL, a caller who cannot update the zone, the broad signage grant, a zone outside the organisation, the context caps, idempotency replay, the parent chain in the prompt, an image job as parent, the jobs index and its kind filter on both controllers, long poll and cancel, resolve, and usage by kind. llm_prompt_spec covers the message order, the caps, the parent chain, the strict rendering of the schema, and every check in Llm::Ops. The storage GET stub now answers with a body_io: WebMock hands a stubbed response to Http.get_bytes's streaming block as-is, so with a body string Store.fetch always failed under WebMock and no spec had proven the source, reference or markup fetch path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PPT-2833. `crystal build ./src/app.cr && ./app -d > OPENAPI_DOC.yml`, run locally against the models branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The vendor is now sent libs/map-editor's ops-schema.json as it is: one
anyOf variant per op, set_style_rule.declarations and insert_shape.attrs
as {name, value} arrays, schema_version in the envelope, $defs for target
and name_value. The Crystal rendering that was sent before was a flat
26-field object with declarations as a string, so nothing the model
returned could pass the editor's own validator. The file is embedded at
compile time and the vocabulary (ops, fields, shapes, positions) is read
from it, so the two copies cannot drift apart silently; the spec asserts
the rendering against the file. Both copies carry sha256
ff41e284fc4e145587baa7227f60e01b16dfc08ca59db14ff5184385f0b2bbd5.
Ops are stored exactly as the model returned them. The server's
bookkeeping moves off the op: result.server_issues maps op-N (N the
1-based position) to the reason, because the editor's schemaIssues
rejects any key outside the variant and the plan's inline server_issue
and id would have locked every row.
The checks follow the editor's rules: attribute, style and geometry
values are syntax-checked (value-syntax.ts ported, named colours
included), declarations are arrays, insert_shape attributes are per
kind with transform refused, the deny-list for replacement markup is
sanitise.ts's list plus <style> and <use>, <a> and data: images are
allowed, url(#fragment) is allowed, every element must be in the SVG
namespace, a backslash in CSS text is refused since browsers decode
escapes past a literal match, a transform with every field null is
refused, duplicate needs translate and a whole count, and a new id used
twice in one answer is flagged.
A finish_reason of length is retried once at MAP_AI_RETRY_OUTPUT_TOKENS
before the job fails as invalid_response; tokens are summed over the
attempts and usage carries the attempt count.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… by kind JobResponse carries server_issues beside the untouched ops. The parent chain is read before the slot is reserved and the row written, so a failure there can no longer leave a QUEUED row the stale sweep never touches with a process-local slot held. An idempotency key already used by an image job answers 409 instead of replaying that image job. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The generate/edit gate and remaining_quota summed every kind, so map edits consumed the image allowance. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A length finish with empty content, which a reasoning model produces when the budget is spent before the answer, reported ok. The probe now sends the same low reasoning effort as an edit, allows 128 output tokens and reports invalid_response when nothing came back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The prompt tells the model that a room or desk name in the instruction identifies the element whose id carries its number, that outlined labels are glyph elements inside the room they name, how to rename one (delete the glyphs, insert a text element), and to act on the best-supported reading instead of asking whenever an element carries the number. The index gains fill and inside columns, read from the context's fill and in fields; the fixture carries both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The prompt prints a LABELS section from the context's labels list (the element each label sits in, its glyph anchors in reading order, box and fill) and tells the model to rename an outlined label by deleting those anchors and inserting a text element sized from the label's box. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The prompt asks for the new text after the label's last glyph, y at the first line's baseline, and lines separated by newline characters, which the editor lays out as rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The index gains a piece column from the context's piece field, and the prompt tells the model that elements sharing a piece are the layers of one object, to be deleted, moved or copied together. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d placement rules Adds an Anthropic Messages adapter: structured output through output_config.format, the effort from MAP_AI_ANTHROPIC_EFFORT, and fallbacks: "default" for the models whose classifiers may decline a request; a model that rejects either parameter is asked again without it. A refusal is a moderation failure, 429 and overloaded are busy. Capabilities now list the text models each provider is known to serve so the editor can offer a choice, and the job usage records the model that answered when the vendor says. The prompt gains the thirds of the map as regions, text labels with their words, outlined labels outside every room, and rules for renaming by words, extending a desk run, making a room bookable and an honest summary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Support API keys that require an explicit workspace and cover requests with and without a workspace ID. Co-authored-by: Codex <noreply@openai.com>
Anthropic rejects the nullable reorder-position enum. Send equivalent typed alternatives while preserving the shared editor and OpenAI schema. Cover the conversion and update stale prompt-index expectations for the existing context columns. Co-authored-by: Codex <noreply@openai.com>
Deduplicate nullable schemas through shared definitions to stay within the structured output union limit while preserving required fields and allowed values. Co-authored-by: Codex <noreply@openai.com>
Use strings for all target fields; empty strings already mean absent to both validators. Removing nested nullable target combinations reduces the compiled output grammar while retaining anchor, ID and hash validation. Co-authored-by: Codex <noreply@openai.com>
The operation grammar exceeds Anthropic structured-output compilation limits even with shared nullable definitions. Use the full operation schema as a non-strict client tool, request one proposal and validate its tool input before the existing operation checks. Automatic tool choice supports Fable 5.1 and Opus 5.5, which reject forced tool selection. Co-authored-by: Codex <noreply@openai.com>
Require readable labels after background edits and duplicate furniture within its destination group, rotating complete pieces as needed. Co-Authored-By: Codex <noreply@openai.com>
…t rules These changes were made on 14 Oct and ran in the rest-api sidecar for the local and mqt.placeos.run demos, but were never committed: - set_id renames update the index in proposal order, so later ops can target the new id and the old id becomes free - set_attr viewBox is allowed on the SVG root only, with four finite numbers - prompt rules for preserving label font weight, composited overlay contrast, referencing ids created earlier in the same proposal, translucent booking overlays, shared-wall doorways and bounded canvas expansion Co-authored-by: Codex <noreply@openai.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the proposal returned as a tool input, a response that stopped at max_tokens either had no tool_use block, which raised InvalidResponse, or a truncated one. Both skipped the runner's retry with a larger output budget. The adapter now sets finish to length before reading the tool input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server accepted any positive viewBox on the root, while the editor rejects one that crops the old map or grows it beyond four times. The op checker now applies the same rule against the viewBox the editor posts in the context, so the job's server issues agree with what the editor will allow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bounds line was written ahead of the ANNOTATIONS heading, so the markup part no longer started with its heading and map_ai_spec could not find it. It explains annotations drawn off the map, so it now sits under that heading and is left out when there are no annotations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ap adapters OpenAIImages and the map edit OpenAIChat adapter each carried the same base URL, Azure check, authentication headers and error mapping. They now include Utils::OpenAIVendor. The image adapter also treats Azure's content_filter error code as a moderation refusal, as the chat adapter already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rollers The map AI controller copied the signage AI controller's quota count, cancel action and recent-jobs query, differing only in the job kinds. They now live in Utils::AIJobs next to the shared long poll: remaining_ai_quota, request_job_cancel and recent_ai_jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review pass on this PR, pushed to the branch as six commits (eb8899a..3450a35). Nothing existing was rewritten. Fixes
Reuse of the signage AI code Most of the groundwork was already shared: provider and job rows, the long poll, the vendor HTTP client, slots, the upload store, error types and credentials. Two places had been copied instead:
Verification: the full spec suite passes in CI on crystal latest and unstable, along with Ameba and style. Five live edits ran through a sidecar built from these commits. Screenshots and findings: https://gist.github.com/camreeves/f323d149ce0f83b245161ff6547e451d Not changed, worth a look: the prompt has grown several paragraphs of scenario rules. They're validated against real maps, but should be trimmed once there's an eval set. 🤖 Generated with Claude Code |
- shard.lock: placeos-models stays pinned to the models review branch, now at c687a35, which includes master's 9.119.5 (multiple root groups); the other shards take master's bumps - the signage AI spec helpers live in spec/spec_helpers/signage_ai.cr on this branch, so master's comment update to signage_group is applied there Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner wrote the final job state and only then released its slot in an ensure, so a client that saw a final job could still find the slot taken. map_ai_spec checks exactly that and failed intermittently. The slot now goes back before the final write on every path, as the image runner already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merge-readiness pass (Steve reported clashes). This PR is now mergeable with master.
CI: 1,258 examples on crystal latest and unstable. Seven MCP / ChatGPT plugin specs fail, and master fails the same seven since 760c004 ("chore: update shards", 15 Oct). They don't come from this branch. Before merging this PR (models #334 merged and released first). Codex added three temporary pins in 3623b2d so CI could run against the unreleased models branch, and they must come out:
These were the right call for a draft stacked on an unreleased models change. They're just easy to forget at merge time. 🤖 Generated with Claude Code |
Claude sometimes JSON-encodes a nested tool input parameter, so the proposal arrived with ops as a string and the job failed with "the model's answer has no ops list". The parser now accepts a string holding a JSON array for ops and questions, and logs the answer's field shapes when there is still no ops list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The AI map editing models changes shipped in models 9.120.0, so the branch pins on the review branch go: the shard override, the overridden lock entry and the migration image's --branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add maps/ai endpoints for capability discovery, edit jobs, polling and usage. The server sends bounded SVG context to OpenAI-compatible or Anthropic text models and returns validated edit operations; the browser applies them as a reviewable preview.
Reuse provider credentials and job storage with separate map quotas. Support Anthropic workspace headers and tool-input proposals. Validate references to newly created elements and counted duplicate IDs. Prompt guidance covers readable labels, collision avoidance and furniture grouping.
Validation: 57 focused adapter/prompt/controller specs pass; five distinct live browser prompts and two corrective retries completed without final validation issues. API documentation regenerated; final container image builds successfully. The review branch pins the companion models branch and uses its migrations in the spec harness; remove that temporary pin after the models change lands.
Draft for review and further end-to-end testing. No merge or deployment requested.
Companion frontend: PlaceOS/user-interfaces#570
Companion drafts: Frontend #570, API #453, Models #334.