chore(dgx): snapshot consolidation WIP pour transfert poc DGX
Regroupe le WIP non committé requis pour le clone/runtime DGX (Option A) : - api_stream.py : préflight replay + smoke santé modèles + handler 403 WP-B - de-hardcode VLM : vlm_config, gpu/*, vram_orchestrator, ollama_manager - stream_processor, semantic_matcher, agent_chat (app/planner/intent) - workflows.db (acquis ; le transfert artifacts le mettra à jour + rewrite chemins) - docs : plans DGX, benchmarks VLM/grounders, recherche SOTA, coordination 8 juin Snapshot destiné à la branche poc-dgx poussée sur Gitea pour cloner le DGX. Scan anti-secret : clean. graphify (repo embarqué) exclu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from core.detection.vlm_config import get_reasoning_model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Répertoires par défaut à scanner pour les workflows
|
||||
@@ -31,10 +33,72 @@ DEFAULT_WORKFLOW_DIRS = [
|
||||
|
||||
# Configuration Ollama par défaut
|
||||
DEFAULT_OLLAMA_ENDPOINT = "http://localhost:11434"
|
||||
DEFAULT_OLLAMA_MODEL = "qwen2.5:7b"
|
||||
DEFAULT_LLM_TIMEOUT = 10 # secondes
|
||||
|
||||
|
||||
def _default_ollama_model() -> str:
|
||||
return get_reasoning_model()
|
||||
|
||||
|
||||
DEFAULT_OLLAMA_MODEL = _default_ollama_model()
|
||||
|
||||
_WORKFLOW_TEXT_KEYS = {
|
||||
"action_type",
|
||||
"description",
|
||||
"expected_window_title",
|
||||
"label",
|
||||
"name",
|
||||
"required_texts",
|
||||
"required_window_title",
|
||||
"tags",
|
||||
"target_text",
|
||||
"text",
|
||||
"title_contains",
|
||||
"title_pattern",
|
||||
"type",
|
||||
"value",
|
||||
"vlm_description",
|
||||
"window_title",
|
||||
}
|
||||
|
||||
_WORKFLOW_TEXT_SKIP_KEYS = {
|
||||
"_prototype_vector",
|
||||
"bbox",
|
||||
"bounding_box",
|
||||
"embedding",
|
||||
"position",
|
||||
"position_x",
|
||||
"position_y",
|
||||
"vector",
|
||||
}
|
||||
|
||||
_TOKEN_SYNONYMS = {
|
||||
"blocnotes": ("bloc", "notes"),
|
||||
"blocnote": ("bloc", "notes"),
|
||||
"notepad": ("bloc", "notes"),
|
||||
"sauvegarde": ("enregistrer",),
|
||||
"sauvegarder": ("enregistrer",),
|
||||
"sauvegardes": ("enregistrer",),
|
||||
"save": ("enregistrer",),
|
||||
"saved": ("enregistrer",),
|
||||
"saving": ("enregistrer",),
|
||||
"enregistre": ("enregistrer",),
|
||||
"enregistres": ("enregistrer",),
|
||||
"enregistrez": ("enregistrer",),
|
||||
}
|
||||
|
||||
_IMPORTANT_ACTION_TOKENS = {
|
||||
"annuler",
|
||||
"dialogue",
|
||||
"ecraser",
|
||||
"enregistrer",
|
||||
"fichier",
|
||||
"ouvrir",
|
||||
"popup",
|
||||
"remplacer",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowMatch:
|
||||
"""Résultat d'un matching de workflow."""
|
||||
@@ -88,7 +152,7 @@ class SemanticMatcher:
|
||||
workflows_dir: Union[str, List[str], None] = None,
|
||||
use_embeddings: bool = True,
|
||||
use_llm: bool = True,
|
||||
llm_model: str = DEFAULT_OLLAMA_MODEL,
|
||||
llm_model: Optional[str] = None,
|
||||
llm_endpoint: str = DEFAULT_OLLAMA_ENDPOINT,
|
||||
llm_timeout: int = DEFAULT_LLM_TIMEOUT,
|
||||
auto_reload_interval: int = 60,
|
||||
@@ -101,7 +165,7 @@ class SemanticMatcher:
|
||||
Peut être un str (un seul répertoire) ou une liste.
|
||||
use_embeddings: Utiliser les embeddings pour le matching (compatibilité)
|
||||
use_llm: Activer le matching sémantique via Ollama LLM
|
||||
llm_model: Modèle Ollama à utiliser (défaut: qwen2.5:7b)
|
||||
llm_model: Modèle Ollama à utiliser (défaut: modèle reasoning central)
|
||||
llm_endpoint: Endpoint Ollama (défaut: http://localhost:11434)
|
||||
llm_timeout: Timeout pour les appels LLM en secondes
|
||||
auto_reload_interval: Intervalle en secondes pour vérifier les nouveaux workflows (0 = désactivé)
|
||||
@@ -121,7 +185,7 @@ class SemanticMatcher:
|
||||
|
||||
self.use_embeddings = use_embeddings
|
||||
self.use_llm = use_llm
|
||||
self.llm_model = llm_model
|
||||
self.llm_model = llm_model or _default_ollama_model()
|
||||
self.llm_endpoint = llm_endpoint
|
||||
self.llm_timeout = llm_timeout
|
||||
|
||||
@@ -181,7 +245,11 @@ class SemanticMatcher:
|
||||
Nombre de workflows chargés
|
||||
"""
|
||||
count = 0
|
||||
for workflow_path in workflows_dir.glob("*.json"):
|
||||
workflow_paths = sorted(
|
||||
workflows_dir.rglob("*.json"),
|
||||
key=lambda p: (len(p.relative_to(workflows_dir).parts), str(p)),
|
||||
)
|
||||
for workflow_path in workflow_paths:
|
||||
try:
|
||||
with open(workflow_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
@@ -298,8 +366,46 @@ class SemanticMatcher:
|
||||
action_type = action.get("type", "")
|
||||
keywords.add(action_type)
|
||||
|
||||
# Workflows appris: les signaux utiles vivent souvent dans les nodes,
|
||||
# conditions et templates, pas seulement dans les tags/edges.
|
||||
for value in self._iter_workflow_text_values(workflow_data):
|
||||
keywords.update(self._tokenize(value))
|
||||
|
||||
return list(keywords)
|
||||
|
||||
def _iter_workflow_text_values(
|
||||
self,
|
||||
value: Any,
|
||||
parent_key: str = "",
|
||||
) -> List[str]:
|
||||
"""Extraire les textes courts utiles au matching depuis un workflow.
|
||||
|
||||
On évite les champs volumineux ou numériques (embeddings, bbox), mais on
|
||||
garde les titres de fenêtres, labels, valeurs et descriptions d'actions.
|
||||
"""
|
||||
texts: List[str] = []
|
||||
|
||||
if isinstance(value, dict):
|
||||
for key, child in value.items():
|
||||
key_lower = str(key).lower()
|
||||
if key_lower in _WORKFLOW_TEXT_SKIP_KEYS:
|
||||
continue
|
||||
if key_lower in _WORKFLOW_TEXT_KEYS and isinstance(child, str):
|
||||
texts.append(child)
|
||||
elif isinstance(child, (dict, list)):
|
||||
texts.extend(self._iter_workflow_text_values(child, key_lower))
|
||||
return texts
|
||||
|
||||
if isinstance(value, list):
|
||||
for item in value:
|
||||
if isinstance(item, str):
|
||||
if parent_key in _WORKFLOW_TEXT_KEYS:
|
||||
texts.append(item)
|
||||
elif isinstance(item, (dict, list)):
|
||||
texts.extend(self._iter_workflow_text_values(item, parent_key))
|
||||
|
||||
return texts
|
||||
|
||||
def _tokenize(self, text: str) -> List[str]:
|
||||
"""Tokeniser un texte en mots-clés."""
|
||||
# Normaliser
|
||||
@@ -319,7 +425,17 @@ class SemanticMatcher:
|
||||
'of', 'with', 'by', 'from', 'is', 'are', 'was', 'were', 'be', 'been'
|
||||
}
|
||||
|
||||
return [w for w in words if len(w) > 2 and w not in stop_words]
|
||||
tokens: List[str] = []
|
||||
for word in words:
|
||||
if len(word) <= 2 or word in stop_words:
|
||||
continue
|
||||
replacement = _TOKEN_SYNONYMS.get(word)
|
||||
if replacement:
|
||||
tokens.extend(replacement)
|
||||
else:
|
||||
tokens.append(word)
|
||||
|
||||
return tokens
|
||||
|
||||
# =========================================================================
|
||||
# Matching LLM (Ollama)
|
||||
@@ -654,6 +770,11 @@ Réponds UNIQUEMENT au format JSON, sans texte avant ni après:
|
||||
if intersection:
|
||||
reasons.append(f"keywords:{','.join(intersection)}")
|
||||
|
||||
important = intersection & _IMPORTANT_ACTION_TOKENS
|
||||
if important:
|
||||
score += 0.2
|
||||
reasons.append(f"action_tokens:{','.join(sorted(important))}")
|
||||
|
||||
# 4. Matching de la description
|
||||
if metadata.description:
|
||||
desc_tokens = set(self._tokenize(metadata.description))
|
||||
|
||||
Reference in New Issue
Block a user