From 604d6f47b50225bf997e9b56a04a468267c9e705 Mon Sep 17 00:00:00 2001 From: adiel-sammak <71227923+aelsammak@users.noreply.github.com> Date: Tue, 27 Oct 2026 08:33:47 -0400 Subject: [PATCH 1/7] EBP-3331: Add the Python design-tree threading question - Add question 3 (threading) to python/design-mode.md after the delivery question on the Direct Pub/Sub and guaranteed pub/sub branches, with a Step 1 signal row and the ninth Threading summary field (n/a on Request-Reply (Direct)) - Dispatch the sample pair on the Threading field in python/implement-mode.md; a missing field reads as Async and Implement does not ask - Give implement-guaranteed-pubsub.md and implement-direct-pubsub.md both shapes: publish_await_acknowledgement() plus receive_message() on the guaranteed leaf, the direct publisher plus receive_message() on Direct Pub/Sub - Resolve the blocking script names in verify.sh with one resolve_script helper and test the resolution offline in check-samples.sh - Cover the blocking shapes in the verification checklist, the Custom and Solace Suggested overlays, and python.md - Grep the Threading field in the Python design output eval; add minimal-threading output cases for the guaranteed and direct leaves and two trigger cases --- .../evals/output-evals.json | 103 ++++++++++++++++++ .../evals/trigger-evals.json | 2 + .../references/python.md | 4 +- .../references/python/custom-mode.md | 4 +- .../references/python/design-mode.md | 23 ++-- .../evals/sample-check/check-samples.sh | 30 +++++ .../python/implement-direct-pubsub.md | 17 +-- .../python/implement-guaranteed-pubsub.md | 33 +++--- .../references/python/implement-mode.md | 33 +++--- .../references/python/scripts/verify.sh | 29 ++++- .../python/solace-suggested-mode.md | 4 +- .../python/verification-checklist.md | 11 +- 12 files changed, 235 insertions(+), 58 deletions(-) diff --git a/plugins/solace-messaging-skills/evals/output-evals.json b/plugins/solace-messaging-skills/evals/output-evals.json index 7d910a8..b7209df 100644 --- a/plugins/solace-messaging-skills/evals/output-evals.json +++ b/plugins/solace-messaging-skills/evals/output-evals.json @@ -151,6 +151,7 @@ { "type": "assistant_grep", "pattern": "Access type\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Topic\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Consumption endpoint\\**:", "expect": "present" }, + { "type": "assistant_grep", "pattern": "Threading\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Auth\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Broker\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Grounding docs\\**:", "expect": "present" }, @@ -303,6 +304,108 @@ { "type": "file_exists", "glob": "*", "expect": "absent" } ] }, + { + "id": "appdev-python-minimal-threading-guaranteed-implement", + "skill": "solace-application-development", + "user_turns": [ + "Build a Python app with the Solace Python API for food-delivery order processing: an order service publishes customer orders and a kitchen worker processes them. No order may be lost, even if the kitchen worker restarts, and only one worker is active at a time. Plain Python project, no web dashboard. We have a Solace Cloud broker. This is Guaranteed Pub/Sub (single service, exclusive) with PERSISTENT delivery; derive the topic and queue names yourself. Keep the messaging data path on a single thread of control with minimal threading: the publisher's main thread sends each order and waits for its acknowledgement, and the worker's main loop pulls each message and acknowledges it itself. No callbacks on the messaging data path.", + "Yes, I am happy with that design. No need to save it, go ahead.", + "Quickstart, against that Solace Cloud broker. The broker URL is plaintext (tcp://). I will not put credentials in the chat, so compile only and hand me the exact verify commands to run myself." + ], + "max_agent_turns": 80, + "graders": [ + { "type": "assistant_grep", "pattern": "Threading[*:`[:space:]]+Minimal", "expect": "present" }, + { "type": "assistant_grep", "pattern": "Quickstart", "fixed": true, "expect": "present", "turn": 2 }, + { "type": "file_exists", "glob": "guaranteed_publisher_blocking.py", "expect": "present" }, + { "type": "file_exists", "glob": "guaranteed_subscriber_blocking.py", "expect": "present" }, + { "type": "file_exists", "glob": "guaranteed_publisher.py", "expect": "absent" }, + { "type": "file_exists", "glob": "guaranteed_subscriber.py", "expect": "absent" }, + { "type": "file_exists", "glob": "solace_connection_config.py", "expect": "present" }, + { "type": "file_exists", "glob": "requirements.txt", "expect": "present", "count": 1 }, + { "type": "compile", "api": "python" }, + { "type": "release_match", "api": "python" }, + { "type": "live_verify", "api": "python" }, + { "type": "disclaimer", "api": "python" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.publish_await_acknowledgement\\(", "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.publish_await_acknowledgement\\([^)]*None", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_message\\(", "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_message\\([[:space:]]*\\)", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_message\\([^)]*None", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_async\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.set_message_publish_receipt_listener\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*time\\.sleep\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.with_(re)?connection_retry_strategy\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[[:space:]]*from [A-Za-z0-9_.]+ import \\*", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[[:space:]]*(import|from) multiprocessing", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: CONNECTED", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: QUEUE_BOUND", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: PUBLISH_ACKED", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: MESSAGE_RECEIVED", "fixed": true, "expect": "present" }, + { "type": "file_exists", "glob": "config.example.json", "expect": "present" }, + { "type": "file_grep", "glob": "config.example.json", "pattern": "tcp://HOST:55555", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": ".gitignore", "pattern": "config.json", "fixed": true, "expect": "present" }, + { "type": "file_exists", "glob": "verify.sh", "expect": "present", "identical_to": "plugins/solace-messaging-skills/skills/solace-application-development/references/python/scripts/verify.sh" }, + { "type": "file_exists", "glob": "verify-hooks.sh", "expect": "present" }, + { "type": "file_exists", "glob": "solace-verification-checklist.md", "expect": "present" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-guaranteed-publisher-blocking-sample\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-guaranteed-subscriber-blocking-sample\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-guaranteed-(publisher|subscriber)-sample\\.py", "expect": "absent" }, + { "type": "tool_use", "tool": "*", "input_pattern": "pypi\\.org/pypi/solace-pubsubplus/json", "expect": "present" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "py_compile", "expect": "present" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "verify\\.sh +(consumer|publisher|roundtrip|app|direct)", "ignore_heredoc_bodies": true, "expect": "absent" }, + { "type": "assistant_grep", "pattern": "./verify.sh", "fixed": true, "expect": "present" } + ] + }, + { + "id": "appdev-python-minimal-threading-direct-implement", + "skill": "solace-application-development", + "user_turns": [ + "Build a Python app with the Solace Python API for a plant-monitoring system: a telemetry publisher pushes temperature readings from the factory floor several times a second, and a live dashboard service consumes them to redraw a wallboard. A dropped reading makes no difference because the next one supersedes it, and nothing needs to be stored. Plain Python project, no web dashboard. We have a Solace Cloud broker. This is Direct Pub/Sub with DIRECT delivery; derive the topic yourself. Keep the dashboard's messaging path on a single thread of control with minimal threading: its main loop pulls the next reading itself instead of taking a callback.", + "Yes, I am happy with that design. No need to save it, go ahead.", + "Quickstart, against that Solace Cloud broker. The broker URL is plaintext (tcp://). I will not put credentials in the chat, so compile only and hand me the exact verify commands to run myself." + ], + "max_agent_turns": 80, + "graders": [ + { "type": "assistant_grep", "pattern": "Threading[*:`[:space:]]+Minimal", "expect": "present" }, + { "type": "assistant_grep", "pattern": "Pattern[*:`[:space:]]+Direct Pub/Sub", "expect": "present" }, + { "type": "assistant_grep", "pattern": "Quickstart", "fixed": true, "expect": "present", "turn": 2 }, + { "type": "file_exists", "glob": "direct_publisher.py", "expect": "present" }, + { "type": "file_exists", "glob": "direct_subscriber_blocking.py", "expect": "present" }, + { "type": "file_exists", "glob": "direct_subscriber.py", "expect": "absent" }, + { "type": "file_exists", "glob": "solace_connection_config.py", "expect": "present" }, + { "type": "file_exists", "glob": "requirements.txt", "expect": "present", "count": 1 }, + { "type": "compile", "api": "python" }, + { "type": "release_match", "api": "python" }, + { "type": "disclaimer", "api": "python" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_message\\(", "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_message\\([[:space:]]*\\)", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_message\\([^)]*None", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.receive_async\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.set_publish_failure_listener\\(", "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.publish_await_acknowledgement\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*create_persistent_message_(publisher|receiver)_builder\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*time\\.sleep\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*\\.with_(re)?connection_retry_strategy\\(", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[[:space:]]*from [A-Za-z0-9_.]+ import \\*", "expect": "absent" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: CONNECTED", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: SUBSCRIBED", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "VERIFY: MESSAGE_RECEIVED", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": "*.py", "pattern": "^[^#]*VERIFY: PUBLISH_ACKED", "expect": "absent" }, + { "type": "file_exists", "glob": "config.example.json", "expect": "present" }, + { "type": "file_grep", "glob": "config.example.json", "pattern": "tcp://HOST:55555", "fixed": true, "expect": "present" }, + { "type": "file_grep", "glob": ".gitignore", "pattern": "config.json", "fixed": true, "expect": "present" }, + { "type": "file_exists", "glob": "verify.sh", "expect": "present", "identical_to": "plugins/solace-messaging-skills/skills/solace-application-development/references/python/scripts/verify.sh" }, + { "type": "file_exists", "glob": "verify-hooks.sh", "expect": "present" }, + { "type": "file_exists", "glob": "solace-verification-checklist.md", "expect": "present" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-direct-publisher-sample\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-direct-subscriber-blocking-sample\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-direct-subscriber-sample\\.py", "expect": "absent" }, + { "type": "tool_use", "tool": "*", "input_pattern": "python-guaranteed-[a-z-]+\\.py", "expect": "absent" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "py_compile", "expect": "present" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "verify\\.sh +(consumer|publisher|roundtrip|app|direct)", "ignore_heredoc_bodies": true, "expect": "absent" }, + { "type": "assistant_grep", "pattern": "./verify.sh direct", "fixed": true, "expect": "present" } + ] + }, { "id": "feedback-email-before-confirm-negative", "skill": "solace-messaging-feedback", diff --git a/plugins/solace-messaging-skills/evals/trigger-evals.json b/plugins/solace-messaging-skills/evals/trigger-evals.json index bcc5107..7f7cf6f 100644 --- a/plugins/solace-messaging-skills/evals/trigger-evals.json +++ b/plugins/solace-messaging-skills/evals/trigger-evals.json @@ -14,6 +14,8 @@ {"skill":"solace-application-development","prompt":"We've standardized on the Solace JMS API. How do I create a JMS ConnectionFactory and a MessageProducer to send to a queue in Java?","should_trigger":false}, {"skill":"solace-application-development","prompt":"I'm writing my Solace client in Python with the solace-pubsubplus library. How do I set up the messaging service and a persistent receiver?","should_trigger":true}, {"skill":"solace-application-development","prompt":"We've got a warehouse-robotics system where two Python services communicate through our Solace event broker using the solace-pubsubplus library. A RobotDispatcher publishes task assignments and a RobotController consumes them and must not lose any while a robot reboots. How should I structure the messaging between these two Python services?","should_trigger":true,"must_pass":true}, + {"skill":"solace-application-development","prompt":"We're building a Python inventory service on the Solace Python API. Stock-level updates must not be lost while the service restarts. I want to keep the app single-threaded: no callbacks on the messaging path, the main loop should pull each message and acknowledge it itself, and the publisher should wait for each acknowledgement before it sends the next one. How should I structure this?","should_trigger":true}, + {"skill":"solace-application-development","prompt":"My Python worker uses a thread pool and the GIL is limiting it. Should I move the work to multiprocessing or asyncio, and how do I keep a single thread of control over the job queue?","should_trigger":false}, {"skill":"solace-application-development","prompt":"Show me how to publish to a Solace topic from C# using the .NET messaging API.","should_trigger":false}, {"skill":"solace-application-development","prompt":"I'm writing a Go service with the Solace PubSub+ Messaging API for Go. How do I build a persistent message publisher and handle publish receipts?","should_trigger":false}, {"skill":"solace-application-development","prompt":"I'm on the Solace Messaging API for Java (com.solace:solace-messaging-client). How do I set up a MessagingService and a PersistentMessageReceiver in my Java service to consume from a durable queue?","should_trigger":false}, diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python.md b/plugins/solace-messaging-skills/skills/solace-application-development/references/python.md index b546b85..4fcccee 100644 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python.md +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python.md @@ -23,7 +23,7 @@ If unclear, default to **Design**. Understand the messaging problem before gener Three gates hold on every path: -- **The design-contract gate.** Implement mode never starts without a design contract, and never invents design values. Exactly three sources satisfy it: (1) a summary Design mode confirmed in this session: use it as-is, with no re-confirm; (2) an explicit summary the developer supplied in chat (the eight fields or an equivalent statement of them): use it directly; (3) a saved `solace-design.md` in the project: restate it in one line and proceed on confirm. A prose build request that merely mentions pattern details is NOT a contract: route it through Design mode, whose fully-specified path derives the summary, echoes it, and takes one confirm. The one carve-out is the mechanical-edit row above. An experienced developer therefore never has to walk the design tree, but never skips the contract either. A request to skip the design, skip all questions, or generate the full project right now does not waive this gate or the door question below: write no file before the confirm. Take the shortest path the gate allows instead: derive the summary from the prompt, present it, say in one line that one confirm starts the build, and wait for that confirm. +- **The design-contract gate.** Implement mode never starts without a design contract, and never invents design values. Exactly three sources satisfy it: (1) a summary Design mode confirmed in this session: use it as-is, with no re-confirm; (2) an explicit summary the developer supplied in chat (the nine fields or an equivalent statement of them): use it directly; (3) a saved `solace-design.md` in the project: restate it in one line and proceed on confirm. A summary with no `Threading` field reads as `Threading: Async`; Implement mode derives it and does not ask. A prose build request that merely mentions pattern details is NOT a contract: route it through Design mode, whose fully-specified path derives the summary, echoes it, and takes one confirm. The one carve-out is the mechanical-edit row above. An experienced developer therefore never has to walk the design tree, but never skips the contract either. A request to skip the design, skip all questions, or generate the full project right now does not waive this gate or the door question below: write no file before the confirm. Take the shortest path the gate allows instead: derive the summary from the prompt, present it, say in one line that one confirm starts the build, and wait for that confirm. - **Implement mode opens with a mandatory door question.** Ask Quickstart, Solace Suggested, or Custom, before you request or accept ANY broker details (`python/implement-mode.md` Step 0). Ask it even when a `config.json` with credentials already exists, even when a local broker is already running, and even when the requested app is bigger than the canonical generated shape; none of those answers the question. A confirmed design summary does not answer it either: the door question always follows the design contract, never merges into it. - **Environment discovery never answers a question.** A running broker container, an existing config file, or found credentials are facts to report, not answers to consume. Report what you found, then still ask which broker the developer wants to target. @@ -74,8 +74,10 @@ Live exceptions (these have no `docs.solace.com` `.md` form; use the live URL di - `python/custom-mode.md`: the thin a la carte Custom overlay reached from implement-mode Step 0 on the Custom path; a leaf-aware checklist of the hardening knobs (secure session, DMQ, HA, decoupled projects, admin-provisioned queue) that generates exactly the ticked subset, reusing the Solace Suggested overlay steps for the mapped knobs. - `python/python-guaranteed-publisher-sample.py`: best-practices publisher sample (basic-auth connect, PERSISTENT publish to a topic, publish receipt handling, graceful shutdown). - `python/python-guaranteed-subscriber-sample.py`: best-practices receiver sample (provisions a durable queue plus a topic subscription, CLIENT-ack receiver). +- `python/python-guaranteed-publisher-blocking-sample.py` and `python/python-guaranteed-subscriber-blocking-sample.py`: the guaranteed pub/sub samples for the minimal-threading shape (`Threading: Minimal (blocking publish and receive)` in the design summary): blocking `publish_await_acknowledgement()` per message, and a main loop that pulls with `receive_message()` and ACKs on the application's own thread. - `python/python-solace-connection-config.py`: the shared connection-config helper every generated app uses. - `python/python-direct-publisher-sample.py` and `python/python-direct-subscriber-sample.py`: the direct pub/sub samples (bounded-buffer publish with a publish failure listener; direct topic subscription with discard detection). +- `python/python-direct-subscriber-blocking-sample.py`: the direct subscriber for the minimal-threading shape (a main loop that pulls with `receive_message()`); the direct publisher is the same sample on both shapes. - `python/python-direct-requestor-blocking-sample.py`, `python/python-direct-requestor-sample.py`, and `python/python-direct-replier-sample.py`: the direct request-reply samples (blocking and non-blocking requestor; replier answering with `replier.reply()`). - `python/verification-checklist.md`: the master verification checklist template that implement-mode Step 6 tailors into every generated project. - `python/scripts/verify.sh`: the bundled verify script that Implement mode copies into every generated project; the `app` stage drives web and embedded shapes through a generated `verify-hooks.sh`. diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/custom-mode.md b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/custom-mode.md index 2601432..cecbeeb 100644 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/custom-mode.md +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/custom-mode.md @@ -2,7 +2,7 @@ The Custom overlay for Implement mode of the Python API. This file is reached from `implement-mode.md` Step 0 when the developer chooses the Custom door. Custom is a la carte: the developer ticks which hardening knobs to include, and generation applies exactly the ticked subset and nothing more. It is a thin overlay. It does NOT re-describe how each knob works. For the four knobs that map to a `solace-suggested-mode.md` step, it points at that step and reuses it, so `solace-suggested-mode.md` stays the single source of truth for each hardening delta. -This overlay defers to `implement-mode.md` for all leaf and sample mechanics, exactly the way `solace-suggested-mode.md` does: the Step 2 design-summary input contract, the Step 4 leaf dispatch (which leaf wiring file and samples each leaf reads, and the near-verbatim adaptation discipline), the AI-assisted disclaimer header on every generated script, explicit imports (never `from solace.messaging... import *`), and the fixed `VERIFY:` marker contract. Custom changes NO `VERIFY:` marker string. Any live run on the Custom path prints the same one-line live-broker heads-up (SKILL.md Invariant 6) immediately before it starts: name the broker host and the run's actual broker-side effects. +This overlay defers to `implement-mode.md` for all leaf and sample mechanics, exactly the way `solace-suggested-mode.md` does: the Step 2 design-summary input contract, the Step 4 leaf dispatch (which leaf wiring file each leaf reads, which sample pair the `Threading` field selects, and the near-verbatim adaptation discipline), the AI-assisted disclaimer header on every generated script, explicit imports (never `from solace.messaging... import *`), and the fixed `VERIFY:` marker contract. Custom changes NO `VERIFY:` marker string. Any live run on the Custom path prints the same one-line live-broker heads-up (SKILL.md Invariant 6) immediately before it starts: name the broker host and the run's actual broker-side effects. ## The checklist model: one up-front multi-select @@ -50,7 +50,7 @@ On **Guaranteed Pub/Sub**, ticking the knob edits the subscriber's generation. D KEEP the rest of the subscriber unchanged: - The local `Queue.durable_exclusive_queue(queue_name)` or `Queue.durable_non_exclusive_queue(queue_name)` handle from the `Access type` field (the receiver still needs a queue object to bind to; the access type itself is now the administrator's broker setting, so state in a comment that it must match). -- `with_message_client_acknowledgement()`, `with_activation_passivation_support(...)`, `receive_async(...)`, `start()`, and `ack()` after processing. +- `with_message_client_acknowledgement()`, `with_activation_passivation_support(...)`, `receive_async(...)` (or the `receive_message(...)` loop on the minimal-threading shape), `start()`, and `ack()` after processing. - The sample's `PubSubPlusClientError` branch around `start()`: it now reports a queue the administrator has not provisioned. All of the Guaranteed Pub/Sub leaf's `VERIFY:` markers still emit, unchanged: `VERIFY: CONNECTED` after connect, `VERIFY: QUEUE_BOUND` after `start()`, and `VERIFY: MESSAGE_RECEIVED` after `ack()`. The publisher is untouched, so its `VERIFY: PUBLISH_ACKED` is unaffected. On the fan-out leaf, the drop applies to every subscriber script. diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/design-mode.md b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/design-mode.md index f3caa2b..3cc5890 100644 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/design-mode.md +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/design-mode.md @@ -23,6 +23,7 @@ Design mode is ask-or-recommend, not interrogate-first. Before walking the tree, | "one active consumer", "strict ordering", "only one worker at a time" | Guaranteed Pub/Sub (single service, exclusive) | | "fire-and-forget telemetry", "high-rate metrics, occasional loss is fine", "live ticks, no persistence" | Direct Pub/Sub | | "must not lose messages", "survive consumer downtime", "persistent delivery" | a Guaranteed leaf (let the remaining questions pick which) | +| "keep it single-threaded", "no callbacks on the data path", "minimal threading", "poll for messages", "pull each message and ack it myself", "one thread of control" | lock the `Threading` field to `Minimal (blocking publish and receive)` (the pattern questions still apply; Request-Reply (Direct) records `n/a` instead). A prompt that states callbacks, handler threads, or a multi-threaded app locks it to `Async` the same way. | If a row matches, or if your own judgment maps an unmatched prompt cleanly to one of the five leaves, take the recommend path. Do not default to a full interrogation just because the prompt is not a verbatim table row; fall back to your own judgment to pick the closest leaf, then confirm. @@ -32,7 +33,7 @@ If a row matches, or if your own judgment maps an unmatched prompt cleanly to on **Partial-intent path (intent is partial).** When the prompt answers some branches but not all, lock the branches the prompt already answers and ask ONLY the remaining tree questions, in the Step 2 order, skipping every question the prompt already settled. Reach a leaf, then confirm it the same one-line way. -**Unclear path (no stated intent).** When the prompt states no usable intent, walk all four tree questions in Step 2 order to reach a leaf. +**Unclear path (no stated intent).** When the prompt states no usable intent, walk all five tree questions in Step 2 order to reach a leaf. **Variant of a leaf (the prompt states a requirement the base shape does not meet).** A leaf is a pattern FAMILY, not a fixed topology. Alongside the intent match, read the prompt for reliability or topology requirements that the chosen leaf's DEFAULT shape does not satisfy (for example "the consumer must dedupe redeliveries"). When you find one, the leaf still resolves to one of the five strings verbatim, but the design is a VARIANT of that leaf: its default sample shape is adapted to meet the requirement. Name the deviation in the same one-line confirm and carry the actual topology into the Step 4 summary. Keep a variant within the same pattern family and to topology or reliability knobs the canonical docs ground; if a requirement leaves the Solace topic and queue publish and subscribe model, or needs behavior the docs cannot ground, say so and treat it as off-catalog rather than inventing it. @@ -42,12 +43,18 @@ Ask only the questions the prompt left open, in this order. Before you answer ea 1. **Interaction shape: request-reply or publish/subscribe?** Grounded in [Message Exchange Patterns](https://docs.solace.com/Get-Started/message-exchange-patterns.md). Decision: a request that expects a correlated reply is Request-Reply; an event fanned out to one or many independent consumers is publish/subscribe. 2. **Delivery guarantee: direct or guaranteed?** This ONE question applies to both interaction shapes, so ask it next regardless of the answer to question 1. Grounded in [Message Delivery Modes](https://docs.solace.com/Get-Started/message-delivery-modes.md). Decision: direct is fire-and-forget with no broker-side persistence, for high-rate flows that tolerate occasional loss; guaranteed (persistent) is persisted and survives consumer downtime. -3. **Number of consuming services (guaranteed publish/subscribe branch only).** Decision: a single consuming service is one logical consumer of the stream; fan-out is several independent services that each need their own full copy of every event. -4. **Competing consumers (guaranteed publish/subscribe, single service branch only).** Grounded in [Topic Endpoints and Queues](https://docs.solace.com/Get-Started/topic-endpoints-queues.md). Decision: exclusive gives one active consumer at a time (strict ordering, hot standby); non-exclusive lets multiple competing workers share the queue and split the load. +3. **Threading on the messaging data path: multi-threaded, or minimal threading? (Direct Pub/Sub and guaranteed publish/subscribe branches only.)** Grounded in [Consuming Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Receive.md) on the guaranteed branch and in [Consuming Direct Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-DM-Receive.md) on the direct branch; both pages state that `receive_message(timeout)` blocks the routine until a message arrives, the timeout passes, or a service interruption occurs, and the first states that `receive_async()` invokes the callback on a new Python thread for every message. Frame the choice as WHERE the application's thread of control sits on the messaging data path, never as speed: make no throughput claim for either answer. The reason the question exists, stated in one line when you ask it: in Python the GIL lets only one thread run Python code at a time, so the extra API threads of the callback shape buy less than they do in other languages, and a developer who wants one thread of control can have it. Three answers: + - **Multi-threaded app** (the push model): the API pushes each received message, and on the guaranteed branch each publish receipt, to an application callback on an API thread, so the application manages the concurrency between its own threads and the API's threads. This is the async shape and the current default. Record `Threading: Async`. + - **Minimal threading preferred** (the pull model): the application's own thread pulls each message with a blocking `receive_message(timeout)` call, and on the guaranteed branch publishes each message with a blocking `publish_await_acknowledgement()` call, so no application callback runs on the data path and no application state is shared with an API thread; only the service event listeners still run on API threads. The cost is one message in flight at a time on each side. This is the blocking shape. Record `Threading: Minimal (blocking publish and receive)`. + - **Unsure**: the async shape. Record `Threading: Async`. -Questions 3 and 4 apply ONLY on the guaranteed publish/subscribe branch. Request-reply and direct publish/subscribe resolve at the delivery-guarantee answer and never reach them. + A prompt that already states either answer (the threading row in the Step 1 table) skips this question. Request-Reply (Direct) never reaches it: its base shape already blocks on the reply, and its summary records `Threading: n/a`. +4. **Number of consuming services (guaranteed publish/subscribe branch only).** Decision: a single consuming service is one logical consumer of the stream; fan-out is several independent services that each need their own full copy of every event. +5. **Competing consumers (guaranteed publish/subscribe, single service branch only).** Grounded in [Topic Endpoints and Queues](https://docs.solace.com/Get-Started/topic-endpoints-queues.md). Decision: exclusive gives one active consumer at a time (strict ordering, hot standby); non-exclusive lets multiple competing workers share the queue and split the load. -**No guaranteed request-reply in the Python API.** When question 1 resolves to Request-Reply and question 2 resolves to guaranteed (the request must be persisted), there is no Python request-reply leaf for it. WebFetch [Request-Reply Messaging in the Solace Python API](https://docs.solace.com/API/API-Developer-Guide-Python/Python-API-Request-Reply.md) and quote its note that request-reply messaging can only be used with direct messages in the Python API. Then route to Guaranteed Pub/Sub, never to Request-Reply (Direct): the request travels as a persistent message to a durable queue, and the application sets a correlation id on it and matches the answer by that id. Continue the tree at question 3 to pick the Guaranteed Pub/Sub leaf, then record the design as a VARIANT of that leaf (Step 1): name the application correlation id in the `Consumption endpoint` field with a short inline reason, for example `durable queue q.acme.orders + topic subscription; application correlation id (persisted request, no Python guaranteed request-reply)`. Tell the developer that Implement generates the request leg only: the answer path (how the consumer returns the answer and how the requestor matches it by the correlation id) is theirs to build, and the checklist records it as developer-owned. Never offer Request-Reply (Direct) for a request that must be persisted, never invent a guaranteed request-reply leaf, and never record `Request-Reply (Guaranteed)` in a Python summary. +Question 3 applies on the Direct Pub/Sub and the guaranteed publish/subscribe branches. Questions 4 and 5 apply ONLY on the guaranteed publish/subscribe branch. Request-reply resolves at the delivery-guarantee answer and never reaches questions 3 to 5; direct publish/subscribe resolves after question 3. + +**No guaranteed request-reply in the Python API.** When question 1 resolves to Request-Reply and question 2 resolves to guaranteed (the request must be persisted), there is no Python request-reply leaf for it. WebFetch [Request-Reply Messaging in the Solace Python API](https://docs.solace.com/API/API-Developer-Guide-Python/Python-API-Request-Reply.md) and quote its note that request-reply messaging can only be used with direct messages in the Python API. Then route to Guaranteed Pub/Sub, never to Request-Reply (Direct): the request travels as a persistent message to a durable queue, and the application sets a correlation id on it and matches the answer by that id. Continue the tree at question 3 (the threading question, then the two consumer questions) to pick the Guaranteed Pub/Sub leaf, then record the design as a VARIANT of that leaf (Step 1): name the application correlation id in the `Consumption endpoint` field with a short inline reason, for example `durable queue q.acme.orders + topic subscription; application correlation id (persisted request, no Python guaranteed request-reply)`. Tell the developer that Implement generates the request leg only: the answer path (how the consumer returns the answer and how the requestor matches it by the correlation id) is theirs to build, and the checklist records it as developer-owned. Never offer Request-Reply (Direct) for a request that must be persisted, never invent a guaranteed request-reply leaf, and never record `Request-Reply (Guaranteed)` in a Python summary. ## Step 3: Resolve to one of the five leaves @@ -75,7 +82,7 @@ Inside the guaranteed publish/subscribe single-service leaf, the exclusive versu ## Step 4: Emit the design summary -Fill in this ONE unified design summary every run, in chat, with the confirmed values. The same eight fields appear every run regardless of which leaf the tree reached, so Implement mode reads one stable structure. Use explicit `n/a` for any field the chosen pattern does not apply. The title names the API, so a saved `solace-design.md` routes back to the Python entry file (the saved-design exception in SKILL.md API Selection). +Fill in this ONE unified design summary every run, in chat, with the confirmed values. The same nine fields appear every run regardless of which leaf the tree reached, so Implement mode reads one stable structure. Use explicit `n/a` for any field the chosen pattern does not apply. The title names the API, so a saved `solace-design.md` routes back to the Python entry file (the saved-design exception in SKILL.md API Selection). ``` Solace Python Design Summary @@ -84,12 +91,13 @@ Solace Python Design Summary - Access type: - Topic: - Consumption endpoint: +- Threading: - Auth: Basic username/password - Broker: Solace Cloud - Grounding docs: ``` -The eight fields are fixed: Pattern, Delivery, Access type, Topic, Consumption endpoint, Auth, Broker, Grounding docs. Formatting is at your discretion, but always present all eight, every run, with explicit `n/a` where a field does not apply. The `Pattern` value stays one of the five leaf strings verbatim even for a variant; the deviation lives in the topology fields, never in a new leaf string. +The nine fields are fixed: Pattern, Delivery, Access type, Topic, Consumption endpoint, Threading, Auth, Broker, Grounding docs. Formatting is at your discretion, but always present all nine, every run, with explicit `n/a` where a field does not apply. The JCSMP summary keeps its own eight fields; the two summaries route by their title and are no longer the same shape. The `Pattern` value stays one of the five leaf strings verbatim even for a variant; the deviation lives in the topology fields, never in a new leaf string. Field semantics: @@ -98,6 +106,7 @@ Field semantics: - **Access type.** `Exclusive` or `Non-exclusive` for the two guaranteed single-service leaves; `n/a` for every other leaf. Surface it explicitly even though it is also encoded in the Pattern string. - **Consumption endpoint.** The pattern-specific place the consuming side reads from. Record the ACTUAL topology the design requires; the defaults are `direct topic subscription` for Direct Pub/Sub, `durable queue + topic subscription` for the three guaranteed leaves, and `API-managed reply (RequestReplyMessagePublisher)` for Request-Reply (Direct). For the durable-queue leaves, include the auto-derived queue name (for example `durable queue q.acme.orders + topic subscription`) so Implement mode reads the concrete queue name here. For a variant, record the variant topology here with a short inline reason. - **Topic.** The recommended topic string or hierarchy, derived by applying the solace-topic-best-practices skill to the use case. +- **Threading.** `Async` or `Minimal (blocking publish and receive)` on the Direct Pub/Sub and Guaranteed Pub/Sub leaves, from question 3; `n/a` for Request-Reply (Direct). Spell the value verbatim: it is the contract Implement mode reads to pick the sample pair. `Async` selects the receive-callback and publish-receipt samples. `Minimal (blocking publish and receive)` selects the blocking samples: `receive_message()` on both pub/sub branches, plus `publish_await_acknowledgement()` on the guaranteed branch (the direct publisher has no receipt to await, so it is the same script on both answers). A summary with no `Threading` line reads as `Async` (a summary typed in chat, or a `solace-design.md` saved before the field existed). - **Auth.** Basic username/password. - **Broker.** Default to `Solace Cloud` and record it without asking. Switch to another type (Software Broker or Appliance) ONLY if the developer explicitly insists. - **Grounding docs.** ONLY the pages this session actually WebFetched. Never list a page you did not fetch. Write `none fetched` when no page was fetched. diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/evals/sample-check/check-samples.sh b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/evals/sample-check/check-samples.sh index 766bbc4..3af6d94 100755 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/evals/sample-check/check-samples.sh +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/evals/sample-check/check-samples.sh @@ -88,4 +88,34 @@ echo "Building the API objects against tcp://localhost:55555 without a broker... bash -n "${REFERENCES_DIR}/scripts/verify.sh" echo "PASS: scripts/verify.sh parses" +# (7) The bundled verify script must resolve the fixed script names of both threading +# shapes (implement-mode.md Step 4) without a broker: with only the named scripts +# present and no .venv, each stage must get past script resolution to its +# virtual-environment check, and with no script present it must name the default. +echo "Checking that scripts/verify.sh resolves the fixed script names..." +verify_stderr() { # : verify.sh's stderr with connection arguments and no .venv + ( cd "$1" && bash "${REFERENCES_DIR}/scripts/verify.sh" "$2" tcp://localhost:55555 vpn user 2>&1 >/dev/null ) || true +} +expect_resolved() { #