feat: DAG executor async + intégration IA/LLM dans le VWB
- DAGExecutor : exécution workflow par graphe de dépendances,
étapes LLM parallèles, UI séquentielles, injection ${step.result}
- LLMActionHandler : analyze_text, translate, extract_data, generate_text
via Ollama /api/chat (qwen3-vl:8b, temperature 0.1)
- VWB palette : catégorie "IA / LLM" avec 4 actions draggables
- VWB propriétés : éditeurs pour chaque action LLM (modèle, prompt, langue)
- VWB endpoint : POST /api/v3/workflow/<id>/execute-dag
- 37 tests unitaires DAG executor (tous passent)
- Fix log spam cache workflows (info → debug)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -788,6 +788,194 @@ export default function PropertiesPanel({ step, onUpdateParams, onDelete }: Prop
|
||||
</>
|
||||
);
|
||||
|
||||
// === DAG LLM ===
|
||||
case 'llm_analyze':
|
||||
return (
|
||||
<>
|
||||
<div className="prop-section-title">
|
||||
<span className="icon">🔬</span> Analyser texte (LLM)
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Texte à analyser</label>
|
||||
<textarea
|
||||
value={String(params.text || '')}
|
||||
onChange={(e) => updateParam('text', e.target.value)}
|
||||
rows={4}
|
||||
placeholder={"Texte direct ou référence : ${etape_prec.result}"}
|
||||
/>
|
||||
<small className="field-hint">Utilisez {'${step_id.result}'} pour injecter le résultat d'une étape précédente</small>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Instruction</label>
|
||||
<textarea
|
||||
value={String(params.instruction || 'Analyse et résume ce texte.')}
|
||||
onChange={(e) => updateParam('instruction', e.target.value)}
|
||||
rows={2}
|
||||
placeholder="Analyse et résume ce texte."
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Modèle Ollama (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.model || '')}
|
||||
onChange={(e) => updateParam('model', e.target.value)}
|
||||
placeholder="Par défaut : qwen3-vl:8b"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Température ({Number(params.temperature || 0.1).toFixed(1)})</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={Number(params.temperature || 0.1)}
|
||||
onChange={(e) => updateParam('temperature', Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'llm_translate':
|
||||
return (
|
||||
<>
|
||||
<div className="prop-section-title">
|
||||
<span className="icon">🌐</span> Traduire (LLM)
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Texte à traduire</label>
|
||||
<textarea
|
||||
value={String(params.text || '')}
|
||||
onChange={(e) => updateParam('text', e.target.value)}
|
||||
rows={4}
|
||||
placeholder={"Texte direct ou référence : ${etape_prec.result}"}
|
||||
/>
|
||||
<small className="field-hint">Utilisez {'${step_id.result}'} pour injecter le résultat d'une étape précédente</small>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Langue cible</label>
|
||||
<select
|
||||
value={String(params.target_lang || 'français')}
|
||||
onChange={(e) => updateParam('target_lang', e.target.value)}
|
||||
>
|
||||
<option value="français">Français</option>
|
||||
<option value="anglais">Anglais</option>
|
||||
<option value="espagnol">Espagnol</option>
|
||||
<option value="allemand">Allemand</option>
|
||||
<option value="chinois">Chinois</option>
|
||||
<option value="arabe">Arabe</option>
|
||||
<option value="japonais">Japonais</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Langue source (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.source_lang || '')}
|
||||
onChange={(e) => updateParam('source_lang', e.target.value)}
|
||||
placeholder="Auto-détection"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Modèle Ollama (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.model || '')}
|
||||
onChange={(e) => updateParam('model', e.target.value)}
|
||||
placeholder="Par défaut : qwen3-vl:8b"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'llm_extract_data':
|
||||
return (
|
||||
<>
|
||||
<div className="prop-section-title">
|
||||
<span className="icon">🗂️</span> Extraire données (LLM)
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Texte source</label>
|
||||
<textarea
|
||||
value={String(params.text || '')}
|
||||
onChange={(e) => updateParam('text', e.target.value)}
|
||||
rows={4}
|
||||
placeholder={"Texte direct ou référence : ${etape_prec.result}"}
|
||||
/>
|
||||
<small className="field-hint">Utilisez {'${step_id.result}'} pour injecter le résultat d'une étape précédente</small>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Schéma d'extraction (JSON)</label>
|
||||
<textarea
|
||||
value={String(params.schema || '{\n "nom": "Nom complet",\n "date": "Date au format JJ/MM/AAAA"\n}')}
|
||||
onChange={(e) => updateParam('schema', e.target.value)}
|
||||
rows={5}
|
||||
placeholder={'{\n "nom": "Nom complet",\n "date": "Date"\n}'}
|
||||
/>
|
||||
<small className="field-hint">Clés = champs à extraire, valeurs = descriptions</small>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Modèle Ollama (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.model || '')}
|
||||
onChange={(e) => updateParam('model', e.target.value)}
|
||||
placeholder="Par défaut : qwen3-vl:8b"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'llm_generate':
|
||||
return (
|
||||
<>
|
||||
<div className="prop-section-title">
|
||||
<span className="icon">✍️</span> Générer texte (LLM)
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Prompt de génération</label>
|
||||
<textarea
|
||||
value={String(params.prompt || '')}
|
||||
onChange={(e) => updateParam('prompt', e.target.value)}
|
||||
rows={4}
|
||||
placeholder="Rédige un email de relance à partir des données suivantes..."
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Contexte (optionnel)</label>
|
||||
<textarea
|
||||
value={String(params.context || '')}
|
||||
onChange={(e) => updateParam('context', e.target.value)}
|
||||
rows={3}
|
||||
placeholder={"Contexte additionnel ou référence : ${etape_prec.result}"}
|
||||
/>
|
||||
<small className="field-hint">Utilisez {'${step_id.result}'} pour injecter le résultat d'une étape précédente</small>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Modèle Ollama (optionnel)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(params.model || '')}
|
||||
onChange={(e) => updateParam('model', e.target.value)}
|
||||
placeholder="Par défaut : qwen3-vl:8b"
|
||||
/>
|
||||
</div>
|
||||
<div className="prop-field">
|
||||
<label>Température ({Number(params.temperature || 0.1).toFixed(1)})</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={Number(params.temperature || 0.1)}
|
||||
onChange={(e) => updateParam('temperature', Number(e.target.value))}
|
||||
/>
|
||||
<small className="field-hint">0 = précis/déterministe, 1 = créatif/varié</small>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
// === BDD ===
|
||||
case 'db_save_data':
|
||||
return (
|
||||
|
||||
@@ -45,6 +45,23 @@ function StepNode({ data, selected }: StepNodeProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aperçu des paramètres LLM DAG */}
|
||||
{step.action_type === 'llm_translate' && step.parameters?.target_lang && (
|
||||
<div className="step-node-params">
|
||||
{`→ ${step.parameters.target_lang}`}
|
||||
</div>
|
||||
)}
|
||||
{step.action_type === 'llm_generate' && typeof step.parameters?.prompt === 'string' && step.parameters.prompt.length > 0 && (
|
||||
<div className="step-node-params">
|
||||
{`"${step.parameters.prompt.slice(0, 25)}${step.parameters.prompt.length > 25 ? '...' : ''}"`}
|
||||
</div>
|
||||
)}
|
||||
{step.action_type === 'llm_analyze' && typeof step.parameters?.instruction === 'string' && step.parameters.instruction.length > 0 && (
|
||||
<div className="step-node-params">
|
||||
{`"${step.parameters.instruction.slice(0, 25)}${step.parameters.instruction.length > 25 ? '...' : ''}"`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!step.anchor_id && action?.needsAnchor && (
|
||||
<div className="step-node-warning">
|
||||
Ancre requise
|
||||
|
||||
@@ -260,3 +260,49 @@ export async function submitReview(
|
||||
}> {
|
||||
return request('POST', `/workflow/${workflowId}/review`, { status, feedback });
|
||||
}
|
||||
|
||||
// DAG Execution — Exécution parallèle avec étapes LLM
|
||||
export interface DAGEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface DAGExecutionResult {
|
||||
success: boolean;
|
||||
steps: Record<string, {
|
||||
step_id: string;
|
||||
step_type: string;
|
||||
status: string;
|
||||
result: unknown;
|
||||
error: string | null;
|
||||
duration: number | null;
|
||||
}>;
|
||||
results: Record<string, unknown>;
|
||||
errors: string[];
|
||||
duration_seconds: number;
|
||||
}
|
||||
|
||||
export async function executeDag(
|
||||
workflowId: string,
|
||||
edges: DAGEdge[],
|
||||
options?: {
|
||||
timeout?: number;
|
||||
model?: string;
|
||||
ollama_endpoint?: string;
|
||||
}
|
||||
): Promise<{ execution: DAGExecutionResult }> {
|
||||
return request('POST', `/workflow/${workflowId}/execute-dag`, {
|
||||
edges,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDagStatus(
|
||||
workflowId: string
|
||||
): Promise<{
|
||||
completed: boolean;
|
||||
status: DAGExecutionResult | null;
|
||||
message?: string;
|
||||
}> {
|
||||
return request('GET', `/workflow/${workflowId}/dag-status`);
|
||||
}
|
||||
|
||||
@@ -48,13 +48,18 @@ export type ActionType =
|
||||
| 'db_save_data'
|
||||
| 'db_read_data'
|
||||
| 'verify_element_exists'
|
||||
| 'verify_text_content';
|
||||
| 'verify_text_content'
|
||||
// === DAG LLM — étapes IA exécutées via le DAGExecutor ===
|
||||
| 'llm_analyze'
|
||||
| 'llm_translate'
|
||||
| 'llm_extract_data'
|
||||
| 'llm_generate';
|
||||
|
||||
export interface ActionDefinition {
|
||||
type: ActionType;
|
||||
label: string;
|
||||
icon: string;
|
||||
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'validation';
|
||||
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'llm' | 'validation';
|
||||
needsAnchor: boolean;
|
||||
params: string[];
|
||||
}
|
||||
@@ -99,6 +104,12 @@ export const ACTIONS: ActionDefinition[] = [
|
||||
{ type: 'db_save_data', label: 'Sauvegarder en BDD', icon: '💿', category: 'data', needsAnchor: false, params: ['table', 'data'] },
|
||||
{ type: 'db_read_data', label: 'Lire depuis BDD', icon: '📖', category: 'data', needsAnchor: false, params: ['query', 'variable_name'] },
|
||||
|
||||
// === DAG LLM — Actions IA via DAGExecutor (parallèle, Ollama) ===
|
||||
{ type: 'llm_analyze', label: 'Analyser texte', icon: '🔬', category: 'llm', needsAnchor: false, params: ['text', 'instruction', 'model'] },
|
||||
{ type: 'llm_translate', label: 'Traduire', icon: '🌐', category: 'llm', needsAnchor: false, params: ['text', 'target_lang', 'model'] },
|
||||
{ type: 'llm_extract_data', label: 'Extraire données', icon: '🗂️', category: 'llm', needsAnchor: false, params: ['text', 'schema', 'model'] },
|
||||
{ type: 'llm_generate', label: 'Générer texte', icon: '✍️', category: 'llm', needsAnchor: false, params: ['prompt', 'context', 'model'] },
|
||||
|
||||
// === VALIDATION ===
|
||||
{ type: 'verify_element_exists', label: 'Vérifier présence', icon: '✅', category: 'validation', needsAnchor: true, params: ['timeout_ms'] },
|
||||
{ type: 'verify_text_content', label: 'Vérifier texte', icon: '🔍', category: 'validation', needsAnchor: true, params: ['expected_text'] },
|
||||
@@ -111,6 +122,7 @@ export const ACTION_CATEGORIES = {
|
||||
data: { label: 'Données', icon: '📊' },
|
||||
logic: { label: 'Logique', icon: '🔀' },
|
||||
ai: { label: 'IA', icon: '🤖' },
|
||||
llm: { label: 'IA / LLM', icon: '🧪' },
|
||||
validation: { label: 'Validation', icon: '✅' },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user