{"record":{"id":"5dcdb9671275ac37","repo":"zylon-ai/private-gpt","slug":"invalid-message-order-expected-expected-roles-a","errorCode":null,"errorMessage":"Invalid message order: expected {expected_roles} after {previous_role}, but got {current_role}.Check ToolUseBlock and ToolResultBlock order.","messagePattern":"Invalid message order: expected (.+?) after (.+?), but got (.+?)\\.Check ToolUseBlock and ToolResultBlock order\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"private_gpt/chat/input_models.py","lineNumber":531,"sourceCode":"                    expected_roles = {\n                        MessageRole.ASSISTANT,\n                        MessageRole.USER,\n                    }\n                elif previous_role == MessageRole.ASSISTANT:\n                    expected_roles = {\n                        MessageRole.USER,\n                        MessageRole.TOOL,\n                    }\n                elif previous_role == MessageRole.TOOL:\n                    expected_roles = {\n                        MessageRole.ASSISTANT,\n                        # Mistral doesn't support user after tool\n                        # Fixed in the tokenizer\n                        MessageRole.USER,\n                    }\n\n                if current_role not in expected_roles:\n                    raise ValueError(\n                        f\"Invalid message order: expected {expected_roles} after {previous_role}, but got {current_role}.\"\n                        \"Check ToolUseBlock and ToolResultBlock order.\"\n                        if current_role == MessageRole.TOOL\n                        else \"\"\n                    )\n\n            previous_role = current_role\n\n    @classmethod\n    def _merge_messages(cls, messages: list[\"MessageInput\"]) -> list[\"MessageInput\"]:\n        \"\"\"Merge consecutive messages with the same role.\"\"\"\n        if not messages:\n            return []\n\n        merged_messages: list[MessageInput] = []\n        current_message = messages[0]\n\n        for msg in messages[1:]:","sourceCodeStart":513,"sourceCodeEnd":549,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/chat/input_models.py#L513-L549","documentation":"Raised by the DELETE /skills/{skill_id} route when the skill does not exist in the given collection (get_skill returned None). Deletion requires the skill to be resolvable first; a miss short-circuits to 404 before any mutation is attempted.","triggerScenarios":"DELETE /skills/{id} with an id from another collection, a deleted skill, or after the collection was cleared; duplicate delete requests.","commonSituations":"Two clients racing to delete the same skill; stale UI state; deleting in the wrong collection context.","solutions":["Treat 404 on delete as already-deleted and refresh the list.","Confirm the collection parameter matches where the skill was created.","Disable duplicate delete actions while a request is in flight."],"exampleFix":"// before\nawait skillsApi.delete(id, collection); // second call 404s\n\n// after\ntry { await skillsApi.delete(id, collection); }\ncatch (e) { if (e.status !== 404) throw e; } // 404 = already gone","handlingStrategy":"try-catch","validationCode":"const exists = (await skillsApi.list(collection)).data.some((s) => s.id === id);\nif (!exists) return; // nothing to delete","typeGuard":null,"tryCatchPattern":"try { await skillsApi.delete(id, collection); }\ncatch (e) { if (e.status === 404) return; /* already deleted */ throw e; }","preventionTips":["Treat skill delete as idempotent","Lock the delete button during in-flight requests","Verify collection context before issuing deletes"],"tags":["fastapi","http-404","skills","deletion","idempotency"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}