● DevStudio — co-pilote IA
DevStudio transforme une description en langage naturel en code exécutable, avec validation HITL à chaque étape critique. → Ouvrir DevStudio · Guide complet
@léger@extrême# Démarrer
./start.sh
# Puis : http://localhost:8000/ui/studio
# Exemple de demande
"Crée une API FastAPI Todo avec SQLite, CRUD complet"
1 · Quickstart 3 commandes
.venv/bin/python -m scripts.init_keys
PASSWORD=ton-pw .venv/bin/python -m scripts.seed_users
./start.sh # ou start.bat / .\start.ps1
Puis ouvre http://127.0.0.1:8000.
Le 1er workflow utile sans config : kpi_dashboard_refresh (introspecte
tes apps Jaikit locales).
2 · Concepts en 30 secondes
- Workflow = un fichier YAML dans
workflows/. - Step = une étape qui invoque un noeud avec des inputs.
- Noeud = brique réutilisable (HTTP, LLM, Gmail, fs, …) — voir §5.
- Run = une exécution d'un workflow, avec status / steps / outputs persistés.
- HITL gate = pause humaine quand confidence LLM < seuil — voir §7.
- Trigger = ce qui lance un workflow :
cron,interval,date,manual,webhook.
3 · Format YAML d'un workflow
id: my_workflow # obligatoire — référence unique
name: Description courte
description: |
Documentation libre (multiligne).
owner: admin # qui peut éditer
visibility: admin # admin | editor | viewer
tags: [demo, llm]
triggers: # 0..N triggers
- type: cron
expr: "0 6 * * *" # tous les jours 06:00
timezone: Europe/Paris
inputs: # paramètres au lancement manuel
brand:
type: str
default: "renault"
env: # variables injectées dans tous les steps
OUT_DIR: ./data/exports
steps:
- id: fetch
node: http.rss
with:
urls: ["https://${{ inputs.brand }}.com/news.rss"]
- id: extract_each
node: llm.extract
foreach: ${{ steps.fetch.outputs.items }}
parallel: 4 # exécution concurrente bornée
with:
content: ${{ each.summary }}
schema:
type: object
properties: { name: {type: string} }
on_low_confidence:
hitl_gate:
timeout_minutes: 1440
on_timeout: skip
on_failure: # exécuté si un step plante
- id: alert
node: notif.telegram
with:
message: "❌ Run ${{ run.id }} a échoué"
4 · Interpolation ${{ … }}
| Expression | Description |
|---|---|
${{ env.X }} | variable du bloc env: |
${{ inputs.X }} | input runtime / défaut |
${{ steps.<id>.outputs.<key> }} | sortie d'un step précédent |
${{ each.X }} | élément courant dans un foreach |
${{ run.id }} / ${{ run.workflow_id }} / ${{ run.started_at }} | métadonnées du run |
${{ secrets.NAME }} | valeur Fernet déchiffrée à la volée |
${{ X | length }} | filtre — aussi: json, upper, lower |
${{ steps.fetch.outputs.items[0].link }} | indexation tableau / clé |
5 · Catalogue des 25 noeuds
| Nom | Inputs (résumé) | Outputs (résumé) |
|---|---|---|
| browser.scenario | scenario_path, record_video, output_dir, extra | status, artifacts, detail |
| fs.read | path, encoding | path, content, bytes |
| fs.write | path, content, encoding, append | path, bytes |
| gmail.archive | message_id | ok |
| gmail.label | message_id, add_labels, remove_labels | ok |
| gmail.list_inbox | query, max | messages |
| gmail.send | to, subject, body, from_addr | message_id |
| http.get | url, headers, params, timeout_seconds, follow_redirects | status, body, headers |
| http.post | url, headers, json_body, data, timeout_seconds, follow_redirects | status, body, headers |
| http.rss | urls, timeout_seconds | items, errors |
| jaikit.car_canary_diff | items, db_path, id_field | new_items, known_count |
| jaikit.car_publish | payload, slot, api_base, db_path | mode, entity_id, detail |
| jaikit.kpi_collect | app, root | app, snapshot, available, detail |
| jaikit.kpi_render | snapshots, out_path | written, bytes |
| jaikit.patterns_ingest | url, theme, title, snippet | mode, accepted, detail |
| jaikit.patterns_search | query, tags, top_k, include_legacy | hits, count, bundle_markdown, backend |
| jaikit.vibe_evolve | action, prompt, project_path, patterns_context, contract, max_iters … | status, ok, job_id, project_path, vibe_status, result … |
| llm.classify | text, classes, model, system, temperature | label, confidence, rationale, raw |
| llm.completion | prompt, model, system, temperature, max_tokens, extra | text, model, usage |
| llm.extract | content, schema_, model, instructions, temperature | data, confidence, raw |
| notif.ntfy | message, topic, server, title, priority, tags … | sent, server, topic |
| notif.smtp | to, subject, body, host, port, username … | sent |
| notif.telegram | message, chat_id, parse_mode, disable_notification | sent, message_id |
| search.ddg | query, top_k, region | hits |
| search.searxng | query, top_k, instance_url, categories | hits |
6 · Exemples prêts à copier
6.1 — Télécharger un RSS et écrire un résumé
id: rss_to_file
triggers: [{type: manual}]
inputs: {}
env: {}
steps:
- id: fetch
node: http.rss
with:
urls: ["https://hnrss.org/frontpage"]
- id: write_titles
node: fs.write
with:
path: ./data/exports/titles.txt
content: |
{% for it in fetch.items %}
Run ${{ run.id }} a récupéré ${{ steps.fetch.outputs.items | length }} items.
6.2 — Classifier un texte LLM avec HITL si confidence basse
id: classify_text
triggers: [{type: manual}]
inputs:
text: {type: str, default: "Le serveur est tombé en panne ce matin"}
env: {}
steps:
- id: classify
node: llm.classify
with:
text: ${{ inputs.text }}
classes: [critical, info, spam]
on_low_confidence:
hitl_gate:
timeout_minutes: 60
on_timeout: skip
- id: log
node: fs.write
with:
path: ./data/exports/classify.txt
content: "label=${{ steps.classify.outputs.label }} conf=${{ steps.classify.outputs.confidence }}"
6.3 — Foreach parallèle (httpx parallèle borné)
steps:
- id: download_many
node: http.get
foreach: ${{ inputs.urls }}
parallel: 8 # max 8 requêtes simultanées
with:
url: ${{ each }}
timeout_seconds: 30
6.4 — Notification Telegram conditionnelle
steps:
- id: alert
node: notif.telegram
if: ${{ steps.classify.outputs.label }} # truthy
with:
message: "Label: ${{ steps.classify.outputs.label }}"
6.5 — Trigger interval (toutes les 30 min)
triggers:
- type: interval
every: "30m" # s / m / h / d
7 · HITL — pattern HITL.001 (5 stratégies)
Quand un noeud LLM retourne confidence < 0.8 (seuil par défaut),
le runner crée un gate et bloque ce step. L'humain ouvre /ui/hitl et choisit
parmi 5 stratégies du pattern HITL.001 :
- verify — demander à un autre LLM de re-vérifier
- decompose — décomposer en sous-étapes
- challenge — challenger le contrat d'extraction
- request_help — l'humain édite manuellement le résultat (champ edits)
- accept_limit — accepter la limite, noter l'incident
- skip — sauter cette itération sans fail
La décision est persistée dans hitl_gates.decision_json + data/runs/<run_id>.jsonl (audit).
8 · Chat HITL (LLM assist)
Sur la page d'un gate (/ui/hitl/<id>) un panneau de chat permet de
dialoguer avec un LLM faible (Groq par défaut) pour t'aider à décider. Inspiré
du chat HITL de jaikit-sysmonitor + principes jaikit-patterns.
- L'évidence du gate est passée en system prompt au LLM.
- L'historique est stocké en
data/hitl_chats/<gate_id>.jsonl. - Bouton ↺ Reset pour repartir d'une nouvelle conversation.
- Si
LITELLM_API_KEYnon configuré : le chat marche en mode scratchpad (notes humaines sans réponse LLM).
9 · Scheduler
APScheduler démarre automatiquement avec uvicorn. Il enregistre :
- tous les workflows YAML actifs (déplacés dans
workflows/_disabled/= pause) - le job système
system_backuptous les jours à 03:17 UTC (rotation = 14 backups dansdata/backups/)
Pour lancer un workflow à la main : bouton ▶ Run now sur sa page,
endpoint POST /api/workflows/<id>/trigger (auth requise), ou CLI :
.venv/bin/python -m scripts.run_workflow <workflow_id>
10 · Credentials chiffrés (KV Fernet)
# Poser
curl -b cookies.txt -X PUT http://127.0.0.1:8000/api/credentials \
-H 'content-type: application/json' \
-d '{"name":"TELEGRAM_BOT_TOKEN","value":"123:abc"}'
# Lister (noms uniquement, jamais les valeurs)
curl -b cookies.txt http://127.0.0.1:8000/api/credentials
# Supprimer
curl -b cookies.txt -X DELETE http://127.0.0.1:8000/api/credentials/TELEGRAM_BOT_TOKEN
Clé maître stockée dans .env sous FLOWS_MASTER_KEY. Générée par
scripts/init_keys.py. À sauvegarder hors machine — sans elle,
le KV est perdu.
11 · API HTTP
| Méthode | Route | Auth | Description |
|---|---|---|---|
| GET | /health | — | sanity check |
| POST | /api/auth/login | — | {email, password} → cookie |
| POST | /api/auth/logout | session | clear cookie |
| GET | /api/auth/me | — | user courant ou anon |
| GET | /api/workflows | — | liste des YAML |
| GET | /api/workflows/{id} | — | meta + YAML brut |
| POST | /api/workflows/{id}/trigger | admin/editor | lance manuellement |
| GET | /api/runs | — | ?limit= &workflow_id= &status= |
| GET | /api/runs/{id} | — | détail + steps |
| POST | /api/runs/{id}/replay | admin/editor | rejoue mêmes inputs |
| GET | /api/hitl/pending | — | gates en attente |
| GET | /api/hitl/{id} | — | détail gate |
| POST | /api/hitl/{id}/decide | — | soumet décision |
| GET | /api/hitl/{id}/chat | — | historique du chat HITL |
| POST | /api/hitl/{id}/chat | — | {message} → réponse LLM |
| GET/PUT/DELETE | /api/credentials | admin | CRUD KV Fernet |
12 · CLI
# Lancer un workflow
.venv/bin/python -m scripts.run_workflow hello_world
.venv/bin/python -m scripts.run_workflow ./workflows/foo.yaml --inputs '{"x":1}'
# Seed admin
PASSWORD=xxx EMAIL=me@x ROLE=admin .venv/bin/python -m scripts.seed_users
# Générer / regénérer les clés crypto
.venv/bin/python -m scripts.init_keys
.venv/bin/python -m scripts.init_keys --force
# Backup manuel SQLite (rotation BACKUP_KEEP, défaut 14)
.venv/bin/python -m scripts.backup
13 · Sécurité prod (checklist)
scripts/init_keys.pyexécuté,.enven chmod 600, backup deFLOWS_MASTER_KEY.- Reverse proxy TLS (nginx/caddy) devant uvicorn.
secure=Truesur le cookie session quand HTTPS actif (éditeflows/core/auth.py).- Bind sur
127.0.0.1+ reverse proxy, jamais0.0.0.0direct. - Job
system_backupvérifié après 24h (data/backups/). - Encore TODO : rate-limit login, CSRF forms HTML, audit log mutations credentials.
14 · Ajouter un noeud custom
# flows/nodes/mycat/mything.py
from pydantic import BaseModel
from flows.nodes.base import Node
class _In(BaseModel):
x: int
class _Out(BaseModel):
y: int
class MyThingNode(Node):
name = "mycat.mything"
InputSchema, OutputSchema = _In, _Out
async def run(self, ctx, inp: _In) -> _Out:
return _Out(y=inp.x * 2)
Au prochain boot, l'auto-discovery l'enregistre automatiquement (voir
flows/nodes/__init__.py). Ensuite tu peux l'utiliser :
steps:
- id: double
node: mycat.mything
with: { x: 21 }
# steps.double.outputs.y == 42
15 · Troubleshooting
| Symptôme | Cause | Fix |
|---|---|---|
| CredentialsError: FLOWS_MASTER_KEY not set | clé non générée | python -m scripts.init_keys |
| 401 sur /trigger | pas connecté | login (admin/editor) |
| unknown node 'foo.bar' | noeud absent/typo | list_nodes() dans REPL |
| Run bloque sur HITL | gate ouvert sans décision | /ui/hitl ou attendre timeout |
| Workflow ne se relance pas | scheduler off (mode CLI) | démarrer uvicorn |
| Port 8000 occupé | autre proc | start.sh / start.bat kill auto |
16 · FAQ
- Comment changer de port ?
- Édite
FLOWS_PORTdans.env. Les scriptsstart.*le lisent et killent le proc qui occupe ce port. - Comment désactiver un workflow ?
- Déplace son YAML vers
workflows/_disabled/et redémarre uvicorn. - Comment voir le YAML brut d'un workflow ?
- /ui/workflows/<id> affiche le contenu complet. API : GET /api/workflows/<id>.
- Pourquoi pas d'Anthropic SDK ?
- Règle §1 Charte Jaikit. Tout LLM passe par LiteLLM proxy (Groq par défaut, Ollama fallback) pour rester gratuit + détachable.
- Combien d'utilisateurs supporté ?
- RBAC posé (viewer / editor / admin / partner). MVP = 1 admin. CRUD users à activer si revente envisagée.