Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion examples/workflows/contract-comparison/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@
"config": {
"inputSchema": {
"properties": {
"files": { "type": "array", "description": "File objects with fileId and fileName, ordered chronologically (oldest first). Minimum 2 required." },
"files": {
"type": "array",
"description": "File objects ordered chronologically (oldest first). Minimum 2 required.",
"items": {
"type": "object",
"properties": {
"fileId": { "type": "string", "description": "Convex storage ID" },
"fileName": { "type": "string", "description": "Original file name" }
},
"required": ["fileId", "fileName"]
}
},
Comment on lines +23 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

files minimum cardinality is documented but not enforced.

The schema says “Minimum 2 required,” but this segment only enforces presence/type, not count. That allows invalid single-file inputs to proceed and break the comparison intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/workflows/contract-comparison/config.json` around lines 23 - 32, The
schema for the "files" array documents a minimum of 2 but doesn't enforce it;
update the array definition for "files" in config.json (the object that
describes items with properties "fileId" and "fileName") to include a minItems:
2 constraint so JSON validation will reject inputs with fewer than two file
objects.

"language": { "type": "string", "description": "Output language for the report (e.g., 'de', 'en', 'fr'). If omitted, auto-detected from documents." }
},
"required": ["files"]
Expand Down
38 changes: 29 additions & 9 deletions examples/workflows/contract-generation/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"workflowConfig": {
"name": "Contract Generation",
"description": "Modify contracts from template files using a base contract. Use when the user asks to rewrite or modify a contract based on template documents.\n\nThe workflow indexes one or more contract templates into RAG (the ONLY source of clauses — no clauses are invented), then uses structured DOCX round-trip editing — extracting paragraphs with stable keys from the base contract, applying targeted text modifications via LLM + RAG, and writing changes back to the original DOCX with Track Changes markup. This preserves all original formatting, styles, headers/footers, and document properties.\n\nA base contract (DOCX) is required. Extract fileIds from pre-analyzed attachments in the conversation (look for '(fileId: ...)' annotations) or ask the user to provide them.",
"description": "Modify contracts from template files using a base contract. Use when the user asks to rewrite or modify a contract based on template documents.\n\nThe workflow indexes one or more contract templates into RAG (the ONLY source of clauses — no clauses are invented), then uses structured DOCX round-trip editing — extracting paragraphs with stable keys from the base contract, applying targeted text modifications via LLM + RAG, and writing changes back to the original DOCX with Track Changes markup. This preserves all original formatting, styles, headers/footers, and document properties.\n\nA base contract (DOCX) is required. Extract file objects (fileId + fileName) from pre-analyzed attachments in the conversation (look for '(fileId: ...)' annotations) or ask the user to provide them.",
"version": "1.0.0",
"workflowType": "predefined",
"config": {
Expand All @@ -18,12 +18,31 @@
"config": {
"inputSchema": {
"properties": {
"templateFileIds": { "type": "array", "description": "Convex storage IDs of contract template files (DOCX or PDF). These are the ONLY source of clauses — extract fileIds from pre-analyzed attachments (look for '(fileId: ...)' annotations)." },
"baseFileId": { "type": "string", "description": "Convex storage ID of an existing DOCX contract to modify. The workflow extracts structured paragraphs, applies targeted modifications with Track Changes markup, and preserves all original formatting." },
"knowledgeFiles": {
"type": "array",
"description": "Contract template files (DOCX or PDF). The ONLY source of clauses — extract from pre-analyzed attachments (look for '(fileId: ...)' annotations).",
"items": {
"type": "object",
"properties": {
"fileId": { "type": "string", "description": "Convex storage ID" },
"fileName": { "type": "string", "description": "Original file name" }
},
"required": ["fileId", "fileName"]
}
},
"baseFile": {
"type": "object",
"description": "Existing DOCX contract to modify. The workflow preserves all original formatting.",
"properties": {
"fileId": { "type": "string", "description": "Convex storage ID" },
"fileName": { "type": "string", "description": "Original file name" }
},
"required": ["fileId", "fileName"]
},
"requirements": { "type": "string", "description": "The user's instructions in natural language. Preserve the user's full intent (e.g., 'Draft a buyer-friendly SPA', 'Strengthen indemnification, extend warranty survival to 24 months')." },
"outputFileName": { "type": "string", "description": "Base name for the output DOCX file (without extension). Choose a sensible name based on the request." }
},
"required": ["templateFileIds", "baseFileId", "requirements", "outputFileName"]
"required": ["knowledgeFiles", "baseFile", "requirements", "outputFileName"]
}
},
"nextSteps": { "success": "loop_templates" }
Expand All @@ -33,7 +52,7 @@
"name": "Loop: Index Templates for RAG",
"stepType": "loop",
"config": {
"items": "{{input.templateFileIds}}"
"items": "{{input.knowledgeFiles | map('fileId')}}"
},
"nextSteps": {
"loop": "index_template_rag",
Expand Down Expand Up @@ -62,7 +81,7 @@
"type": "document",
"parameters": {
"operation": "extract_docx_structured",
"fileId": "{{input.baseFileId}}"
"fileId": "{{input.baseFile.fileId}}"
}
},
"nextSteps": { "success": "init_mod_loop" }
Expand Down Expand Up @@ -107,8 +126,9 @@
"config": {
"name": "Batch Modification Planner",
"tools": ["rag_search", "request_human_input"],
"systemPrompt": "You are a professional contract lawyer.\n\nYour task is to edit legal text conservatively.\n\nEditing principles:\n- Make minimal necessary changes\n- Preserve at least 85% of the original wording\n- Do NOT rewrite the entire clause\n- Do NOT restructure paragraphs or sections\n- Preserve numbering, references, and formatting\n\nEditing discipline:\n- Prefer small, precise modifications over broad rewrites\n- Only change text where clearly necessary\n- If no meaningful improvement is needed, return the original text unchanged\n\nCRITICAL EDITING RULE:\n- You must REPLACE existing text, not append to it\n- Do NOT keep both original and modified versions\n- The result must be a clean, single, final version of the clause\n\nSTRICTLY AVOID:\n- Duplicated sentences\n- Conflicting formulations\n- Partially merged text\n\nIf a sentence is modified, remove or fully integrate the original version.\n\nConsistency:\n- Ensure the revised text remains internally consistent\n- Avoid introducing new legal concepts unless required by the instruction\n\nCROSS-BATCH CONSISTENCY:\nYou may receive PREVIOUS BATCH MODIFICATIONS showing changes from the immediately preceding batch. Use this to maintain consistency — if a term, cap, threshold, or defined term was changed, align your modifications accordingly.\n\nLANGUAGE PRESERVATION: Output text MUST be in the SAME LANGUAGE as the input paragraph. NEVER translate.\n\nRAG DISCOVERY:\n- ONLY search for paragraphs you have already decided need modification based on the user requirements. Do NOT search for paragraphs that do not need changes.\n- Use rag_search with the provided template file IDs to find relevant template language\n- Run 2 or 3 diverse queries per paragraph you plan to modify (clause topic, key legal terms)\n- Templates are the ONLY source of clause language — NEVER invent clause text\n- If rag_search returns no relevant language, do NOT modify that paragraph\n- Always pass topK: 2 in every rag_search call\n\nPARAGRAPH BOUNDARIES: Do NOT merge or split paragraphs. Each modification replaces exactly one paragraph.\nDEFINED TERMS: Preserve defined terms in their original language.\nPLACEHOLDERS: When you encounter fill-in placeholders ([●], [TBD], [___], [Datum], etc.), use the request_human_input tool to ask the user for the actual values. Do not invent values or leave placeholders unfilled.\nEMPTY PARAGRAPHS: Skip entirely.\n\nUSER CONFIRMATION:\nWhen you encounter ANY of the following, use the request_human_input tool to ask the user before proceeding:\n- Fill-in placeholders: [●], [___], [Datum], [TBD], or similar blank fields that need specific values\n- Missing key information: party names, addresses, dates, registration numbers, or other identifying details\n- Ambiguous business decisions: liability caps, warranty periods, indemnification limits, governing law, jurisdiction, or other terms that require a commercial decision\n- Unclear requirements: if the user's instructions are ambiguous about how to modify a specific clause\nWhen calling request_human_input:\n- Collect ALL uncertain items from the current batch into a SINGLE request (do not make multiple calls)\n- Use text_input format for open-ended questions, single_select for binary choices, yes_no for confirmations\n- Provide clear context about what the placeholder or decision is about\n- If prior user responses are available (via {{humanInputContext}}), use those values directly — do NOT re-ask\n\n{{humanInputContext}}\n\nThe output clause must read like a clean, final legal draft ready for review by a lawyer.\n\nOUTPUT FORMAT:\nFor each paragraph that needs changes:\n--- MODIFICATION ---\nKey: [paragraph key]\nNew text: [complete replacement text]\n--- END ---\n\nOnly include paragraphs that actually need changes. If no paragraphs need changes: \"No modifications needed for this batch.\"",
"userPrompt": "PREVIOUS BATCH MODIFICATIONS:\n{{variables.previousBatchMods}}\n\nPARAGRAPHS IN THIS BATCH:\n{{loop.item}}\n\nTEMPLATE FILE IDS (pass these as fileIds to rag_search):\n{{input.templateFileIds}}\n\nUSER REQUIREMENTS:\n{{input.requirements}}",
"knowledgeFileIds": "{{input.knowledgeFiles | map('fileId')}}",
"systemPrompt": "You are a professional contract lawyer.\n\nYour task is to edit legal text conservatively.\n\nEditing principles:\n- Make minimal necessary changes\n- Preserve at least 85% of the original wording\n- Do NOT rewrite the entire clause\n- Do NOT restructure paragraphs or sections\n- Preserve numbering, references, and formatting\n\nEditing discipline:\n- Prefer small, precise modifications over broad rewrites\n- Only change text where clearly necessary\n- If no meaningful improvement is needed, return the original text unchanged\n\nCRITICAL EDITING RULE:\n- You must REPLACE existing text, not append to it\n- Do NOT keep both original and modified versions\n- The result must be a clean, single, final version of the clause\n\nSTRICTLY AVOID:\n- Duplicated sentences\n- Conflicting formulations\n- Partially merged text\n\nIf a sentence is modified, remove or fully integrate the original version.\n\nConsistency:\n- Ensure the revised text remains internally consistent\n- Avoid introducing new legal concepts unless required by the instruction\n\nCROSS-BATCH CONSISTENCY:\nYou may receive PREVIOUS BATCH MODIFICATIONS showing changes from the immediately preceding batch. Use this to maintain consistency — if a term, cap, threshold, or defined term was changed, align your modifications accordingly.\n\nLANGUAGE PRESERVATION: Output text MUST be in the SAME LANGUAGE as the input paragraph. NEVER translate.\n\nRAG DISCOVERY:\n- ONLY search for paragraphs you have already decided need modification based on the user requirements. Do NOT search for paragraphs that do not need changes.\n- Use rag_search to find relevant template language (file scope is automatically configured)\n- Run 2 or 3 diverse queries per paragraph you plan to modify (clause topic, key legal terms)\n- Templates are the ONLY source of clause language — NEVER invent clause text\n- If rag_search returns no relevant language, do NOT modify that paragraph\n- Always pass topK: 2 in every rag_search call\n\nPARAGRAPH BOUNDARIES: Do NOT merge or split paragraphs. Each modification replaces exactly one paragraph.\nDEFINED TERMS: Preserve defined terms in their original language.\nPLACEHOLDERS: When you encounter fill-in placeholders ([●], [TBD], [___], [Datum], etc.), use the request_human_input tool to ask the user for the actual values. Do not invent values or leave placeholders unfilled.\nEMPTY PARAGRAPHS: Skip entirely.\n\nUSER CONFIRMATION:\nWhen you encounter ANY of the following, use the request_human_input tool to ask the user before proceeding:\n- Fill-in placeholders: [●], [___], [Datum], [TBD], or similar blank fields that need specific values\n- Missing key information: party names, addresses, dates, registration numbers, or other identifying details\n- Ambiguous business decisions: liability caps, warranty periods, indemnification limits, governing law, jurisdiction, or other terms that require a commercial decision\n- Unclear requirements: if the user's instructions are ambiguous about how to modify a specific clause\nWhen calling request_human_input:\n- Collect ALL uncertain items from the current batch into a SINGLE request (do not make multiple calls)\n- Use text_input format for open-ended questions, single_select for binary choices, yes_no for confirmations\n- Provide clear context about what the placeholder or decision is about\n- If prior user responses are available (via {{humanInputContext}}), use those values directly — do NOT re-ask\n\n{{humanInputContext}}\n\nThe output clause must read like a clean, final legal draft ready for review by a lawyer.\n\nOUTPUT FORMAT:\nFor each paragraph that needs changes:\n--- MODIFICATION ---\nKey: [paragraph key]\nNew text: [complete replacement text]\n--- END ---\n\nOnly include paragraphs that actually need changes. If no paragraphs need changes: \"No modifications needed for this batch.\"",
"userPrompt": "PREVIOUS BATCH MODIFICATIONS:\n{{variables.previousBatchMods}}\n\nPARAGRAPHS IN THIS BATCH:\n{{loop.item}}\n\nUSER REQUIREMENTS:\n{{input.requirements}}",
"outputFormat": "text"
},
"nextSteps": { "success": "review_batch" }
Expand Down Expand Up @@ -183,7 +203,7 @@
"type": "document",
"parameters": {
"operation": "apply_docx_structured",
"templateFileId": "{{input.baseFileId}}",
"templateFileId": "{{input.baseFile.fileId}}",
"sourceHash": "{{steps.extract_base_structured.output.data.source_hash}}",
"modifications": "{{variables.allModifications}}",
"fileName": "{{input.outputFileName}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ function AutomationAssistantContent({
handleKeyDown,
workflowUpdateApprovals,
workflowCreationApprovals,
workflowRunApprovals,
humanInputRequests,
documentWriteApprovals,
integrationApprovals,
} = useAssistantChat({
automationId,
organizationId,
Expand All @@ -63,6 +67,10 @@ function AutomationAssistantContent({
organizationId={organizationId}
workflowUpdateApprovals={workflowUpdateApprovals}
workflowCreationApprovals={workflowCreationApprovals}
workflowRunApprovals={workflowRunApprovals}
humanInputRequests={humanInputRequests}
documentWriteApprovals={documentWriteApprovals}
integrationApprovals={integrationApprovals}
onImagePreview={(src, alt) =>
setPreviewImage({ isOpen: true, src, alt })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@ vi.mock('@/lib/i18n/client', () => ({
useT: () => ({ t: (key: string) => key }),
}));

vi.mock('@/app/features/chat/components/workflow-update-approval-card', () => ({
WorkflowUpdateApprovalCard: ({ approvalId }: { approvalId: string }) => (
<div data-testid={`update-approval-${approvalId}`}>Update Approval</div>
vi.mock('@/app/features/chat/components/approval-card-renderer', () => ({
ApprovalCardRenderer: ({
item,
}: {
item: { type: string; data: { _id: string } };
}) => (
<div data-testid={`${item.type}-approval-${item.data._id}`}>
{item.type} Approval
</div>
),
}));

vi.mock(
'@/app/features/chat/components/workflow-creation-approval-card',
() => ({
WorkflowCreationApprovalCard: ({ approvalId }: { approvalId: string }) => (
<div data-testid={`creation-approval-${approvalId}`}>
Creation Approval
</div>
),
}),
);
vi.mock('@/app/features/chat/components/collapsible-system-message', () => ({
CollapsibleSystemMessage: ({ content }: { content: string }) => (
<div data-testid="system-message">{content}</div>
),
}));
Comment on lines +34 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add direct coverage for the new approval and system-message branches.

These mocks and props are wired in, but the assertions still only exercise workflow update/creation approvals. Please add direct cases for workflowRunApprovals, humanInputRequests, documentWriteApprovals, integrationApprovals, and a multiline isHumanInputResponse message so the new selection/rendering paths in message-list.tsx are regression-tested.

Also applies to: 96-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@services/platform/app/features/automations/components/automation-assistant/message-list.test.tsx`
around lines 34 - 38, Add explicit unit tests in message-list.test.tsx that
render MessageList (or the tested wrapper) with messages covering the new
branches: create one message object with workflowRunApprovals, another with
humanInputRequests (and a separate test for multiline isHumanInputResponse
content), one with documentWriteApprovals, and one with integrationApprovals;
for each test, pass the message array into the same component setup used in
existing tests and assert the DOM shows the expected approval/selection UI and
that the CollapsibleSystemMessage mock (data-testid="system-message") contains
the correct content for system-message paths. Ensure you use the same prop
shapes/keys used in message-list.tsx (workflowRunApprovals, humanInputRequests,
isHumanInputResponse, documentWriteApprovals, integrationApprovals) so the new
selection/rendering branches are directly exercised and add analogous assertions
to the other case group near the existing approval tests.


vi.mock('./thinking-animation', () => ({
ThinkingAnimation: () => <div data-testid="thinking" />,
Expand Down Expand Up @@ -92,6 +93,10 @@ const defaultProps = {
workflow: { status: 'active' },
organizationId: 'org-1',
onImagePreview: vi.fn(),
workflowRunApprovals: [],
humanInputRequests: [],
documentWriteApprovals: [],
integrationApprovals: [],
};

describe('MessageList', () => {
Expand All @@ -116,7 +121,7 @@ describe('MessageList', () => {
);

expect(
screen.getByTestId('update-approval-approval-1'),
screen.getByTestId('workflow_update_approval-approval-approval-1'),
).toBeInTheDocument();
});

Expand All @@ -138,7 +143,7 @@ describe('MessageList', () => {
);

expect(
screen.queryByTestId('update-approval-completed-ap'),
screen.queryByTestId('workflow_update_approval-approval-completed-ap'),
).not.toBeInTheDocument();
});

Expand All @@ -160,7 +165,7 @@ describe('MessageList', () => {
);

expect(
screen.queryByTestId('update-approval-rejected-ap'),
screen.queryByTestId('workflow_update_approval-approval-rejected-ap'),
).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -189,9 +194,11 @@ describe('MessageList', () => {
);

// Only the latest (ap-2) should be shown
expect(screen.getByTestId('creation-approval-ap-2')).toBeInTheDocument();
expect(
screen.queryByTestId('update-approval-ap-1'),
screen.getByTestId('workflow_approval-approval-ap-2'),
).toBeInTheDocument();
expect(
screen.queryByTestId('workflow_update_approval-approval-ap-1'),
).not.toBeInTheDocument();
});

Expand Down Expand Up @@ -219,10 +226,10 @@ describe('MessageList', () => {

// Only the latest (newer-ap) should render
expect(
screen.getByTestId('update-approval-newer-ap'),
screen.getByTestId('workflow_update_approval-approval-newer-ap'),
).toBeInTheDocument();
expect(
screen.queryByTestId('update-approval-older-ap'),
screen.queryByTestId('workflow_update_approval-approval-older-ap'),
).not.toBeInTheDocument();
});
});
Expand Down
Loading
Loading