diff --git a/plugins/solace-messaging-skills/evals/README.md b/plugins/solace-messaging-skills/evals/README.md index 35b0293..9d4c61e 100644 --- a/plugins/solace-messaging-skills/evals/README.md +++ b/plugins/solace-messaging-skills/evals/README.md @@ -37,7 +37,7 @@ Each `turnN.jsonl` in the work directory is the raw `--output-format stream-json | Grader `turn` | Selects which `turn.jsonl` file the grader reads. It is not a field inside the file. | | `skill` (the implicit skill-fired gate) | An `assistant` event whose `message.content[]` holds a `tool_use` block with `name` `Skill`. Its `input.skill` is `plugin:skill-name`; the runner strips the prefix. | | `assistant_grep` | `assistant` events, `message.content[]` blocks with `type` `text`. Thinking blocks and tool payloads are excluded. | -| `tool_use` | `assistant` events, blocks with `type` `tool_use`. The runner matches `name` and greps the compact JSON of `input`. | +| `tool_use` | `assistant` events, blocks with `type` `tool_use`. The runner matches `name` and greps the compact JSON of `input`, minus a Bash call's `description` field (narration, not the command). | | `llm_judge` with `webfetch_urls` or `webfetch_results` | WebFetch `tool_use` blocks supply the URLs. `user` events whose `tool_result.tool_use_id` matches a WebFetch block supply the fetched content. | | Session continuity (`--resume`) | `session_id` on the `result` event, with the `init` event as the fallback. | | INFRA verdict for a turn | A missing `result` event, or `result.is_error` true with a `subtype` other than `error_max_turns`. | diff --git a/plugins/solace-messaging-skills/evals/output-evals.json b/plugins/solace-messaging-skills/evals/output-evals.json index 7d910a8..ad631a4 100644 --- a/plugins/solace-messaging-skills/evals/output-evals.json +++ b/plugins/solace-messaging-skills/evals/output-evals.json @@ -89,7 +89,7 @@ { "type": "tool_use", "tool": "*", "input_pattern": "repo1\\.maven\\.org/maven2/com/solacesystems/sol-jcsmp/maven-metadata\\.xml", "expect": "present" }, { "type": "tool_use", "tool": "Bash", "input_pattern": "solrsearch|search\\.maven\\.org", "expect": "absent" }, { "type": "tool_use", "tool": "WebFetch", "input_pattern": "solrsearch|search\\.maven\\.org", "expect": "absent" }, - { "type": "tool_use", "tool": "Bash", "input_pattern": "verify\\.sh +(consumer|publisher|roundtrip|app)", "ignore_heredoc_bodies": true, "expect": "absent" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "(\"command\":\"|[^\"'`#\\\\])(\\./|(ba)?sh )verify\\.sh +(consumer|publisher|roundtrip|app)", "ignore_heredoc_bodies": true, "expect": "absent" }, { "type": "assistant_grep", "pattern": "./verify.sh", "fixed": true, "expect": "present" } ] }, @@ -132,7 +132,7 @@ { "type": "compile" }, { "type": "file_grep", "glob": "*.java", "pattern": "VERIFY: REQUEST_RECEIVED", "fixed": true, "expect": "present" }, { "type": "file_grep", "glob": "*.java", "pattern": "VERIFY: REPLY_RECEIVED", "fixed": true, "expect": "present" }, - { "type": "tool_use", "tool": "Bash", "input_pattern": "verify\\.sh +(consumer|publisher|roundtrip|app|direct|guaranteed-request-reply|direct-request-reply)", "ignore_heredoc_bodies": true, "expect": "absent" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "(\"command\":\"|[^\"'`#\\\\])(\\./|(ba)?sh )verify\\.sh +(consumer|publisher|roundtrip|app|direct|guaranteed-request-reply|direct-request-reply)", "ignore_heredoc_bodies": true, "expect": "absent" }, { "type": "assistant_grep", "pattern": "verify.sh", "fixed": true, "expect": "present" } ] }, @@ -146,11 +146,14 @@ "max_agent_turns": 25, "graders": [ { "type": "assistant_grep", "pattern": "[Bb]roker", "expect": "present", "turn": 1 }, + { "type": "assistant_grep", "pattern": "[Mm]ulti-threaded|[Mm]inimal threading", "expect": "present", "turn": 1 }, { "type": "assistant_grep", "pattern": "Pattern\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Delivery\\**:", "expect": "present" }, { "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": "Threading[*:`[:space:]]+Async", "expect": "present" }, { "type": "assistant_grep", "pattern": "Auth\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Broker\\**:", "expect": "present" }, { "type": "assistant_grep", "pattern": "Grounding docs\\**:", "expect": "present" }, @@ -178,7 +181,8 @@ { "type": "tool_use", "tool": "Write", "expect": "absent" }, { "type": "tool_use", "tool": "Edit", "expect": "absent" }, { "type": "file_exists", "glob": "*", "expect": "absent" }, - { "type": "assistant_grep", "pattern": "[Dd]esign|Pattern:", "expect": "present" } + { "type": "assistant_grep", "pattern": "[Dd]esign|Pattern:", "expect": "present" }, + { "type": "assistant_grep", "pattern": "Threading[*:`[:space:]]+Async", "expect": "present" } ] }, { @@ -187,14 +191,19 @@ "must_pass": true, "user_turns": [ "Build a Python app with the Solace Python API (solace-pubsubplus) for food-delivery order processing. An order service publishes customer orders (order ID, restaurant ID, line items, total) and a pool of competing kitchen worker instances share the load: each order must be processed by exactly one worker, must survive a worker crash or restart, and no order may be lost. Plain Python project, no web dashboard. We have a Solace Cloud broker. This is Guaranteed Pub/Sub (single service, non-exclusive) with PERSISTENT delivery; derive the topic and queue names yourself.", + "A multi-threaded app is fine.", "Yes, I am happy with that design. Save it to solace-design.md, then 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": "Quickstart", "fixed": true, "expect": "present", "turn": 2 }, - { "type": "assistant_grep", "pattern": "Solace Suggested", "fixed": true, "expect": "present", "turn": 2 }, - { "type": "assistant_grep", "pattern": "Custom", "fixed": true, "expect": "present", "turn": 2 }, + { "type": "tool_use", "tool": "*", "input_pattern": "src/solace_connection_config\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "Write", "input_pattern": "\"file_path\": *\"[^\"]*\\.py\"", "exclude_pattern": "\"file_path\": *\"[^\"]*/src/[^\"/]+\\.py\"", "expect": "absent" }, + { "type": "assistant_grep", "pattern": "[Mm]ulti-threaded|[Mm]inimal threading", "expect": "present", "turn": 1 }, + { "type": "assistant_grep", "pattern": "Threading[*:`[:space:]]+Async", "expect": "present" }, + { "type": "assistant_grep", "pattern": "Quickstart", "fixed": true, "expect": "present", "turn": 3 }, + { "type": "assistant_grep", "pattern": "Solace Suggested", "fixed": true, "expect": "present", "turn": 3 }, + { "type": "assistant_grep", "pattern": "Custom", "fixed": true, "expect": "present", "turn": 3 }, { "type": "file_exists", "glob": "solace-design.md", "expect": "present" }, { "type": "file_exists", "glob": "requirements.txt", "expect": "present", "count": 1 }, { "type": "file_exists", "glob": "guaranteed_publisher.py", "expect": "present" }, @@ -221,16 +230,16 @@ { "type": "file_grep", "glob": ".gitignore", "pattern": ".venv", "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_grep", "glob": "verify-hooks.sh", "pattern": "^START_CMD=", "expect": "present" }, - { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^TRIGGER_CMD=", "expect": "present" }, + { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^START_CMD=.*src/guaranteed_subscriber\\.py", "expect": "present" }, + { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^TRIGGER_CMD=.*src/guaranteed_publisher\\.py", "expect": "present" }, { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^READY_MARKER=", "expect": "present" }, { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^PASS_MARKER=", "expect": "present" }, { "type": "file_exists", "glob": "solace-verification-checklist.md", "expect": "present" }, { "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": "tool_use", "tool": "Bash", "input_pattern": "(\"command\":\"|[^\"'`#\\\\])(\\./|(ba)?sh )verify\\.sh +(consumer|publisher|roundtrip|app|direct)", "ignore_heredoc_bodies": true, "expect": "absent" }, { "type": "assistant_grep", "pattern": "./verify.sh", "fixed": true, "expect": "present" }, - { "type": "assistant_grep", "pattern": "[Tt]rust[ -]?store", "expect": "present", "turn": 3 } + { "type": "assistant_grep", "pattern": "[Tt]rust[ -]?store", "expect": "present", "turn": 4 } ] }, { @@ -256,13 +265,18 @@ "skill": "solace-application-development", "user_turns": [ "Build a Python app with the Solace Python API (solace-pubsubplus) 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. Plain Python, no dashboard. We have a TLS-enabled Solace Cloud broker. This is Guaranteed Pub/Sub (single service, exclusive) with PERSISTENT delivery; derive the topic and queue names yourself.", + "I am not sure, go with the default.", "Yes, I am happy with that design. No need to save it, go ahead.", "Solace Suggested, against that Solace Cloud broker.", "No HA failover needed. I will put the broker's CA certificate into a trust store directory myself. I will not share credentials in the chat; compile only and hand me the commands to run myself." ], "max_agent_turns": 80, "graders": [ - { "type": "assistant_grep", "pattern": "[Ff]ailover", "expect": "present", "turn": 3 }, + { "type": "tool_use", "tool": "*", "input_pattern": "src/solace_connection_config\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "Write", "input_pattern": "\"file_path\": *\"[^\"]*\\.py\"", "exclude_pattern": "\"file_path\": *\"[^\"]*/src/[^\"/]+\\.py\"", "expect": "absent" }, + { "type": "assistant_grep", "pattern": "[Mm]ulti-threaded|[Mm]inimal threading", "expect": "present", "turn": 1 }, + { "type": "assistant_grep", "pattern": "Threading[*:`[:space:]]+Async", "expect": "present" }, + { "type": "assistant_grep", "pattern": "[Ff]ailover", "expect": "present", "turn": 4 }, { "type": "assistant_grep", "pattern": "ght_client_certs.md", "fixed": true, "expect": "present" }, { "type": "disclaimer", "api": "python" }, { "type": "file_exists", "glob": "requirements.txt", "expect": "present", "count": 2 }, @@ -282,7 +296,7 @@ { "type": "file_grep", "glob": ".gitignore", "pattern": "config.json", "fixed": true, "expect": "all_files" }, { "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": "tool_use", "tool": "Bash", "input_pattern": "verify\\.sh +(consumer|publisher|roundtrip|app|direct)", "ignore_heredoc_bodies": true, "expect": "absent" }, + { "type": "tool_use", "tool": "Bash", "input_pattern": "(\"command\":\"|[^\"'`#\\\\])(\\./|(ba)?sh )verify\\.sh +(consumer|publisher|roundtrip|app|direct)", "ignore_heredoc_bodies": true, "expect": "absent" }, { "type": "assistant_grep", "pattern": "--subscriber-dir", "fixed": true, "expect": "present" } ] }, @@ -297,12 +311,124 @@ "graders": [ { "type": "tool_use", "tool": "WebFetch", "input_pattern": "Python-API-Request-Reply\\.md", "expect": "present" }, { "type": "assistant_grep", "pattern": "Pattern[*:`[:space:]]+Guaranteed Pub/Sub \\(", "expect": "present" }, + { "type": "assistant_grep", "pattern": "[Mm]ulti-threaded|[Mm]inimal threading", "expect": "present", "turn": 1 }, + { "type": "assistant_grep", "pattern": "Threading[*:`[:space:]]+Async", "expect": "present" }, { "type": "assistant_grep", "pattern": "Pattern[*:`[:space:]]+Request-Reply", "expect": "absent" }, { "type": "assistant_grep", "pattern": "[Cc]orrelation", "expect": "present" }, { "type": "tool_use", "tool": "Write", "expect": "absent" }, { "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": "tool_use", "tool": "*", "input_pattern": "src/solace_connection_config\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "Write", "input_pattern": "\"file_path\": *\"[^\"]*\\.py\"", "exclude_pattern": "\"file_path\": *\"[^\"]*/src/[^\"/]+\\.py\"", "expect": "absent" }, + { "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_grep", "glob": "verify-hooks.sh", "pattern": "^START_CMD=.*src/guaranteed_subscriber_blocking\\.py", "expect": "present" }, + { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^TRIGGER_CMD=.*src/guaranteed_publisher_blocking\\.py", "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": "(\"command\":\"|[^\"'`#\\\\])(\\./|(ba)?sh )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": "tool_use", "tool": "*", "input_pattern": "src/solace_connection_config\\.py", "expect": "present" }, + { "type": "tool_use", "tool": "Write", "input_pattern": "\"file_path\": *\"[^\"]*\\.py\"", "exclude_pattern": "\"file_path\": *\"[^\"]*/src/[^\"/]+\\.py\"", "expect": "absent" }, + { "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_grep", "glob": "verify-hooks.sh", "pattern": "^START_CMD=.*src/direct_subscriber_blocking\\.py", "expect": "present" }, + { "type": "file_grep", "glob": "verify-hooks.sh", "pattern": "^TRIGGER_CMD=.*src/direct_publisher\\.py", "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": "(\"command\":\"|[^\"'`#\\\\])(\\./|(ba)?sh )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..b774b00 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 (every field of the summary 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 (an unstated `Threading` records `Async`, named as the default; `python/design-mode.md` question 3), 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..ca713bb 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,16 +23,19 @@ 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 (the requestor blocks on each reply; the replier answers from a receive_async() callback)` instead, so the developer sees that the leaf meets the preference on the requestor side only). 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. **Recommend path (intent is clear).** Recommend the leaf in ONE line and confirm, for example "Sounds like Guaranteed Pub/Sub (fan-out), confirm?", then proceed on the developer's confirm. One line plus one confirm, not a rationale paragraph and not a silent zero-confirm jump. -**Fully-specified path (the prompt answers every applicable tree question).** When the prompt resolves the leaf AND answers every Step 2 question that applies to that leaf, skip the separate leaf confirm. Derive the full summary, present it (Step 4), and let the close-out question carry the single confirm: one confirm covers the leaf and the design. Do not ask a leaf confirm and then a close-out confirm; that is two questions where the prompt earned one. +**Threading is never inferred.** Question 3 (Step 2) applies to every Direct Pub/Sub and guaranteed publish/subscribe design. Only an explicit statement in the prompt (the threading row above) answers it; a use case, a rate, or a resolved leaf does not. A prompt that settles everything except threading is on the partial-intent path, so ask question 3 even when the leaf is clear: ask it in ONE message that also names the resolved leaf in one line, and take the developer's answer as the leaf confirm too, then present the summary (Step 4). The one exception is a request to skip all questions (the design-contract gate in `python.md`): record `Threading: Async` and say in the summary that it is the default. + +**Fully-specified path (the prompt answers every applicable tree question).** When the prompt resolves the leaf AND answers every Step 2 question that applies to that leaf, including the threading question, skip the separate leaf confirm. Derive the full summary, present it (Step 4), and let the close-out question carry the single confirm: one confirm covers the leaf and the design. Do not ask a leaf confirm and then a close-out confirm; that is two questions where the prompt earned one. **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 +45,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. Name the two answers with the labels written here, multi-threaded and minimal threading, whatever words the framing adds: the developer's reply maps onto those labels. 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`. + + A prompt that already states either answer (the threading row in the Step 1 table) skips this question. Otherwise ALWAYS ask it, with the one skip-all-questions exception in Step 1: never infer the answer from the use case and never record `Async` without asking. Request-Reply (Direct) never reaches it: its base shape already blocks on the reply, and its summary records `Threading: n/a`. When the prompt stated a minimal-threading preference, the line reads `Threading: n/a (the requestor blocks on each reply; the replier answers from a receive_async() callback)`, so the developer sees that the leaf does not meet the preference on the replier side; a blocking replier is not a shape this skill generates. +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. -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. +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 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. +**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 +84,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 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 +93,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 fields are fixed: Pattern, Delivery, Access type, Topic, Consumption endpoint, Threading, Auth, Broker, Grounding docs. Formatting is at your discretion, but always present every field in that list, every run, with explicit `n/a` where a field does not apply. The JCSMP summary keeps its own, shorter field list; 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 +108,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..9899894 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,43 @@ 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. Single-project mode stops at the +# long-running role's missing .venv before it looks at the foreground role, so the +# fixture uses separate-projects mode with a stub interpreter in the long-running +# role's directory: with both scripts present under src/ the stage must get past both +# names to the foreground role's virtual-environment check, and with either script +# missing it must name that script in that directory. +echo "Checking that scripts/verify.sh resolves the fixed script names..." +verify_stderr() { # : verify.sh's stderr in separate-projects mode, no broker + ( bash "${REFERENCES_DIR}/scripts/verify.sh" "$1" --subscriber-dir "$2" --publisher-dir "$3" 2>&1 >/dev/null ) || true +} +make_roles() { # sets SUB and PUB to fresh role directories; SUB carries a stub interpreter + SUB="$(mktemp -d "${WORK_DIR}/sub.XXXXXX")"; PUB="$(mktemp -d "${WORK_DIR}/pub.XXXXXX")" + mkdir -p "${SUB}/src" "${SUB}/.venv/bin" "${PUB}/src" + : > "${SUB}/.venv/bin/python"; chmod +x "${SUB}/.venv/bin/python" +} +expect_resolved() { # : the stage resolves both names + make_roles; : > "${SUB}/src/$2"; : > "${PUB}/src/$3" + grep -qF "no virtual environment in: $(cd "${PUB}" && pwd)" <<<"$(verify_stderr "$1" "${SUB}" "${PUB}")" \ + || { echo "ERROR: verify.sh $1 did not resolve: $2 $3" >&2; exit 1; } +} +expect_missing() { # : the stage names the absent role's default + make_roles; [ "$2" = - ] || : > "${SUB}/src/$2"; [ "$3" = - ] || : > "${PUB}/src/$3" + local dir; if [ "$4" = sub ]; then dir="${SUB}"; else dir="${PUB}"; fi + grep -qF "no src/$5 found in: $(cd "${dir}" && pwd)" <<<"$(verify_stderr "$1" "${SUB}" "${PUB}")" \ + || { echo "ERROR: verify.sh $1 did not report src/$5 as missing in the $4 directory" >&2; exit 1; } +} +expect_resolved consumer guaranteed_subscriber.py guaranteed_publisher.py +expect_resolved roundtrip guaranteed_subscriber_blocking.py guaranteed_publisher_blocking.py +expect_resolved direct direct_subscriber.py direct_publisher.py +expect_resolved direct direct_subscriber_blocking.py direct_publisher.py +expect_resolved direct-request-reply direct_replier.py direct_requestor_blocking.py +expect_resolved direct-request-reply direct_replier.py direct_requestor.py +expect_missing consumer - guaranteed_publisher.py sub guaranteed_subscriber.py +expect_missing roundtrip guaranteed_subscriber.py - pub guaranteed_publisher.py +expect_missing direct - direct_publisher.py sub direct_subscriber.py +expect_missing direct-request-reply direct_replier.py - pub direct_requestor_blocking.py +echo "PASS: scripts/verify.sh resolves the fixed script names of both threading shapes" + echo "PASS: the baked Python samples against solace-pubsubplus ${VERSION}" diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-direct-pubsub.md b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-direct-pubsub.md index b2dc851..f479d64 100644 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-direct-pubsub.md +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-direct-pubsub.md @@ -4,25 +4,26 @@ The per-leaf wiring for the Direct Pub/Sub leaf of the Python API. Read this fil ## Direct pub/sub flow -Generation reads the direct pub/sub sample pair and adapts each into its own generated script, the same near-verbatim discipline the guaranteed flow uses: `direct_publisher.py` is a near-verbatim adaptation of the publisher sample, and `direct_subscriber.py` is a near-verbatim adaptation of the subscriber sample. Direct messaging is at-most-once: there is no broker receipt and no redelivery, so this leaf has NO queue, NO provisioning, NO client acknowledgement, and NO publish receipt listener. +Generation reads the direct publisher sample and ONE direct subscriber sample, picked by the design summary's `Threading` field (`implement-mode.md` Step 4), and adapts each into its own generated script, the same near-verbatim discipline the guaranteed flow uses: `direct_publisher.py` is a near-verbatim adaptation of the publisher sample, and `direct_subscriber.py` (or `direct_subscriber_blocking.py` on the blocking shape) is a near-verbatim adaptation of the chosen subscriber sample. Read ONLY the subscriber sample the field selects. Direct messaging is at-most-once: there is no broker receipt and no redelivery, so this leaf has NO queue, NO provisioning, NO client acknowledgement, and NO publish receipt listener. -- `python-direct-publisher-sample.py`: basic-auth connect, a continuous DIRECT binary publish loop with a rotating payload to a dynamic topic, a publish failure listener for the asynchronous failures the API reports, capacity-bounded back pressure, service event listeners registered before `connect()`, and a graceful SIGINT/SIGTERM shutdown. No receipt listener and no user context (direct has no broker receipt). Generation STRIPS the continuous loop, the rotating payload, the dynamic topic, and the stats thread down to a short bounded burst that self-exits (publisher steps below). -- `python-direct-subscriber-sample.py`: basic-auth connect, a plain topic subscription on a direct receiver (no queue, no provisioning), capacity-bounded back pressure with discard detection, service event listeners registered before `connect()`, and a graceful SIGINT/SIGTERM shutdown. The subscriber is the long-running SIGINT target. +- `python-direct-publisher-sample.py` (both threading shapes): basic-auth connect, a continuous DIRECT binary publish loop with a rotating payload to a dynamic topic, a publish failure listener for the asynchronous failures the API reports, capacity-bounded back pressure, service event listeners registered before `connect()`, and a graceful SIGINT/SIGTERM shutdown. No receipt listener and no user context (direct has no broker receipt). Generation STRIPS the continuous loop, the rotating payload, the dynamic topic, and the stats thread down to a short bounded burst that self-exits (publisher steps below). The direct publisher has no blocking variant: `publish()` returns at once because direct has no receipt to await, and the publish failure listener stays on both shapes as the only channel for an asynchronous send failure. +- `python-direct-subscriber-sample.py` (`Threading: Async`, or no `Threading` field): basic-auth connect, a plain topic subscription on a direct receiver (no queue, no provisioning), capacity-bounded back pressure with discard detection, service event listeners registered before `connect()`, and a graceful SIGINT/SIGTERM shutdown. The subscriber is the long-running SIGINT target. +- `python-direct-subscriber-blocking-sample.py` (`Threading: Minimal (blocking publish and receive)`): the same subscription, back pressure, and discard detection, but NO `receive_async()` handler: the main loop pulls each message with `receive_message(timeout)` and a POSITIVE timeout and processes it on the application's own thread, and shutdown calls `terminate(0)`. Each script builds its OWN `MessagingService` (two services, one per script, matching the samples). Keep the samples' structure: the shared state dataclass, `setup_solace(...)` that builds the service and the receiver or publisher before any connection exists, `connect_solace(...)`, the main loop, and `teardown_solace(...)` in `main()`'s `finally`. Keep every listener class; they are part of the best-practices contract and must not be stripped during adaptation. **Ground before you write.** Before writing either script, WebFetch [Publishing Direct Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-DM-Publish.md) and [Consuming Direct Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-DM-Receive.md) in this session (SKILL.md Invariant 2). -## Subscriber script (`direct_subscriber.py`) +## Subscriber script (`direct_subscriber.py`, or `direct_subscriber_blocking.py` on the blocking shape) -The subscriber subscribes directly to the topic (no queue), receives asynchronously, and is the long-running process the developer runs FIRST. Start the file with the disclaimer header, then wire it in this sequence: +The subscriber subscribes directly to the topic (no queue), receives on a callback (callback shape) or pulls on its main loop (blocking shape), and is the long-running process the developer runs FIRST. Start the file with the disclaimer header, then wire it in this sequence: 1. Build the connection properties with `load_service_properties(args, APP_NAME)`, then the service with `MessagingService.builder().from_properties(properties).build()` (on Quickstart without the reconnection strategy call, per the fail-fast rule). 2. Build the direct receiver as the sample does: `create_direct_message_receiver_builder()` with `with_subscriptions([TopicSubscription.of(topic)])` for the design summary's topic and `on_back_pressure_drop_latest(RECEIVE_BUFFER_CAPACITY)`, then `build()`. Register the three service event listeners BEFORE `connect()`. There is NO queue and NO missing-resources creation strategy on the direct path. 3. `messaging_service.connect()`. Immediately after `connect()` returns, emit the connect marker: `trace("VERIFY: CONNECTED")`. -4. `receiver.receive_async(handler)` BEFORE `receiver.start()`, as the sample orders them, then `receiver.start()`. `start()` applies the subscription and blocks until the broker confirms it. Immediately after `start()` returns, emit the subscribed marker: `trace("VERIFY: SUBSCRIBED")`. **No reapply-subscriptions setting.** The JCSMP direct subscriber must set `REAPPLY_SUBSCRIPTIONS` to `true`, or its subscription is lost on an automatic reconnect. The Python API has no such setting and needs none: the [Messaging Service](https://docs.solace.com/API/API-Developer-Guide-Python/Python-API-Messaging-Service.md) page states that the API re-applies the application's direct subscriptions on reconnect. Do not search for or invent a Python equivalent. Keep the sample's `on_reconnected` comment that says so. When reconnect attempts are exhausted, the same page states that the application must recreate the session with all its subscriptions; the sample's `on_service_interrupted` handler covers that case by shutting down. -5. In the handler's `on_message`, keep the sample's payload read and its discard check (`get_message_discard_notification()`), then emit the receipt marker: `trace("VERIFY: MESSAGE_RECEIVED")`. There is NO `ack()` on the direct path. -6. KEEP the long-running wait loop, the SIGINT/SIGTERM handler that only sets the shutdown event, and `teardown_solace(...)` with `terminate(grace_period)` and `disconnect()`. The subscriber is the SIGINT target, and its shutdown line (`Shutdown signal received ...`) stays a `trace(...)` call. +4. Callback shape: `receiver.receive_async(handler)` BEFORE `receiver.start()`, as the sample orders them, then `receiver.start()`. Blocking shape: NO `receive_async()`; `receiver.start()` alone, as the blocking sample does, and the loop of step 5 pulls the messages. `start()` applies the subscription and blocks until the broker confirms it. On both shapes, immediately after `start()` returns, emit the subscribed marker: `trace("VERIFY: SUBSCRIBED")`. **No reapply-subscriptions setting.** The JCSMP direct subscriber must set `REAPPLY_SUBSCRIPTIONS` to `true`, or its subscription is lost on an automatic reconnect. The Python API has no such setting and needs none: the [Messaging Service](https://docs.solace.com/API/API-Developer-Guide-Python/Python-API-Messaging-Service.md) page states that the API re-applies the application's direct subscriptions on reconnect. Do not search for or invent a Python equivalent. Keep the sample's `on_reconnected` comment that says so. When reconnect attempts are exhausted, the same page states that the application must recreate the session with all its subscriptions; the sample's `on_service_interrupted` handler covers that case by shutting down. +5. Callback shape: in the handler's `on_message`, keep the sample's payload read and its discard check (`get_message_discard_notification()`). Blocking shape: in the main loop, keep the sample's `message = receiver.receive_message(RECEIVE_TIMEOUT_MS)` call with its POSITIVE timeout in milliseconds and its `PubSubPlusClientError` branch, and hand each returned message to the sample's `process_message(...)` with the same payload read and discard check; `None` means a timeout, and the loop goes round to its shutdown check. On both shapes, after the discard check, emit the receipt marker: `trace("VERIFY: MESSAGE_RECEIVED")`. There is NO `ack()` on the direct path. +6. KEEP the long-running wait loop (on the blocking shape, the `receive_message()` loop itself, which checks the shutdown event each time the call returns), the SIGINT/SIGTERM handler that only sets the shutdown event, and `teardown_solace(...)` with `terminate(grace_period)` and `disconnect()`. On the blocking shape keep the sample's `TERMINATE_GRACE_PERIOD_MS` of 0 and its `IncompleteMessageDeliveryError` warning: the loop has stopped, so nothing drains the buffer, and dropping its direct messages is a normal at-most-once stop. The subscriber is the SIGINT target, and its shutdown line (`Shutdown signal received ...`) stays a `trace(...)` call. ## Publisher script (`direct_publisher.py`) @@ -30,7 +31,7 @@ The publisher only connects and publishes a short bounded burst of DIRECT messag 1. Build the connection properties and the service exactly as the subscriber does (step 1 above), then the publisher with `create_direct_message_publisher_builder().on_back_pressure_wait(PUBLISH_BUFFER_CAPACITY).build()`. Keep `set_publish_failure_listener(...)`: direct has no broker receipt, so a buffered message that cannot be sent is reported only through that listener. Register the three service event listeners BEFORE `connect()`. 2. `messaging_service.connect()`. Immediately after `connect()` returns, emit the connect marker: `trace("VERIFY: CONNECTED")`. Then `publisher.start()`. -3. Publish a SHORT BOUNDED BURST to the design summary's topic with `publish(payload, Topic.of(topic))` in a small fixed-count loop, then return from the publish step. REMOVE the sample's continuous loop, its rotating payload and dynamic topic, and its stats thread. Keep the sample's `PubSubPlusClientError` branch, which sets the shared state's `exit_code` to 1. `teardown_solace(...)` in `main()`'s `finally` calls `terminate(grace_period)`, which gives the buffered messages up to the grace period to leave before `disconnect()`. Keep the SIGINT/SIGTERM handler, so Ctrl-C during a back pressure wait still reaches teardown. When the request describes a real application, its own messages replace the burst (the demo-harness rule). +3. Publish a SHORT BOUNDED BURST to the design summary's topic with `publish(payload, Topic.of(topic))` in a small fixed-count loop, then return from the publish step. REMOVE the sample's continuous loop, its rotating payload and dynamic topic, and its stats thread. Keep the sample's `PubSubPlusClientError` branch, which sets the shared state's `exit_code` to 1. `teardown_solace(...)` in `main()`'s `finally` calls `terminate(grace_period)`, which gives the buffered messages up to the grace period to leave before `disconnect()`. Keep the SIGINT/SIGTERM handler, so Ctrl-C during a back pressure wait still reaches teardown. When the request describes a real application, its own messages replace the burst (the demo-harness rule). That replacement changes the payload and the topic, never the shape: a request that describes a continuous or high-rate feed (readings several times a second, a telemetry stream) still gets a publisher that sends a bounded batch of the application's real messages and exits on its own, because the `direct` stage waits for that exit. Do not keep the sample's open-ended loop behind an environment-variable cap, and do not move the project to the `app` stage to drive it; the continuous trigger is the developer's integration step after verification. 4. The publisher emits NO send-side marker. Direct has no broker receipt, so there is no `PUBLISH_ACKED` analog; a marker after `publish()` would only prove the local call returned. The proof of the round trip is `VERIFY: MESSAGE_RECEIVED` in the subscriber output. ## The marker contract diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-guaranteed-pubsub.md b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-guaranteed-pubsub.md index 826e4b9..88303fb 100644 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-guaranteed-pubsub.md +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-guaranteed-pubsub.md @@ -4,18 +4,25 @@ The per-leaf wiring for the three Guaranteed Pub/Sub leaves of the Python API. R ## Guaranteed pub/sub flow -The skill ships two best-practices reference samples for this flow. Read them by their exact relative paths and adapt each into its own generated script. There is no merge and no third skeleton: `guaranteed_publisher.py` is a near-verbatim adaptation of the publisher sample, and `guaranteed_subscriber.py` is a near-verbatim adaptation of the subscriber sample. +The skill ships two best-practices reference sample PAIRS for this flow, and the design summary's `Threading` field picks one (`implement-mode.md` Step 4): `Async`, or no `Threading` field, picks the callback pair; `Minimal (blocking publish and receive)` picks the blocking pair. Read ONLY the chosen pair, by their exact relative paths, and adapt each into its own generated script. There is no merge and no third skeleton: the generated publisher is a near-verbatim adaptation of the chosen publisher sample, and the generated subscriber is a near-verbatim adaptation of the chosen subscriber sample. + +The callback pair (`Threading: Async`), generated as `guaranteed_publisher.py` and `guaranteed_subscriber.py`: - `python-guaranteed-publisher-sample.py`: basic-auth connect, persistent binary publish to a topic from a message template that sets the time-to-live once, publish receipt (ACK/NACK) handling, capacity-bounded back pressure, service event listeners registered before `connect()`, and a graceful SIGINT/SIGTERM shutdown that drains outstanding receipts. - `python-guaranteed-subscriber-sample.py`: basic-auth connect, durable queue plus topic subscription provisioned at start, a client-ack receiver that ACKs only after processing, receiver state and termination handling, service event listeners registered before `connect()`, and a graceful SIGINT/SIGTERM shutdown. +The blocking pair (`Threading: Minimal (blocking publish and receive)`), generated as `guaranteed_publisher_blocking.py` and `guaranteed_subscriber_blocking.py`: + +- `python-guaranteed-publisher-blocking-sample.py`: the same connect and message template, but one blocking `publish_await_acknowledgement()` per message with a POSITIVE acknowledgement timeout, NO publish receipt listener and NO back-pressure setting (one message is in flight at a time, so the publish buffer cannot grow), the "Discard the Message" handling of a timeout, a NACK, or an overflow from the Publishing Persistent Messages page, and the same graceful shutdown. The main thread publishes each message and handles its ACK or NACK itself. +- `python-guaranteed-subscriber-blocking-sample.py`: the same provisioning and client-ack receiver, but NO `receive_async()` handler: the main loop pulls each message with `receive_message(timeout)` and a POSITIVE timeout, processes it, and ACKs it on the application's own thread, and shutdown calls `terminate(0)`. + Each script builds its OWN `MessagingService` (two services, one per script, matching the samples). Keep the samples' structure: the shared state dataclass, `setup_solace(...)` that builds the service and the receiver or publisher before any connection exists, `connect_solace(...)`, the main loop, and `teardown_solace(...)` in `main()`'s `finally`. Keep every listener class; they are part of the best-practices contract and must not be stripped during adaptation. **Ground before you write.** Before writing either script, WebFetch [Publishing Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Publish.md) and [Consuming Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Receive.md) in this session (SKILL.md Invariant 2). The second page also grounds the `receive_async()` and acknowledgement rules in `implement-mode.md` Step 4's concurrency section. -The three guaranteed pub/sub leaves generate the same publisher and subscriber. The exclusive versus non-exclusive choice is one queue-factory line (read the `Access type` summary field). The fan-out leaf reuses the single-service subscriber once per consuming service (see Fan-out below). +The three guaranteed pub/sub leaves generate the same publisher and subscriber. The exclusive versus non-exclusive choice is one queue-factory line (read the `Access type` summary field). The fan-out leaf reuses the single-service subscriber once per consuming service (see Fan-out below). The two threading shapes share the sequences below; a step that differs names both shapes. -## Subscriber script (`guaranteed_subscriber.py`) +## Subscriber script (`guaranteed_subscriber.py`, or `guaranteed_subscriber_blocking.py` on the blocking shape) The subscriber provisions the durable queue and the topic subscription, and it is the long-running process the developer runs FIRST. The publisher does NOT provision anything. Start the file with the disclaimer header, then wire the subscriber in this sequence, each step grounded in the pages linked in `implement-mode.md`'s Grounding references: @@ -23,9 +30,9 @@ The subscriber provisions the durable queue and the topic subscription, and it i 2. Build the queue from the design summary's `Consumption endpoint` queue name and `Access type` field (see the access-type snippet below). 3. Build the persistent receiver as the sample does: `create_persistent_message_receiver_builder()` with `with_missing_resources_creation_strategy(MissingResourcesCreationStrategy.CREATE_ON_START)`, `with_subscriptions([TopicSubscription.of(topic)])` for the design summary's topic, `with_message_client_acknowledgement()`, and `with_activation_passivation_support(...)`, then `build(queue)`. Keep the receiver termination listener and register the three service event listeners BEFORE `connect()`. See [Creating Queues with the Solace Python API](https://docs.solace.com/API/API-Developer-Guide-Python/Python-API-Create-Queues.md). 4. `messaging_service.connect()`. Immediately after `connect()` returns, emit the connect marker: `trace("VERIFY: CONNECTED")`. -5. `receiver.receive_async(handler)` BEFORE `receiver.start()`, as the sample orders them, then `receiver.start()`. Immediately after `start()` returns without raising, emit the queue-bound marker: `trace("VERIFY: QUEUE_BOUND")`. Keep the sample's `PubSubPlusClientError` branch around `start()`: a client that may not create endpoints fails there, and the error message tells the developer what to do. -6. In the handler's `on_message`, keep the sample's try/except/finally and call `receiver.ack(message)` only after processing. Immediately after `ack()` in the `finally`, emit the round-trip marker: `trace("VERIFY: MESSAGE_RECEIVED")`. See [Consuming Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Receive.md). -7. KEEP the long-running wait loop, the SIGINT/SIGTERM handler that only sets the shutdown event, and `teardown_solace(...)` with `terminate(grace_period)` and `disconnect()`. The subscriber is the SIGINT target, and its shutdown line (`Shutdown signal received ...`) stays a `trace(...)` call. +5. Callback shape: `receiver.receive_async(handler)` BEFORE `receiver.start()`, as the sample orders them, then `receiver.start()`. Blocking shape: NO `receive_async()`; `receiver.start()` alone, as the blocking sample does, and the loop of step 6 pulls the messages. On both shapes, immediately after `start()` returns without raising, emit the queue-bound marker: `trace("VERIFY: QUEUE_BOUND")`. Keep the sample's `PubSubPlusClientError` branch around `start()`: a client that may not create endpoints fails there, and the error message tells the developer what to do. +6. Callback shape: in the handler's `on_message`, keep the sample's try/except/finally and call `receiver.ack(message)` only after processing. Blocking shape: in the main loop, keep the sample's `message = receiver.receive_message(RECEIVE_TIMEOUT_MS)` call with its POSITIVE timeout in milliseconds and its `PubSubPlusClientError` branch, and hand each returned message to the sample's `process_message(...)`, whose try/except/finally calls `receiver.ack(message)` only after processing; `None` means a timeout, and the loop goes round to its shutdown check. On both shapes, immediately after `ack()` in the `finally`, emit the round-trip marker: `trace("VERIFY: MESSAGE_RECEIVED")`. See [Consuming Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Receive.md). +7. KEEP the long-running wait loop (on the blocking shape, the `receive_message()` loop itself, which checks the shutdown event each time the call returns), the SIGINT/SIGTERM handler that only sets the shutdown event, and `teardown_solace(...)` with `terminate(grace_period)` and `disconnect()`. On the blocking shape keep the sample's `TERMINATE_GRACE_PERIOD_MS` of 0 and its `IncompleteMessageDeliveryError` warning: the loop has stopped, so nothing drains the receiver's buffer, and a message that was never ACKed stays on the queue for redelivery. The subscriber is the SIGINT target, and its shutdown line (`Shutdown signal received ...`) stays a `trace(...)` call. **Access type (exclusive vs non-exclusive): one line, not a separate sample.** The `Access type` field picks the queue factory: @@ -36,19 +43,19 @@ queue = Queue.durable_exclusive_queue(queue_name) # one active consumer / When the `Access type` field reads exclusive, keep the `durable_exclusive_queue` line: only one bound receiver is active at a time, and the receiver state listener reports the passive standby. When it reads non-exclusive, use the `durable_non_exclusive_queue` line instead, so several subscriber processes bound to the same queue share the messages. Emit exactly one of the two lines. Ground the choice in [Topic Endpoints and Queues](https://docs.solace.com/Get-Started/topic-endpoints-queues.md). -**Fan-out.** On the fan-out leaf, every consuming service gets its own durable queue with the same topic subscription, so each service receives its own full copy of every event. Generate one subscriber script per consuming service: `guaranteed_subscriber.py` for the first service, and `guaranteed_subscriber_.py` for each further service, where `` is a lowercase Python identifier (letters, digits, and underscores, for example `billing` or `audit_log`) so the import smoke can import the module, each with its own queue name from the design summary and an exclusive durable queue. The `verify.sh` stages drive `guaranteed_subscriber.py` only, so hand back one run command per further subscriber in Step 5, and add each script to the import smoke. +**Fan-out.** On the fan-out leaf, every consuming service gets its own durable queue with the same topic subscription, so each service receives its own full copy of every event. Generate one subscriber script per consuming service: `guaranteed_subscriber.py` for the first service, and `guaranteed_subscriber_.py` for each further service (on the blocking shape, `guaranteed_subscriber_blocking.py` and `guaranteed_subscriber_blocking_.py`), where `` is a lowercase Python identifier (letters, digits, and underscores, for example `billing` or `audit_log`) so the import smoke can import the module, each with its own queue name from the design summary and an exclusive durable queue. The `verify.sh` stages drive the first subscriber script only, so hand back one run command per further subscriber in Step 5, and add each script to the import smoke. -## Publisher script (`guaranteed_publisher.py`) +## Publisher script (`guaranteed_publisher.py`, or `guaranteed_publisher_blocking.py` on the blocking shape) The publisher only connects and publishes; it does NOT provision the queue or the subscription. It must exit on its own after the broker acknowledges its messages, so the developer does not have to interrupt it. Start the file with the disclaimer header, then wire the publisher in this sequence: -1. Build the connection properties and the service exactly as the subscriber does (step 1 above), then the publisher with `create_persistent_message_publisher_builder().on_back_pressure_wait(PUBLISH_BUFFER_CAPACITY).build()`. Register the three service event listeners BEFORE `connect()`. +1. Build the connection properties and the service exactly as the subscriber does (step 1 above), then the publisher. Callback shape: `create_persistent_message_publisher_builder().on_back_pressure_wait(PUBLISH_BUFFER_CAPACITY).build()`. Blocking shape: `create_persistent_message_publisher_builder().build()` with NO back-pressure setting, as the blocking sample explains: one message is in flight at a time, so the publish buffer cannot grow. Register the three service event listeners BEFORE `connect()`. 2. `messaging_service.connect()`. Immediately after `connect()` returns, emit the connect marker: `trace("VERIFY: CONNECTED")`. -3. `publisher.start()`, then `publisher.set_message_publish_receipt_listener(...)` AFTER `start()`, as the sample orders them. In the receipt handler's ACK branch, emit the publish-ACK marker: `trace("VERIFY: PUBLISH_ACKED")`. CRITICAL: the sample logs the ACK with `logger.debug`, which the INFO-level logging never shows, so the marker would never appear. Emit it through `trace(...)`, not through the logger. In the NACK branch, keep the sample's warning and also set the shared state's `exit_code` to 1, so a rejected message makes the publisher exit non-zero. Pass the shared state to the handler to do this. -4. Publish persistent messages to the design summary's topic as the sample does: one `message_builder()` template with `PERSISTENT_TIME_TO_LIVE` set once, `build(payload, additional_message_properties={APPLICATION_MESSAGE_ID: msg_id})` per message, and `publish(message, topic, user_context=msg_id)`. See [Publishing Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Publish.md). -5. EXIT on its own: replace the sample's continuous publish loop with the application's own messages, or with one message on a try-it run, then return from the publish step. `teardown_solace(...)` in `main()`'s `finally` calls `terminate(grace_period)`, which sends what is buffered and waits for the broker's receipt of every published message, so the `VERIFY: PUBLISH_ACKED` marker prints before the script exits. Drop the stats thread with the loop. Keep the SIGINT/SIGTERM handler, so Ctrl-C during a back pressure wait still reaches teardown. +3. `publisher.start()`. Callback shape: then `publisher.set_message_publish_receipt_listener(...)` AFTER `start()`, as the sample orders them. In the receipt handler's ACK branch, emit the publish-ACK marker: `trace("VERIFY: PUBLISH_ACKED")`. CRITICAL: the sample logs the ACK with `logger.debug`, which the INFO-level logging never shows, so the marker would never appear. Emit it through `trace(...)`, not through the logger. In the NACK branch, keep the sample's warning and also set the shared state's `exit_code` to 1, so a rejected message makes the publisher exit non-zero. Pass the shared state to the handler to do this. Blocking shape: NO receipt listener; the marker moves into step 4. +4. Publish persistent messages to the design summary's topic as the sample does: one `message_builder()` template with `PERSISTENT_TIME_TO_LIVE` set once, and `build(payload, additional_message_properties={APPLICATION_MESSAGE_ID: msg_id})` per message. Callback shape: `publish(message, topic, user_context=msg_id)`. Blocking shape: `publish_await_acknowledgement(message, topic, PUBLISH_ACK_TIMEOUT_MS)` with the sample's POSITIVE timeout in milliseconds and no `user_context`; a normal return IS the broker's ACK, so emit `trace("VERIFY: PUBLISH_ACKED")` immediately after the call returns. Keep the blocking sample's three discard branches (`PubSubTimeoutError`, the NACK error classes, and `PublisherOverflowError`), which log the message id at INFO and continue (the Discard the Message strategy from the Publishing Persistent Messages page), but set the shared state's `exit_code` to 1 in EACH of them, the same way the callback shape's NACK branch does: the loop still sends the remaining messages, and the publisher exits non-zero at the end, so a run that lost one message out of many fails the `verify.sh` stage instead of passing on the markers of the others. Keep its `PubSubPlusClientError` branch too, which sets `exit_code` to 1 and stops. See [Publishing Persistent Messages](https://docs.solace.com/API/API-Developer-Guide-Python/Python-PM-Publish.md). +5. EXIT on its own: replace the sample's continuous publish loop with the application's own messages, or with one message on a try-it run, then return from the publish step. `teardown_solace(...)` in `main()`'s `finally` calls `terminate(grace_period)`. Callback shape: it sends what is buffered and waits for the broker's receipt of every published message, so the `VERIFY: PUBLISH_ACKED` marker prints before the script exits. Blocking shape: every call has already returned with its ACK and printed the marker, so `terminate()` returns at once; keep the sample's `IncompleteMessageDeliveryError` branch for a message the API never handed to the transport. Drop the stats thread (or the blocking sample's inline rate report) with the loop. Keep the SIGINT/SIGTERM handler, so Ctrl-C during a back pressure wait or an acknowledgement wait still reaches teardown. -**Persisted-request variant (application correlation id).** When the `Consumption endpoint` field records an application correlation id (a persisted request that `design-mode.md` routed here), generate the request leg only. The publisher adds `message_properties.CORRELATION_ID: msg_id` to the same `additional_message_properties` dict in step 4, and the subscriber stays unchanged. Do NOT generate an answer path: no reply topic, no reply subscription, no reply publish, and no `VERIFY:` marker beyond the four below. List the answer path under Developer-owned items in the checklist (`implement-mode.md` Step 6), and name it in the close-out as the developer's next step. +**Persisted-request variant (application correlation id).** When the `Consumption endpoint` field records an application correlation id (a persisted request that `design-mode.md` routed here), generate the request leg only. The publisher adds `message_properties.CORRELATION_ID: msg_id` to the same `additional_message_properties` dict in step 4 (on either threading shape), and the subscriber stays unchanged. Do NOT generate an answer path: no reply topic, no reply subscription, no reply publish, and no `VERIFY:` marker beyond the four below. List the answer path under Developer-owned items in the checklist (`implement-mode.md` Step 6), and name it in the close-out as the developer's next step. ## The marker contract diff --git a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-mode.md b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-mode.md index fb95d3a..4ab3cd5 100644 --- a/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-mode.md +++ b/plugins/solace-messaging-skills/skills/solace-application-development/references/python/implement-mode.md @@ -12,12 +12,14 @@ Work through the steps in order. Read this file's grounding links only as each s Every Implement run writes ALL of these as generation output, on every app shape. None of them is gated on consent, and none of them waits for the end of the session: -1. The generated scripts for the chosen leaf, plus the shared `solace_connection_config.py` helper. Every source file starts with the AI-assisted disclaimer header (Step 4). +1. The generated scripts for the chosen leaf, plus the shared `solace_connection_config.py` helper, all under `src/` at the project root. Every source file starts with the AI-assisted disclaimer header (Step 4). 2. `requirements.txt` with the `solace-pubsubplus` version resolved per Step 3. 3. `config.example.json` (placeholders) and a `.gitignore` that ignores `config.json` and the virtual environment (Step 4). 4. A tailored `solace-verification-checklist.md` at each project root (tailoring rules in Step 6; the WRITE happens here at generation time, like the scripts). 5. A copy of the bundled Python `scripts/verify.sh` at each project root, plus a generated `verify-hooks.sh` beside it (Step 5), so the verification commands resolve from inside the project. +**Project layout.** Every `.py` file the project generates goes under `src/`, and nothing else does. The project root holds `requirements.txt`, `.venv/`, `config.example.json`, `config.json`, `.gitignore`, `verify.sh`, `verify-hooks.sh`, `solace-design.md`, and `solace-verification-checklist.md`, so code and Markdown never share a directory. `src/` is a plain directory, not a package: no `__init__.py` and no `pyproject.toml`. Every script runs from the project root as `.venv/bin/python src/