mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
chat_bio
This commit is contained in:
parent
d6949309f1
commit
a0e59442f4
@ -79,6 +79,10 @@ uvicorn api:app --host 0.0.0.0 --port 8000 --reload
|
|||||||
- **API** : http://localhost:8000
|
- **API** : http://localhost:8000
|
||||||
- **Endpoint IA** : http://localhost:8000/ask?q=votre_question
|
- **Endpoint IA** : http://localhost:8000/ask?q=votre_question
|
||||||
- **Documentation** : http://localhost:8000/docs
|
- **Documentation** : http://localhost:8000/docs
|
||||||
|
- **Santé** : http://localhost:8000/health — renvoie `status`, `ollama_url`, `llm_model`, métadonnées vault, et `observability.langfuse_enabled`.
|
||||||
|
- **Recharger le vault à chaud** : `POST http://localhost:8000/reload-vault` — à appeler après création/modification d'une note dans `vault-grasbot/` (sinon `load_vault()` reste en cache mémoïsé jusqu'au prochain redémarrage d'uvicorn).
|
||||||
|
|
||||||
|
> **Tuning du pipeline LLM** : les paramètres Ollama (`num_ctx`, `num_predict`, `think`), la troncature des sources RAG secondaires (`SEARCH_SECONDARY_MAX_CHARS`, `SEARCH_SECONDARY_KEEP_RATIO`), le system prompt anti-hallucination et la note `bio-fernand` sont documentés en détail dans `docs-site-interne/langfuse-observability.md` (section *Tuning du pipeline — 2026-04-23*).
|
||||||
|
|
||||||
## 📊 Ports Utilisés
|
## 📊 Ports Utilisés
|
||||||
|
|
||||||
|
|||||||
@ -105,12 +105,16 @@ Les variables Langfuse **ne sont pas** dans `.env.local` de Next.js — elles ne
|
|||||||
- `relevant_notes` : notes effectivement incluses dans le contexte
|
- `relevant_notes` : notes effectivement incluses dans le contexte
|
||||||
- `system_chars`, `user_chars` : tailles utiles pour debug de fenêtre de contexte
|
- `system_chars`, `user_chars` : tailles utiles pour debug de fenêtre de contexte
|
||||||
- `min_score_threshold` : valeur du `MIN_SCORE` au moment de l'appel
|
- `min_score_threshold` : valeur du `MIN_SCORE` au moment de l'appel
|
||||||
|
- `truncation` : `{ secondary_max_chars, secondary_keep_ratio, truncated_notes: [...] }` —
|
||||||
|
liste des sources rank 2+ résumées automatiquement (avec leur slug, score,
|
||||||
|
taille d'origine et taille tronquée). Vide s'il n'y a eu aucune troncature.
|
||||||
|
|
||||||
### Span `ollama-chat` (type **generation**)
|
### Span `ollama-chat` (type **generation**)
|
||||||
- **input** : `[{role: "system", content}, {role: "user", content}]`
|
- **input** : `[{role: "system", content}, {role: "user", content}]`
|
||||||
- **output** : réponse brute du modèle
|
- **output** : réponse brute du modèle
|
||||||
- **model** : `LLM_MODEL` (ex. `qwen3:8b`)
|
- **model** : `LLM_MODEL` (ex. `qwen3:8b`)
|
||||||
- **model_parameters** : `{temperature: 0.4, num_predict: 512}`
|
- **model_parameters** : `{temperature: 0.4, num_ctx: 8192, num_predict: 1024, think: false}`
|
||||||
|
(voir section "Tuning 2026-04-23" ci-dessous pour le rationnel).
|
||||||
- **usage** : `{input, output, total}` — extraits de `prompt_eval_count` / `eval_count` si Ollama les renvoie
|
- **usage** : `{input, output, total}` — extraits de `prompt_eval_count` / `eval_count` si Ollama les renvoie
|
||||||
- Si réponse vide → span `level: ERROR` avec le payload Ollama brut en metadata.
|
- Si réponse vide → span `level: ERROR` avec le payload Ollama brut en metadata.
|
||||||
|
|
||||||
@ -182,6 +186,39 @@ Si Langfuse tombe en panne ou si l'instrumentation pose un souci :
|
|||||||
- Le client Langfuse envoie les traces **en asynchrone** avec un buffer → bien appeler `flush()` au shutdown pour ne rien perdre (déjà fait via le `lifespan` FastAPI).
|
- Le client Langfuse envoie les traces **en asynchrone** avec un buffer → bien appeler `flush()` au shutdown pour ne rien perdre (déjà fait via le `lifespan` FastAPI).
|
||||||
- **Contenu sensible** : les prompts complets passent dans Langfuse. Vérifier que **le vault ne contient pas d'infos privées** (`visibility: private` est filtré côté search, mais si tu ajoutais un jour un vault mixte public/privé, il faudrait un filtre supplémentaire avant l'envoi à Langfuse).
|
- **Contenu sensible** : les prompts complets passent dans Langfuse. Vérifier que **le vault ne contient pas d'infos privées** (`visibility: private` est filtré côté search, mais si tu ajoutais un jour un vault mixte public/privé, il faudrait un filtre supplémentaire avant l'envoi à Langfuse).
|
||||||
|
|
||||||
|
## Tuning du pipeline — 2026-04-23
|
||||||
|
|
||||||
|
Audit des premières traces après mise en production : les réponses sur les
|
||||||
|
questions biographiques ("qui est Fernand ?") étaient parfois **hallucinées**
|
||||||
|
(âge erroné, statut inventé) et les réponses longues **tronquées** en plein
|
||||||
|
milieu. Quatre ajustements ciblés ont stabilisé le comportement :
|
||||||
|
|
||||||
|
| # | Fichier | Changement | Effet attendu |
|
||||||
|
|---|---------|------------|---------------|
|
||||||
|
| 1 | `search.py` · `generate()` | `num_ctx` explicite à **8192** | Fin de la troncature silencieuse du prompt (le défaut Ollama à 2048/4096 coupait le début du contexte quand plusieurs notes entières étaient injectées). |
|
||||||
|
| 1 | `search.py` · `generate()` | `num_predict` **512 → 1024** | Réponses longues (descriptions de projet, explications) ne sont plus coupées en plein milieu. |
|
||||||
|
| 1 | `search.py` · `generate()` | `think: false` **top-level** | Désactive le mode *thinking* de qwen3. Le modèle n'utilise plus de budget de sortie pour du raisonnement interne. |
|
||||||
|
| 2 | `search.py` · `build_prompt()` | Troncature conditionnelle des sources **rank 2+** | Les notes secondaires (ex. `inception` sur une question bio) sont résumées à `SEARCH_SECONDARY_MAX_CHARS` chars quand leur score est < `SEARCH_SECONDARY_KEEP_RATIO` × score(#1). Réduit le bruit sans supprimer de source. |
|
||||||
|
| 3 | `vault-grasbot/30-Parcours/bio-fernand.md` | **Nouvelle note** dédiée à la présentation courte | Source canonique pour les questions du type *"qui est Fernand"*. Priorité 10, aliases biographiques courts. Renvoie vers le CV complet pour le détail. |
|
||||||
|
| 3 | CV (`cv-grascalvet-fernand.md`) | Incohérence d'âge corrigée (46 → 47 ans) | Supprime la contradiction interne qui alimentait les hallucinations sur l'âge. |
|
||||||
|
| 4 | `search.py` · `SYSTEM_PROMPT` | Section "Règles de fidélité aux sources" | Force le modèle à (a) s'appuyer en priorité sur `type=parcours` pour les questions bio, (b) ne jamais inventer un fait factuel, (c) écrire *« non précisé dans les notes »* si l'info manque, (d) gérer les contradictions, (e) signaler les notes tronquées. |
|
||||||
|
|
||||||
|
Observabilité : dans les spans Langfuse, `prompt_build.metadata.truncation`
|
||||||
|
liste chaque source tronquée automatiquement → sert de point de vigilance pour
|
||||||
|
vérifier que la troncature reste pertinente (et n'écrase pas une source qu'on
|
||||||
|
aurait dû garder entière).
|
||||||
|
|
||||||
|
Variables d'environnement associées (dans `llm-api/.env` ou shell) :
|
||||||
|
|
||||||
|
| Variable | Défaut | Effet |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| `SEARCH_SECONDARY_MAX_CHARS` | `1500` | Taille max des sources secondaires dans le prompt |
|
||||||
|
| `SEARCH_SECONDARY_KEEP_RATIO` | `0.8` | Tant que score(rank≥2) ≥ ratio × score(#1) → source gardée entière |
|
||||||
|
|
||||||
|
Rappel : `load_vault()` est mémoïsé. Après création/modification d'une note du
|
||||||
|
vault, appeler `POST /reload-vault` pour recharger le cache sans redémarrer
|
||||||
|
uvicorn (voir `api.py`).
|
||||||
|
|
||||||
## Évolutions futures possibles
|
## Évolutions futures possibles
|
||||||
|
|
||||||
- **Feedback utilisateur** : ajouter un 👍/👎 sur chaque réponse bot dans `ChatBot.js`, relayé à `/api/feedback` qui appellerait `langfuse.score(trace_id, name="user_feedback", value=1|0)`. Le `trace_id` serait retourné par `/ask` (actuellement omis).
|
- **Feedback utilisateur** : ajouter un 👍/👎 sur chaque réponse bot dans `ChatBot.js`, relayé à `/api/feedback` qui appellerait `langfuse.score(trace_id, name="user_feedback", value=1|0)`. Le `trace_id` serait retourné par `/ask` (actuellement omis).
|
||||||
|
|||||||
@ -22,12 +22,20 @@ Pipeline :
|
|||||||
|
|
||||||
Variables d'environnement (toutes optionnelles) :
|
Variables d'environnement (toutes optionnelles) :
|
||||||
|
|
||||||
- `OLLAMA_URL` (default: http://localhost:11434)
|
- `OLLAMA_URL` (default: http://localhost:11434)
|
||||||
- `LLM_MODEL` (default: qwen3:8b)
|
- `LLM_MODEL` (default: qwen3:8b)
|
||||||
- `VAULT_DIR` (default: <repo_root>/vault-grasbot)
|
- `VAULT_DIR` (default: <repo_root>/vault-grasbot)
|
||||||
- `SEARCH_TOP_K` (default: 5)
|
- `SEARCH_TOP_K` (default: 5)
|
||||||
- `SEARCH_MIN_SCORE` (default: 1.0) — seuil en-dessous duquel on considère
|
- `SEARCH_MIN_SCORE` (default: 1.0) — seuil en-dessous duquel on
|
||||||
qu'aucune note pertinente n'a été trouvée.
|
considère qu'aucune note pertinente n'a été trouvée.
|
||||||
|
- `SEARCH_SECONDARY_MAX_CHARS` (default: 1500) — taille max (en chars) du body
|
||||||
|
des sources rank 2+ dans le prompt. Les sources
|
||||||
|
dépassant cette limite sont tronquées à la
|
||||||
|
frontière de paragraphe la plus proche.
|
||||||
|
- `SEARCH_SECONDARY_KEEP_RATIO` (default: 0.8) — seuil relatif au score de la
|
||||||
|
source #1. Tant que score(rank>=2) est ≥
|
||||||
|
ratio × score(#1), la source est gardée
|
||||||
|
entière (considérée aussi pertinente).
|
||||||
|
|
||||||
Instrumentation Langfuse (2026-04-23) :
|
Instrumentation Langfuse (2026-04-23) :
|
||||||
|
|
||||||
@ -66,6 +74,16 @@ VAULT_DIR = Path(os.environ.get("VAULT_DIR", _DEFAULT_VAULT))
|
|||||||
TOP_K = int(os.environ.get("SEARCH_TOP_K", "5"))
|
TOP_K = int(os.environ.get("SEARCH_TOP_K", "5"))
|
||||||
MIN_SCORE = float(os.environ.get("SEARCH_MIN_SCORE", "1.0"))
|
MIN_SCORE = float(os.environ.get("SEARCH_MIN_SCORE", "1.0"))
|
||||||
|
|
||||||
|
# Troncature des sources secondaires dans le prompt (étape 2, 2026-04-23).
|
||||||
|
# Rationnel : BM25 peut remonter des projets entiers (ex. `inception`, `cpp-partie2`)
|
||||||
|
# avec un score respectable pour des questions biographiques — ils polluent le
|
||||||
|
# contexte sans apporter d'info pertinente. On garde la source #1 entière et on
|
||||||
|
# tronque uniquement les sources rank 2+ dont le score est < SECONDARY_KEEP_RATIO
|
||||||
|
# fois celui de la #1, ET dont le body dépasse SECONDARY_MAX_CHARS caractères.
|
||||||
|
# Aucune source n'est jamais supprimée : le modèle voit toujours le top-K complet.
|
||||||
|
SECONDARY_MAX_CHARS = int(os.environ.get("SEARCH_SECONDARY_MAX_CHARS", "1500"))
|
||||||
|
SECONDARY_KEEP_RATIO = float(os.environ.get("SEARCH_SECONDARY_KEEP_RATIO", "0.8"))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tokenisation FR (stop-words minimalistes, suffisants pour 36 notes)
|
# Tokenisation FR (stop-words minimalistes, suffisants pour 36 notes)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -573,29 +591,93 @@ Ton rôle :
|
|||||||
- Répondre aux visiteurs du site sur le parcours, les projets, les compétences de Fernand.
|
- Répondre aux visiteurs du site sur le parcours, les projets, les compétences de Fernand.
|
||||||
- T'appuyer sur les notes du vault personnel fournies dans le contexte.
|
- T'appuyer sur les notes du vault personnel fournies dans le contexte.
|
||||||
|
|
||||||
Règles :
|
Règles de ton :
|
||||||
- Réponds en français, ton sobre et précis, sans emojis.
|
- Réponds en français, ton sobre et précis, sans emojis.
|
||||||
- Cite tes sources entre crochets carrés en utilisant le slug (ex. [push-swap], [ia]).
|
- Cite tes sources entre crochets carrés en utilisant le slug (ex. [push-swap], [ia]).
|
||||||
- Si l'information n'apparaît pas dans les notes fournies, dis-le honnêtement et oriente vers le site (/portfolio, /competences, /contact) sans inventer.
|
|
||||||
- Reste concis (3 à 6 phrases en général), sauf demande explicite de détail.
|
- Reste concis (3 à 6 phrases en général), sauf demande explicite de détail.
|
||||||
- Si la question est hors sujet (ex. question généraliste sans rapport avec Fernand), indique poliment ton rôle et invite à poser une question sur son parcours."""
|
- Si la question est hors sujet (ex. question généraliste sans rapport avec Fernand), indique poliment ton rôle et invite à poser une question sur son parcours.
|
||||||
|
|
||||||
|
Règles de fidélité aux sources (important) :
|
||||||
|
- Chaque source fournie est annotée `type=parcours | projet | moc | competence | glossaire`.
|
||||||
|
- Pour toute question biographique (qui est Fernand, âge, situation, école, objectif, contact, localisation), appuie-toi **en priorité** sur les sources de `type=parcours` (ex. [bio-fernand], [cv-grascalvet-fernand]). Ne **déduis jamais** d'informations biographiques depuis une source `type=projet` ou `type=moc`.
|
||||||
|
- Ne **jamais inventer** un fait factuel (âge, date, diplôme, école, entreprise, technologie utilisée) qui n'apparaît pas littéralement dans les sources. Si l'info n'est pas présente, écris « non précisé dans les notes » et oriente vers /portfolio, /competences ou /contact.
|
||||||
|
- En cas de contradiction entre deux sources, privilégie la source de plus haut score, mentionne brièvement la divergence, et ne choisis jamais une valeur absente des deux.
|
||||||
|
- Une note dont le body se termine par « note tronquée » a été résumée : signale-le si tu t'appuies dessus pour un point précis, ou invite à consulter la note complète."""
|
||||||
|
|
||||||
|
|
||||||
|
_TRUNCATION_MARKER = "\n\n… *(note tronquée — voir le vault pour le détail)*"
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate_body(body: str, max_chars: int) -> str:
|
||||||
|
"""Coupe `body` à `max_chars` en essayant de finir sur une frontière propre.
|
||||||
|
|
||||||
|
Stratégie :
|
||||||
|
1. Si le body est déjà ≤ max_chars → inchangé.
|
||||||
|
2. Sinon on garde `body[:max_chars]` puis on cherche la dernière coupure
|
||||||
|
"naturelle" (double saut de ligne = fin de paragraphe, sinon fin de
|
||||||
|
phrase). On ne recule que si la coupure trouvée est dans la moitié
|
||||||
|
haute de la fenêtre, pour éviter de perdre trop de contenu.
|
||||||
|
3. On ajoute un marqueur explicite pour signaler au modèle que la note
|
||||||
|
a été résumée (évite qu'il conclue "il n'y a pas d'info sur ...").
|
||||||
|
"""
|
||||||
|
if len(body) <= max_chars:
|
||||||
|
return body
|
||||||
|
|
||||||
|
truncated = body[:max_chars]
|
||||||
|
cutoff = -1
|
||||||
|
for sep in ("\n\n", ". ", "\n", " "):
|
||||||
|
idx = truncated.rfind(sep)
|
||||||
|
if idx >= max_chars * 0.6:
|
||||||
|
cutoff = idx + (len(sep) if sep.endswith(" ") else 0)
|
||||||
|
break
|
||||||
|
if cutoff > 0:
|
||||||
|
truncated = truncated[:cutoff]
|
||||||
|
return truncated.rstrip() + _TRUNCATION_MARKER
|
||||||
|
|
||||||
|
|
||||||
def build_prompt(query: str, scored_notes: list[ScoredNote]) -> tuple[str, str]:
|
def build_prompt(query: str, scored_notes: list[ScoredNote]) -> tuple[str, str]:
|
||||||
"""Assemble (system, user) pour Qwen3. Notes **entières** dans le contexte."""
|
"""Assemble (system, user) pour Qwen3.
|
||||||
# Seuil : si toutes les notes sont en-dessous, on considère "pas de contexte pertinent"
|
|
||||||
|
- Sources gardées : celles dont le score ≥ MIN_SCORE.
|
||||||
|
- Troncature : la source #1 (top score) reste **entière**. Les sources
|
||||||
|
rank 2+ dont le score est < SECONDARY_KEEP_RATIO × score(#1) et dont le
|
||||||
|
body dépasse SECONDARY_MAX_CHARS sont résumées par `_truncate_body`.
|
||||||
|
Aucune source n'est supprimée — le modèle voit toujours tout le top-K.
|
||||||
|
"""
|
||||||
relevant = [s for s in scored_notes if s.score >= MIN_SCORE]
|
relevant = [s for s in scored_notes if s.score >= MIN_SCORE]
|
||||||
|
|
||||||
with langfuse.start_as_current_span(
|
with langfuse.start_as_current_span(
|
||||||
name="prompt_build",
|
name="prompt_build",
|
||||||
input={"query": query, "scored_count": len(scored_notes)},
|
input={"query": query, "scored_count": len(scored_notes)},
|
||||||
) as span:
|
) as span:
|
||||||
|
truncated_log: list[dict[str, Any]] = []
|
||||||
|
|
||||||
if relevant:
|
if relevant:
|
||||||
|
top_score = relevant[0].score
|
||||||
|
keep_full_threshold = top_score * SECONDARY_KEEP_RATIO
|
||||||
context_blocks = []
|
context_blocks = []
|
||||||
for i, s in enumerate(relevant, 1):
|
for i, s in enumerate(relevant, 1):
|
||||||
n = s.note
|
n = s.note
|
||||||
|
body = n.body
|
||||||
|
original_chars = len(body)
|
||||||
|
should_truncate = (
|
||||||
|
i > 1
|
||||||
|
and s.score < keep_full_threshold
|
||||||
|
and original_chars > SECONDARY_MAX_CHARS
|
||||||
|
)
|
||||||
|
if should_truncate:
|
||||||
|
body = _truncate_body(body, SECONDARY_MAX_CHARS)
|
||||||
|
truncated_log.append(
|
||||||
|
{
|
||||||
|
"rank": i,
|
||||||
|
"slug": n.slug,
|
||||||
|
"score": round(s.score, 2),
|
||||||
|
"original_chars": original_chars,
|
||||||
|
"truncated_chars": len(body),
|
||||||
|
}
|
||||||
|
)
|
||||||
header = f"[SOURCE {i} · slug={n.slug} · type={n.type} · score={s.score:.1f}] {n.title}"
|
header = f"[SOURCE {i} · slug={n.slug} · type={n.type} · score={s.score:.1f}] {n.title}"
|
||||||
context_blocks.append(f"{header}\n{n.body}")
|
context_blocks.append(f"{header}\n{body}")
|
||||||
context = "\n\n---\n\n".join(context_blocks)
|
context = "\n\n---\n\n".join(context_blocks)
|
||||||
user = (
|
user = (
|
||||||
"Voici les notes pertinentes du vault personnel de Fernand :\n\n"
|
"Voici les notes pertinentes du vault personnel de Fernand :\n\n"
|
||||||
@ -622,6 +704,11 @@ def build_prompt(query: str, scored_notes: list[ScoredNote]) -> tuple[str, str]:
|
|||||||
"system_chars": len(SYSTEM_PROMPT),
|
"system_chars": len(SYSTEM_PROMPT),
|
||||||
"user_chars": len(user),
|
"user_chars": len(user),
|
||||||
"min_score_threshold": MIN_SCORE,
|
"min_score_threshold": MIN_SCORE,
|
||||||
|
"truncation": {
|
||||||
|
"secondary_max_chars": SECONDARY_MAX_CHARS,
|
||||||
|
"secondary_keep_ratio": SECONDARY_KEEP_RATIO,
|
||||||
|
"truncated_notes": truncated_log,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -637,10 +724,21 @@ def generate(system: str, user: str) -> str:
|
|||||||
Span Langfuse de type `generation` → expose latence, modèle, paramètres,
|
Span Langfuse de type `generation` → expose latence, modèle, paramètres,
|
||||||
et tokens (si l'API Ollama les retourne dans `prompt_eval_count` /
|
et tokens (si l'API Ollama les retourne dans `prompt_eval_count` /
|
||||||
`eval_count`) comme un LLM-call standard dans le dashboard.
|
`eval_count`) comme un LLM-call standard dans le dashboard.
|
||||||
|
|
||||||
|
Paramètres clefs (tunés 2026-04-23 après audit des traces Langfuse) :
|
||||||
|
- `num_ctx=8192` : fenêtre de contexte explicite (le défaut Ollama
|
||||||
|
à 2048/4096 tronquait silencieusement le début du prompt quand les
|
||||||
|
sources du RAG étaient volumineuses, d'où hallucinations sur l'identité).
|
||||||
|
- `num_predict=1024` : budget de sortie doublé (512 coupait les réponses
|
||||||
|
détaillées — p. ex. description du site ou d'un projet — en plein milieu).
|
||||||
|
- `think=False` (top-level, hors `options`) : désactive le mode *thinking*
|
||||||
|
de qwen3. Sinon le modèle consomme du budget de sortie en raisonnement
|
||||||
|
interne avant de générer la réponse visible.
|
||||||
"""
|
"""
|
||||||
model_params = {
|
model_params = {
|
||||||
"temperature": 0.4,
|
"temperature": 0.4,
|
||||||
"num_predict": 512,
|
"num_ctx": 8192,
|
||||||
|
"num_predict": 1024,
|
||||||
}
|
}
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "system", "content": system},
|
{"role": "system", "content": system},
|
||||||
@ -652,7 +750,7 @@ def generate(system: str, user: str) -> str:
|
|||||||
name="ollama-chat",
|
name="ollama-chat",
|
||||||
model=LLM_MODEL,
|
model=LLM_MODEL,
|
||||||
input=messages,
|
input=messages,
|
||||||
model_parameters=model_params,
|
model_parameters={**model_params, "think": False},
|
||||||
) as generation:
|
) as generation:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{OLLAMA_URL}/api/chat",
|
f"{OLLAMA_URL}/api/chat",
|
||||||
@ -660,6 +758,7 @@ def generate(system: str, user: str) -> str:
|
|||||||
"model": LLM_MODEL,
|
"model": LLM_MODEL,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
|
"think": False,
|
||||||
"options": model_params,
|
"options": model_params,
|
||||||
"keep_alive": "30m",
|
"keep_alive": "30m",
|
||||||
},
|
},
|
||||||
|
|||||||
69
vault-grasbot/30-Parcours/bio-fernand.md
Normal file
69
vault-grasbot/30-Parcours/bio-fernand.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
title: "Bio — Fernand Gras-Calvet (présentation courte)"
|
||||||
|
slug: bio-fernand
|
||||||
|
type: parcours
|
||||||
|
source: manual
|
||||||
|
domains: [parcours, ecole-42, ia]
|
||||||
|
tags: [bio, presentation, parcours, alternance]
|
||||||
|
aliases:
|
||||||
|
- bio
|
||||||
|
- biographie
|
||||||
|
- présentation
|
||||||
|
- présentation courte
|
||||||
|
- en bref
|
||||||
|
- fernand en bref
|
||||||
|
- qui est fernand
|
||||||
|
- qui est-il
|
||||||
|
- parle-moi de fernand
|
||||||
|
- que peux-tu me dire de fernand
|
||||||
|
answers:
|
||||||
|
- "Qui est Fernand ?"
|
||||||
|
- "Qui est Fernand Gras-Calvet ?"
|
||||||
|
- "Que peux-tu me dire de Fernand ?"
|
||||||
|
- "Parle-moi de Fernand."
|
||||||
|
- "Fernand en quelques mots ?"
|
||||||
|
- "Présente-toi."
|
||||||
|
priority: 10
|
||||||
|
linked:
|
||||||
|
- "[[cv-grascalvet-fernand]]"
|
||||||
|
- "[[MOC-Parcours]]"
|
||||||
|
related:
|
||||||
|
- "[[ia]]"
|
||||||
|
updated: 2026-04-23
|
||||||
|
visibility: public
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bio — Fernand Gras-Calvet
|
||||||
|
|
||||||
|
> [!info] Rôle de cette note
|
||||||
|
> Version **courte et factuelle** de la présentation de Fernand, pensée
|
||||||
|
> comme premier résultat du chatbot sur les questions du type *« qui est
|
||||||
|
> Fernand ? »*. Pour le détail complet (compétences, expériences, passions),
|
||||||
|
> voir [[cv-grascalvet-fernand|le CV]].
|
||||||
|
|
||||||
|
## Identité
|
||||||
|
|
||||||
|
- **Nom** : Fernand Gras-Calvet
|
||||||
|
- **Âge** : 47 ans
|
||||||
|
- **Situation** : étudiant en informatique à l'**École 42 Perpignan**
|
||||||
|
- **Localisation** : Rivesaltes (Pyrénées-Orientales, 66)
|
||||||
|
- **Statut** : reconversion professionnelle, bénéficiaire d'une **RQTH**
|
||||||
|
|
||||||
|
## Objectif professionnel
|
||||||
|
|
||||||
|
Trouver une **alternance de 2 ans** en **Data / IA** pour se spécialiser dans
|
||||||
|
l'**automatisation agentique en entreprise** (LLM, agents, intégration
|
||||||
|
d'outils internes).
|
||||||
|
|
||||||
|
## Parcours en une phrase
|
||||||
|
|
||||||
|
Ancien **infirmier diplômé d'État** (10 ans en gériatrie, 2014-2023), après
|
||||||
|
plus de 12 ans en **ostréiculture familiale** à Leucate, aujourd'hui en
|
||||||
|
reconversion IT à l'École 42 (2023-2025) avec un stage réalisé autour d'un
|
||||||
|
**chatbot multi-agent** en entreprise.
|
||||||
|
|
||||||
|
## Pour aller plus loin
|
||||||
|
|
||||||
|
- [[cv-grascalvet-fernand|CV détaillé]] — expériences, compétences techniques, intérêts
|
||||||
|
- [[MOC-Projets]] — vue d'ensemble des projets École 42
|
||||||
|
- [[ia|IA]] — note thématique sur son domaine cible
|
||||||
@ -28,6 +28,7 @@ answers:
|
|||||||
- "A-t-il de l'expérience professionnelle ?"
|
- "A-t-il de l'expérience professionnelle ?"
|
||||||
priority: 10
|
priority: 10
|
||||||
linked:
|
linked:
|
||||||
|
- "[[bio-fernand]]"
|
||||||
- "[[MOC-Parcours]]"
|
- "[[MOC-Parcours]]"
|
||||||
- "[[MOC-Ecole-42]]"
|
- "[[MOC-Ecole-42]]"
|
||||||
- "[[MOC-Ia]]"
|
- "[[MOC-Ia]]"
|
||||||
@ -64,7 +65,7 @@ visibility: public
|
|||||||
|
|
||||||
## Présentation
|
## Présentation
|
||||||
|
|
||||||
Ancien infirmier de 46 ans, actuellement étudiant en informatique à l'École 42
|
Ancien infirmier de 47 ans, actuellement étudiant en informatique à l'École 42
|
||||||
Perpignan. Je recherche une alternance de 2 ans pour me spécialiser dans
|
Perpignan. Je recherche une alternance de 2 ans pour me spécialiser dans
|
||||||
**l'automatisation agentique au sein des entreprises**, en y apportant mon
|
**l'automatisation agentique au sein des entreprises**, en y apportant mon
|
||||||
expérience sur le traitement de Data et les nouveaux process basés sur les LLM.
|
expérience sur le traitement de Data et les nouveaux process basés sur les LLM.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user