mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-22 11:51:04 +02:00
Merge 712dbdef1f into 49e68b49d5
This commit is contained in:
commit
6ecf83edc8
6 changed files with 58 additions and 5 deletions
|
|
@ -207,7 +207,7 @@ async def execute_pending_text_chat_turn(
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
)
|
)
|
||||||
raise TextChatSessionExecutionError(
|
raise TextChatSessionExecutionError(
|
||||||
"Failed to execute text chat assistant turn"
|
f"Failed to execute text chat assistant turn: {e}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
completed_session_data = normalize_text_chat_session_data(text_session.session_data)
|
completed_session_data = normalize_text_chat_session_data(text_session.session_data)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from api.services.workflow.text_chat_session_service import (
|
||||||
TextChatTurnNotFoundError,
|
TextChatTurnNotFoundError,
|
||||||
_reload_text_chat_session,
|
_reload_text_chat_session,
|
||||||
build_pending_text_chat_turn,
|
build_pending_text_chat_turn,
|
||||||
|
execute_pending_text_chat_turn,
|
||||||
truncate_text_chat_future_turns,
|
truncate_text_chat_future_turns,
|
||||||
validate_text_chat_turn_cursor,
|
validate_text_chat_turn_cursor,
|
||||||
)
|
)
|
||||||
|
|
@ -77,6 +78,36 @@ async def test_reload_text_chat_session_uses_run_id_to_resolve_organization(
|
||||||
get_text_session.assert_awaited_once_with(123, organization_id=77)
|
get_text_session.assert_awaited_once_with(123, organization_id=77)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_execute_pending_turn_surfaces_original_exception_message(monkeypatch):
|
||||||
|
session = WorkflowRunTextSessionModel(workflow_run_id=42)
|
||||||
|
session.session_data = {
|
||||||
|
"turns": [{"id": "turn-1", "status": "pending"}],
|
||||||
|
"cursor_turn_id": "turn-1",
|
||||||
|
}
|
||||||
|
session.checkpoint = None
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
text_chat_session_service,
|
||||||
|
"execute_text_chat_pending_turn",
|
||||||
|
AsyncMock(side_effect=RuntimeError("Workflow has 2 start nodes")),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
text_chat_session_service,
|
||||||
|
"_mark_pending_turn_failed",
|
||||||
|
AsyncMock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
TextChatSessionExecutionError, match="Workflow has 2 start nodes"
|
||||||
|
):
|
||||||
|
await execute_pending_text_chat_turn(
|
||||||
|
workflow_id=1,
|
||||||
|
run_id=42,
|
||||||
|
text_session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_reload_text_chat_session_raises_when_run_organization_is_missing(
|
async def test_reload_text_chat_session_raises_when_run_organization_is_missing(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -690,6 +690,7 @@ function RenderWorkflow({
|
||||||
isOpen={isAddNodePanelOpen}
|
isOpen={isAddNodePanelOpen}
|
||||||
onNodeSelect={handleNodeSelect}
|
onNodeSelect={handleNodeSelect}
|
||||||
onClose={() => setIsAddNodePanelOpen(false)}
|
onClose={() => setIsAddNodePanelOpen(false)}
|
||||||
|
nodes={nodes}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VersionHistoryPanel
|
<VersionHistoryPanel
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { EdgeChange, NodeChange } from "@xyflow/system";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import posthog from "posthog-js";
|
import posthog from "posthog-js";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
|
import { useWorkflowStore } from "@/app/workflow/[workflowId]/stores/workflowStore";
|
||||||
import {
|
import {
|
||||||
|
|
@ -306,6 +307,13 @@ export const useWorkflowState = ({
|
||||||
// This avoids a race condition where rfInstance.toObject() may return
|
// This avoids a race condition where rfInstance.toObject() may return
|
||||||
// stale node data if React hasn't re-rendered yet after a store update.
|
// stale node data if React hasn't re-rendered yet after a store update.
|
||||||
const { nodes: currentNodes, edges: currentEdges } = useWorkflowStore.getState();
|
const { nodes: currentNodes, edges: currentEdges } = useWorkflowStore.getState();
|
||||||
|
const triggerCount = currentNodes.filter(
|
||||||
|
(n) => bySpecName.get(n.type)?.category === 'trigger',
|
||||||
|
).length;
|
||||||
|
if (triggerCount > 1) {
|
||||||
|
toast.error('A workflow can only have one trigger. Remove the extra trigger node before saving.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const viewport = rfInstance.current.getViewport();
|
const viewport = rfInstance.current.getViewport();
|
||||||
const flow = { nodes: currentNodes, edges: currentEdges, viewport };
|
const flow = { nodes: currentNodes, edges: currentEdges, viewport };
|
||||||
let result: { versionNumber?: number; versionStatus?: string } | undefined;
|
let result: { versionNumber?: number; versionStatus?: string } | undefined;
|
||||||
|
|
@ -372,6 +380,7 @@ export const useWorkflowState = ({
|
||||||
user,
|
user,
|
||||||
validateWorkflow,
|
validateWorkflow,
|
||||||
applyWorkflowErrors,
|
applyWorkflowErrors,
|
||||||
|
bySpecName,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set up keyboard shortcut for save (Cmd/Ctrl + S)
|
// Set up keyboard shortcut for save (Cmd/Ctrl + S)
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,13 @@ import type { NodeSpec } from '@/client/types.gen';
|
||||||
import { useNodeSpecs } from '@/components/flow/renderer';
|
import { useNodeSpecs } from '@/components/flow/renderer';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
import { NodeType } from './types';
|
import { FlowNode, NodeType } from './types';
|
||||||
|
|
||||||
type AddNodePanelProps = {
|
type AddNodePanelProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onNodeSelect: (nodeType: NodeType) => void;
|
onNodeSelect: (nodeType: NodeType) => void;
|
||||||
|
nodes: FlowNode[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Section ordering and labels. Drives both the category → section title
|
// Section ordering and labels. Drives both the category → section title
|
||||||
|
|
@ -32,10 +33,12 @@ function NodeSection({
|
||||||
title,
|
title,
|
||||||
specs,
|
specs,
|
||||||
onNodeSelect,
|
onNodeSelect,
|
||||||
|
disableTriggers,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
specs: NodeSpec[];
|
specs: NodeSpec[];
|
||||||
onNodeSelect: (nodeType: NodeType) => void;
|
onNodeSelect: (nodeType: NodeType) => void;
|
||||||
|
disableTriggers: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (specs.length === 0) return null;
|
if (specs.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
|
|
@ -46,12 +49,15 @@ function NodeSection({
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{specs.map((spec) => {
|
{specs.map((spec) => {
|
||||||
const Icon = resolveIcon(spec.icon);
|
const Icon = resolveIcon(spec.icon);
|
||||||
|
const disabled = disableTriggers && spec.category === 'trigger';
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={spec.name}
|
key={spec.name}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-start p-4 h-auto hover:bg-accent/50 transition-colors"
|
className="w-full justify-start p-4 h-auto hover:bg-accent/50 transition-colors"
|
||||||
onClick={() => onNodeSelect(spec.name as NodeType)}
|
onClick={() => onNodeSelect(spec.name as NodeType)}
|
||||||
|
disabled={disabled}
|
||||||
|
title={disabled ? 'A trigger already exists in this workflow' : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="bg-muted p-2 rounded-lg mr-3 border border-border">
|
<div className="bg-muted p-2 rounded-lg mr-3 border border-border">
|
||||||
|
|
@ -74,8 +80,8 @@ function NodeSection({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodePanelProps) {
|
export default function AddNodePanel({ isOpen, onNodeSelect, onClose, nodes }: AddNodePanelProps) {
|
||||||
const { specs } = useNodeSpecs();
|
const { specs, bySpecName } = useNodeSpecs();
|
||||||
|
|
||||||
// Group registered specs by category, preserving the SECTION_ORDER.
|
// Group registered specs by category, preserving the SECTION_ORDER.
|
||||||
// Adding a new node type with a new spec.category just shows up here.
|
// Adding a new node type with a new spec.category just shows up here.
|
||||||
|
|
@ -86,6 +92,11 @@ export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodeP
|
||||||
}));
|
}));
|
||||||
}, [specs]);
|
}, [specs]);
|
||||||
|
|
||||||
|
const hasTrigger = useMemo(
|
||||||
|
() => nodes.some((n) => bySpecName.get(n.type)?.category === 'trigger'),
|
||||||
|
[nodes, bySpecName],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Escape' && isOpen) {
|
if (event.key === 'Escape' && isOpen) {
|
||||||
|
|
@ -128,6 +139,7 @@ export default function AddNodePanel({ isOpen, onNodeSelect, onClose }: AddNodeP
|
||||||
title={title}
|
title={title}
|
||||||
specs={specs}
|
specs={specs}
|
||||||
onNodeSelect={onNodeSelect}
|
onNodeSelect={onNodeSelect}
|
||||||
|
disableTriggers={hasTrigger}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue