mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 15:38:12 +02:00
v0.2.10 : mode agent unifie (un seul toggle = shell + skills) + jeu de mot 'ajean' + refonte du header (pastille d'etat, preset, espacements)
This commit is contained in:
1 parent
175bbd34d9
commit
d3a2f1c428
11 files changed
+199
-207
No files matched your search
@@ -21,7 +21,7 @@ Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouve
|
|||||||
- **ROCm/HIP** (AMD), **Metal** (macOS / Apple Silicon), **Vulkan**, ou repli **CPU**
|
- **ROCm/HIP** (AMD), **Metal** (macOS / Apple Silicon), **Vulkan**, ou repli **CPU**
|
||||||
- `update` récupère le dernier commit, arrête le service le temps de recompiler, puis le redémarre
|
- `update` récupère le dernier commit, arrête le service le temps de recompiler, puis le redémarre
|
||||||
- **Intégration systemd** — `jean install` écrit l'unité, une règle sudoers `systemctl` sans mot de passe, et les dossiers de données
|
- **Intégration systemd** — `jean install` écrit l'unité, une règle sudoers `systemctl` sans mot de passe, et les dossiers de données
|
||||||
- **Interface web** (`jean web`) — chat, changement de modèle/preset, activation des skills & tools
|
- **Interface web** (`jean web`) — chat, changement de modèle/preset, activation du mode agent
|
||||||
- **Chat terminal** (`jean chat`) — réponses en streaming
|
- **Chat terminal** (`jean chat`) — réponses en streaming
|
||||||
- **Accès distant chiffré** (`jean link`) — connecte ce serveur à [ajean.link](https://ajean.link) par une connexion **sortante** (aucun port à ouvrir, marche même en CGNAT). Le chat est **chiffré de bout en bout** : le relais ne voit jamais tes conversations (voir [Sécurité — boîte noire](#sécurité--boîte-noire))
|
- **Accès distant chiffré** (`jean link`) — connecte ce serveur à [ajean.link](https://ajean.link) par une connexion **sortante** (aucun port à ouvrir, marche même en CGNAT). Le chat est **chiffré de bout en bout** : le relais ne voit jamais tes conversations (voir [Sécurité — boîte noire](#sécurité--boîte-noire))
|
||||||
- **Presets** (`jean switch`) — garde plusieurs profils `config.env` et bascule entre eux
|
- **Presets** (`jean switch`) — garde plusieurs profils `config.env` et bascule entre eux
|
||||||
@@ -119,9 +119,8 @@ Accès distant (ajean.link) :
|
|||||||
link code génère un code d'appairage (valable 10 min, usage unique) pour le portail
|
link code génère un code d'appairage (valable 10 min, usage unique) pour le portail
|
||||||
link status | logout état du lien / oublier le token
|
link status | logout état du lien / oublier le token
|
||||||
|
|
||||||
Outils côté LLM :
|
Mode agent :
|
||||||
skills [on|off|list] laisse le modèle lire SKILLS/<nom>/SKILL.md
|
agent [on|off|status] active TOUS les outils du modèle (shell complet + skills) — un seul interrupteur
|
||||||
machine [on|off|status] active l'accès machine (le modèle dispose d'un shell complet sur le serveur)
|
|
||||||
|
|
||||||
Backend (llama.cpp) :
|
Backend (llama.cpp) :
|
||||||
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), règle BIN
|
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), règle BIN
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Le « mode agent » est l'unique interrupteur qui donne à l'IA l'accès à ses
|
||||||
|
// outils. Un skill est un outil comme un autre : quand le mode agent est actif,
|
||||||
|
// l'IA dispose à la fois du shell (run_shell) et de la gestion des skills.
|
||||||
|
// Plus de drapeaux machine/skills séparés — un seul fichier .agent_enabled.
|
||||||
|
|
||||||
|
func agentEnabled() bool {
|
||||||
|
if _, err := os.Stat(agentFlag()); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Migration : si l'un des anciens drapeaux séparés traîne encore, on
|
||||||
|
// considère le mode agent comme actif (et on le matérialise au prochain set).
|
||||||
|
if _, err := os.Stat(legacyToolsFlag()); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(legacySkillsFlag()); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAgentEnabled(on bool) error {
|
||||||
|
_ = os.MkdirAll(JeanHome(), 0o755)
|
||||||
|
// On nettoie systématiquement les anciens drapeaux pour ne pas garder un
|
||||||
|
// état fantôme « à moitié activé » hérité de l'ancien modèle à deux toggles.
|
||||||
|
_ = os.Remove(legacyToolsFlag())
|
||||||
|
_ = os.Remove(legacySkillsFlag())
|
||||||
|
if on {
|
||||||
|
f, err := os.Create(agentFlag())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
if err := os.Remove(agentFlag()); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdAgent(args []string) error {
|
||||||
|
sub := ""
|
||||||
|
if len(args) > 0 {
|
||||||
|
sub = args[0]
|
||||||
|
}
|
||||||
|
switch sub {
|
||||||
|
case "on":
|
||||||
|
if err := setAgentEnabled(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(green("[ok]") + " mode agent activé — l'IA dispose du shell complet et de ses skills")
|
||||||
|
case "off":
|
||||||
|
if err := setAgentEnabled(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(green("[ok]") + " mode agent désactivé")
|
||||||
|
case "", "status", "list":
|
||||||
|
state := dim("off")
|
||||||
|
if agentEnabled() {
|
||||||
|
state = green("on")
|
||||||
|
}
|
||||||
|
fmt.Printf("%s état: %s\n", cyan("Mode agent"), state)
|
||||||
|
fmt.Printf(" outils : run_shell (timeout %ds, max %ds) + skill (read/write/append/delete)\n", toolDefaultTimeout, toolMaxTimeout)
|
||||||
|
sk := ListSkills()
|
||||||
|
if len(sk) == 0 {
|
||||||
|
fmt.Printf(" skills : aucun — crée %s/<nom>/SKILL.md\n", skillsDir())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Printf(" skills (%s) :\n", skillsDir())
|
||||||
|
for _, s := range sk {
|
||||||
|
fmt.Printf(" %s %s\n", bold(s.Name), s.Desc)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("usage: jean agent [on|off|status]")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -87,14 +87,15 @@ func runShellTool() Tool {
|
|||||||
// what the model can do for this conversation, instead of always inheriting the
|
// what the model can do for this conversation, instead of always inheriting the
|
||||||
// machine's global config. Use globalCaps() to fall back to the global config.
|
// machine's global config. Use globalCaps() to fall back to the global config.
|
||||||
type Caps struct {
|
type Caps struct {
|
||||||
Tools bool
|
// Agent = mode agent actif : un seul interrupteur qui débloque TOUS les
|
||||||
Skills bool
|
// outils de l'IA (shell + skills). Un skill est un outil comme un autre.
|
||||||
|
Agent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// globalCaps reads the machine-wide config — the default when a request doesn't
|
// globalCaps reads the machine-wide config — the default when a request doesn't
|
||||||
// specify its own capabilities.
|
// specify its own capabilities.
|
||||||
func globalCaps() Caps {
|
func globalCaps() Caps {
|
||||||
return Caps{Tools: toolsEnabled(), Skills: skillsEnabled()}
|
return Caps{Agent: agentEnabled()}
|
||||||
}
|
}
|
||||||
|
|
||||||
// InjectSkills prepends context system messages to msgs: the decisive-agent
|
// InjectSkills prepends context system messages to msgs: the decisive-agent
|
||||||
@@ -130,11 +131,8 @@ func InjectSkills(msgs []Message, caps Caps) []Message {
|
|||||||
// EnabledTools returns the tools to advertise on the next inference call.
|
// EnabledTools returns the tools to advertise on the next inference call.
|
||||||
func EnabledTools(caps Caps) []Tool {
|
func EnabledTools(caps Caps) []Tool {
|
||||||
tools := []Tool{}
|
tools := []Tool{}
|
||||||
if caps.Skills {
|
if caps.Agent {
|
||||||
tools = append(tools, skillTool())
|
tools = append(tools, skillTool(), runShellTool())
|
||||||
}
|
|
||||||
if caps.Tools {
|
|
||||||
tools = append(tools, runShellTool())
|
|
||||||
}
|
}
|
||||||
return tools
|
return tools
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.2.9"
|
const Version = "0.2.10"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := os.Args[1:]
|
args := os.Args[1:]
|
||||||
@@ -47,10 +47,11 @@ func main() {
|
|||||||
mustExit(cmdWeb(args))
|
mustExit(cmdWeb(args))
|
||||||
case "link":
|
case "link":
|
||||||
mustExit(cmdLink(args))
|
mustExit(cmdLink(args))
|
||||||
case "skills":
|
case "agent", "skills", "machine", "tools":
|
||||||
mustExit(cmdSkills(args))
|
// « mode agent » unifie l'ancien couple machine + skills : un seul
|
||||||
case "machine", "tools":
|
// interrupteur active TOUS les outils de l'IA (shell + skills). Les
|
||||||
mustExit(cmdTools(args))
|
// anciens noms restent acceptés comme alias rétro-compatibles.
|
||||||
|
mustExit(cmdAgent(args))
|
||||||
case "serve":
|
case "serve":
|
||||||
mustExit(cmdServe(args))
|
mustExit(cmdServe(args))
|
||||||
case "test":
|
case "test":
|
||||||
@@ -96,7 +97,7 @@ Presets:
|
|||||||
|
|
||||||
Interaction:
|
Interaction:
|
||||||
chat [system-prompt] chat terminal streamé
|
chat [system-prompt] chat terminal streamé
|
||||||
web [PORT] UI web (défaut :8090) — chat + presets + skills + tools
|
web [PORT] UI web (défaut :8090) — chat + presets + mode agent
|
||||||
|
|
||||||
Accès distant (ajean.link) :
|
Accès distant (ajean.link) :
|
||||||
link <token> enregistre le token et démarre le lien au relais (token = 1re fois / pour le changer)
|
link <token> enregistre le token et démarre le lien au relais (token = 1re fois / pour le changer)
|
||||||
@@ -106,9 +107,8 @@ Accès distant (ajean.link) :
|
|||||||
link (sans argument) affiche l'aide des sous-commandes link
|
link (sans argument) affiche l'aide des sous-commandes link
|
||||||
link serve exécute le worker au premier plan (utilisé par jean-link.service ; pendant de 'jean serve')
|
link serve exécute le worker au premier plan (utilisé par jean-link.service ; pendant de 'jean serve')
|
||||||
|
|
||||||
LLM-side outils:
|
Mode agent:
|
||||||
skills [on|off|list] active la lecture de SKILLS/<nom>/SKILL.md par l'IA
|
agent [on|off|status] active TOUS les outils de l'IA (shell complet + skills) — un seul interrupteur
|
||||||
machine [on|off|status] active l'accès machine (l'IA dispose d'un shell complet sur le serveur)
|
|
||||||
|
|
||||||
Backend llama.cpp :
|
Backend llama.cpp :
|
||||||
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), pointe BIN dessus
|
llamacpp install clone + compile llama.cpp (détecte CUDA/ROCm/Metal/CPU), pointe BIN dessus
|
||||||
@@ -176,8 +176,10 @@ func readEtcDefault() string {
|
|||||||
func confPath() string { return filepath.Join(JeanHome(), "config.env") }
|
func confPath() string { return filepath.Join(JeanHome(), "config.env") }
|
||||||
func presetsDir() string { return filepath.Join(JeanHome(), "configs") }
|
func presetsDir() string { return filepath.Join(JeanHome(), "configs") }
|
||||||
func skillsDir() string { return filepath.Join(JeanHome(), "SKILLS") }
|
func skillsDir() string { return filepath.Join(JeanHome(), "SKILLS") }
|
||||||
func skillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
|
func agentFlag() string { return filepath.Join(JeanHome(), ".agent_enabled") }
|
||||||
func toolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
|
// Anciens drapeaux séparés, conservés pour la migration vers le mode agent unifié.
|
||||||
|
func legacySkillsFlag() string { return filepath.Join(skillsDir(), ".enabled") }
|
||||||
|
func legacyToolsFlag() string { return filepath.Join(JeanHome(), ".tools_enabled") }
|
||||||
func apiKeyPath() string { return filepath.Join(JeanHome(), ".api_key") }
|
func apiKeyPath() string { return filepath.Join(JeanHome(), ".api_key") }
|
||||||
func serviceName() string {
|
func serviceName() string {
|
||||||
if n := os.Getenv("JEAN_SERVICE"); n != "" {
|
if n := os.Getenv("JEAN_SERVICE"); n != "" {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -15,26 +15,6 @@ type Skill struct {
|
|||||||
Content string
|
Content string
|
||||||
}
|
}
|
||||||
|
|
||||||
func skillsEnabled() bool {
|
|
||||||
_, err := os.Stat(skillsFlag())
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setSkillsEnabled(on bool) error {
|
|
||||||
_ = os.MkdirAll(skillsDir(), 0o755)
|
|
||||||
if on {
|
|
||||||
f, err := os.Create(skillsFlag())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return f.Close()
|
|
||||||
}
|
|
||||||
if err := os.Remove(skillsFlag()); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSkills walks SKILLS/<name>/SKILL.md, returning name+desc+full content.
|
// ListSkills walks SKILLS/<name>/SKILL.md, returning name+desc+full content.
|
||||||
func ListSkills() []Skill {
|
func ListSkills() []Skill {
|
||||||
entries, err := os.ReadDir(skillsDir())
|
entries, err := os.ReadDir(skillsDir())
|
||||||
@@ -157,7 +137,7 @@ func DeleteSkill(name string) error {
|
|||||||
// skillsSystemPrompt returns the lightweight skills directory message to
|
// skillsSystemPrompt returns the lightweight skills directory message to
|
||||||
// prepend to the conversation when skills are enabled.
|
// prepend to the conversation when skills are enabled.
|
||||||
func skillsSystemPrompt(caps Caps) string {
|
func skillsSystemPrompt(caps Caps) string {
|
||||||
if !caps.Skills {
|
if !caps.Agent {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
list := ListSkills()
|
list := ListSkills()
|
||||||
@@ -181,39 +161,3 @@ func skillsSystemPrompt(caps Caps) string {
|
|||||||
}
|
}
|
||||||
return strings.TrimRight(b.String(), "\n")
|
return strings.TrimRight(b.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdSkills(args []string) error {
|
|
||||||
sub := ""
|
|
||||||
if len(args) > 0 {
|
|
||||||
sub = args[0]
|
|
||||||
}
|
|
||||||
switch sub {
|
|
||||||
case "on":
|
|
||||||
if err := setSkillsEnabled(true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(green("[ok]") + " skills activés")
|
|
||||||
case "off":
|
|
||||||
if err := setSkillsEnabled(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(green("[ok]") + " skills désactivés")
|
|
||||||
case "", "list":
|
|
||||||
state := dim("off")
|
|
||||||
if skillsEnabled() {
|
|
||||||
state = green("on")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s (%s) état: %s\n", cyan("Skills"), skillsDir(), state)
|
|
||||||
sk := ListSkills()
|
|
||||||
if len(sk) == 0 {
|
|
||||||
fmt.Printf(" (aucun — crée %s/<nom>/SKILL.md)\n", skillsDir())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, s := range sk {
|
|
||||||
fmt.Printf(" %s %s\n", bold(s.Name), s.Desc)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("usage: jean skills [on|off|list]")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -17,26 +17,6 @@ const (
|
|||||||
toolMaxOutput = 8000 // characters of stdout/stderr returned to the model
|
toolMaxOutput = 8000 // characters of stdout/stderr returned to the model
|
||||||
)
|
)
|
||||||
|
|
||||||
func toolsEnabled() bool {
|
|
||||||
_, err := os.Stat(toolsFlag())
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setToolsEnabled(on bool) error {
|
|
||||||
_ = os.MkdirAll(JeanHome(), 0o755)
|
|
||||||
if on {
|
|
||||||
f, err := os.Create(toolsFlag())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return f.Close()
|
|
||||||
}
|
|
||||||
if err := os.Remove(toolsFlag()); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// baseSystemPrompt is the always-on system preamble. Structured like pi's
|
// baseSystemPrompt is the always-on system preamble. Structured like pi's
|
||||||
// proven prompt (identity → method → guidelines → concision → date): a concrete,
|
// proven prompt (identity → method → guidelines → concision → date): a concrete,
|
||||||
// procedural prompt gives the model rails so it stops deliberating forever
|
// procedural prompt gives the model rails so it stops deliberating forever
|
||||||
@@ -47,7 +27,7 @@ func baseSystemPrompt(caps Caps) string {
|
|||||||
// No tool access → no agentic preamble. A plain chat model told to "call
|
// No tool access → no agentic preamble. A plain chat model told to "call
|
||||||
// tools immediately" hallucinates textual tool calls (e.g. default_api:bash)
|
// tools immediately" hallucinates textual tool calls (e.g. default_api:bash)
|
||||||
// that leak into the answer. Let the user's own system prompt stand alone.
|
// that leak into the answer. Let the user's own system prompt stand alone.
|
||||||
if !caps.Tools && !caps.Skills {
|
if !caps.Agent {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
@@ -72,7 +52,7 @@ func baseSystemPrompt(caps Caps) string {
|
|||||||
// run_shell acts upon (and doesn't claim it has no access to "your PC").
|
// run_shell acts upon (and doesn't claim it has no access to "your PC").
|
||||||
// Returns "" when machine access is off.
|
// Returns "" when machine access is off.
|
||||||
func machineSystemPrompt(caps Caps) string {
|
func machineSystemPrompt(caps Caps) string {
|
||||||
if !caps.Tools {
|
if !caps.Agent {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
host, _ := os.Hostname()
|
host, _ := os.Hostname()
|
||||||
@@ -156,32 +136,3 @@ func tailRunes(s string, n int) string {
|
|||||||
}
|
}
|
||||||
return string(r[len(r)-n:])
|
return string(r[len(r)-n:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func cmdTools(args []string) error {
|
|
||||||
sub := ""
|
|
||||||
if len(args) > 0 {
|
|
||||||
sub = args[0]
|
|
||||||
}
|
|
||||||
switch sub {
|
|
||||||
case "on":
|
|
||||||
if err := setToolsEnabled(true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(green("[ok]") + " accès machine activé — l'IA dispose d'un shell complet sur le serveur")
|
|
||||||
case "off":
|
|
||||||
if err := setToolsEnabled(false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(green("[ok]") + " accès machine désactivé")
|
|
||||||
case "", "status":
|
|
||||||
state := dim("off")
|
|
||||||
if toolsEnabled() {
|
|
||||||
state = green("on")
|
|
||||||
}
|
|
||||||
fmt.Printf("%s état: %s\n", cyan("Accès machine"), state)
|
|
||||||
fmt.Printf(" timeout défaut: %ds, max: %ds\n", toolDefaultTimeout, toolMaxTimeout)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("usage: jean machine [on|off|status]")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
+62
-40
@@ -30,10 +30,34 @@ body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background
|
|||||||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
|
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
|
||||||
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
|
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
|
||||||
}
|
}
|
||||||
h1{margin:0 0 4px;font-size:18px;color:var(--accent)}
|
h1{margin:0;font-size:18px;color:var(--accent)}
|
||||||
|
#brand{display:inline-flex;align-items:baseline}
|
||||||
|
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
|
||||||
|
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||||
|
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
||||||
|
.statuspill.ok{color:var(--ok);border-color:rgba(63,185,80,.45);background:rgba(63,185,80,.10)}
|
||||||
|
.statuspill.err{color:var(--err);border-color:rgba(248,81,73,.45);background:rgba(248,81,73,.10)}
|
||||||
|
.statuspill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
||||||
|
.statuspill .port{opacity:.6;font-weight:400}
|
||||||
|
/* en-tête + bouton d'ajout de skill (toujours visible) */
|
||||||
|
.skillshead{display:flex;align-items:center;justify-content:space-between;margin:12px 0 5px}
|
||||||
|
.skillshead>span{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-weight:600}
|
||||||
|
.addbtn{font-size:11px;padding:3px 11px 4px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;line-height:1.2}
|
||||||
|
.addbtn:hover{border-color:var(--accent);color:var(--accent)}
|
||||||
|
#brand-a{display:inline-block;line-height:1;max-width:0;opacity:0;overflow:hidden;color:#fff;transition:max-width .35s ease,opacity .35s ease}
|
||||||
|
#brand.agent #brand-a{max-width:1ch;opacity:1;margin-right:.04em}
|
||||||
|
/* interrupteur (switch) */
|
||||||
|
.switchrow{display:flex;align-items:center;gap:9px;font-size:12px;padding:6px 0;cursor:pointer;user-select:none}
|
||||||
|
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||||
|
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||||||
|
.switch .slider{position:absolute;inset:0;background:var(--border);border-radius:999px;transition:background .25s ease}
|
||||||
|
.switch .slider::before{content:"";position:absolute;width:14px;height:14px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .25s ease;box-shadow:0 1px 2px rgba(0,0,0,.4)}
|
||||||
|
.switch input:checked + .slider{background:var(--accent)}
|
||||||
|
.switch input:checked + .slider::before{transform:translateX(16px)}
|
||||||
|
.switch input:focus-visible + .slider{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
|
||||||
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
|
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
|
||||||
details{margin:8px 0;border-top:1px solid var(--border);padding-top:6px}
|
details{margin:7px 0;border-top:1px solid var(--border);padding-top:4px}
|
||||||
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:4px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
|
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:3px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
|
||||||
details>summary::-webkit-details-marker{display:none}
|
details>summary::-webkit-details-marker{display:none}
|
||||||
details>summary::after{content:"▸";transition:transform .15s;color:var(--dim);font-size:10px}
|
details>summary::after{content:"▸";transition:transform .15s;color:var(--dim);font-size:10px}
|
||||||
details[open]>summary::after{transform:rotate(90deg)}
|
details[open]>summary::after{transform:rotate(90deg)}
|
||||||
@@ -72,6 +96,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
.msg.assistant,.msg.reasoning{white-space:normal}
|
.msg.assistant,.msg.reasoning{white-space:normal}
|
||||||
.msg .body{overflow-wrap:anywhere;word-break:break-word}
|
.msg .body{overflow-wrap:anywhere;word-break:break-word}
|
||||||
.msg.user{align-self:flex-end;background:#1f6feb;color:#fff}
|
.msg.user{align-self:flex-end;background:#1f6feb;color:#fff}
|
||||||
|
.msg.user .label{color:rgba(255,255,255,.8)}
|
||||||
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
|
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
|
||||||
.msg .body>*{margin:1em 0}
|
.msg .body>*{margin:1em 0}
|
||||||
.msg .body>*:first-child{margin-top:0}
|
.msg .body>*:first-child{margin-top:0}
|
||||||
@@ -145,8 +170,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
<button id="menubtn" onclick="toggleSide()">☰</button>
|
<button id="menubtn" onclick="toggleSide()">☰</button>
|
||||||
<div id="backdrop" onclick="toggleSide()"></div>
|
<div id="backdrop" onclick="toggleSide()"></div>
|
||||||
<div class="side" id="side">
|
<div class="side" id="side">
|
||||||
<h1>jean</h1>
|
<div class="sidehead">
|
||||||
<div id="statusline" class="muted"><span id="status-svc">…</span> <span id="status-preset"></span></div>
|
<h1 id="brand"><span id="brand-a">a</span>jean</h1>
|
||||||
|
<span id="status-svc" class="statuspill">…</span>
|
||||||
|
</div>
|
||||||
<details open><summary>GPU</summary>
|
<details open><summary>GPU</summary>
|
||||||
<div id="vram" class="muted">…</div>
|
<div id="vram" class="muted">…</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -156,12 +183,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
<details><summary>Presets<button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
|
<details><summary>Presets<button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
|
||||||
<div id="presets"></div>
|
<div id="presets"></div>
|
||||||
</details>
|
</details>
|
||||||
<details><summary>Accès machine <span id="tools-badge" style="margin-left:auto;font-size:10px"></span></summary>
|
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;font-size:10px"></span></summary>
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="tools-toggle" onchange="toggleTools()"> activer l'accès machine</label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent</label>
|
||||||
<div class="muted" style="font-size:11px">⚠️ donne à l'IA un accès shell complet au serveur — elle peut inspecter, lancer des scripts et tout faire via le terminal (timeout 30s, max 300s)</div>
|
<div class="muted" style="font-size:11px">⚠️ active l'<b>accès machine</b> (shell complet sur le serveur) <b>et</b> les <b>skills</b> — l'IA peut inspecter, lancer des scripts et exécuter ses skills (timeout 30s, max 300s)</div>
|
||||||
</details>
|
<div class="skillshead"><span>Skills</span><button class="addbtn" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter un skill">+ ajouter</button></div>
|
||||||
<details><summary>Skills <span id="skills-badge" style="margin-left:auto;font-size:10px"></span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter">+</button></summary>
|
|
||||||
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="skills-toggle" onchange="toggleSkills()"> activer</label>
|
|
||||||
<div id="skills-list" class="muted" style="font-size:12px"></div>
|
<div id="skills-list" class="muted" style="font-size:12px"></div>
|
||||||
</details>
|
</details>
|
||||||
<details><summary>System prompt</summary>
|
<details><summary>System prompt</summary>
|
||||||
@@ -188,8 +213,11 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
<div id="composer">
|
<div id="composer">
|
||||||
<div id="ctxtrack"><div id="ctx-fill"></div></div>
|
<div id="ctxtrack"><div id="ctx-fill"></div></div>
|
||||||
<div id="ctxmeta">
|
<div id="ctxmeta">
|
||||||
<span id="ctx-text" class="muted">contexte 0 / 0</span>
|
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
|
||||||
<button id="ctx-compact" onclick="compactContext()">compacter</button>
|
<span style="display:flex;align-items:center;gap:10px;margin-left:auto">
|
||||||
|
<span id="ctx-text" class="muted">contexte 0 / 0</span>
|
||||||
|
<button id="ctx-compact" onclick="compactContext()">compacter</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="inputbar">
|
<div id="inputbar">
|
||||||
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
|
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
|
||||||
@@ -298,8 +326,9 @@ async function jget(u){ const r=await jfetch(u); return r.json(); }
|
|||||||
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
|
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
|
||||||
async function loadStatus(){
|
async function loadStatus(){
|
||||||
const s=await jget('/api/status');
|
const s=await jget('/api/status');
|
||||||
const t=s.active?'<span class="tag ok">● active</span>':'<span class="tag err">○ '+s.state+'</span>';
|
const el=document.getElementById('status-svc');
|
||||||
document.getElementById('status-svc').innerHTML = t + ' <span class="muted">:'+s.port+'</span>';
|
el.className='statuspill '+(s.active?'ok':'err');
|
||||||
|
el.innerHTML='<span class="dot"></span>'+(s.active?'active':s.state)+' <span class="port">:'+s.port+'</span>';
|
||||||
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
||||||
}
|
}
|
||||||
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
||||||
@@ -338,12 +367,8 @@ async function loadPresets(){
|
|||||||
// Build via DOM (not string concat) so preset names can contain anything —
|
// Build via DOM (not string concat) so preset names can contain anything —
|
||||||
// spaces, accents, quotes, < > & — without breaking markup or handlers.
|
// spaces, accents, quotes, < > & — without breaking markup or handlers.
|
||||||
const sp = document.getElementById('status-preset');
|
const sp = document.getElementById('status-preset');
|
||||||
sp.innerHTML='';
|
sp.textContent = act ? act.name : '';
|
||||||
if(act){
|
sp.title = act ? 'preset actif' : '';
|
||||||
const tag=document.createElement('span');
|
|
||||||
tag.className='tag'; tag.style.borderColor='var(--accent)'; tag.style.color='var(--accent)';
|
|
||||||
tag.textContent=act.name; sp.appendChild(tag);
|
|
||||||
}
|
|
||||||
const cont=document.getElementById('presets');
|
const cont=document.getElementById('presets');
|
||||||
cont.innerHTML='';
|
cont.innerHTML='';
|
||||||
if(!p.length){ cont.innerHTML='<span class="muted">(aucun)</span>'; return; }
|
if(!p.length){ cont.innerHTML='<span class="muted">(aucun)</span>'; return; }
|
||||||
@@ -383,31 +408,28 @@ async function loadPresets(){
|
|||||||
cont.appendChild(row);
|
cont.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function loadSkills(){
|
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||||
const s=await jget('/api/skills');
|
// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ».
|
||||||
document.getElementById('skills-toggle').checked = s.enabled;
|
async function loadAgent(){
|
||||||
document.getElementById('skills-badge').innerHTML = s.enabled
|
const s=await jget('/api/agent');
|
||||||
|
const on = s.enabled;
|
||||||
|
document.getElementById('agent-toggle').checked = on;
|
||||||
|
document.getElementById('agent-badge').innerHTML = on
|
||||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
||||||
|
document.getElementById('brand').classList.toggle('agent', on);
|
||||||
document.getElementById('skills-list').innerHTML = (s.skills||[]).length
|
document.getElementById('skills-list').innerHTML = (s.skills||[]).length
|
||||||
? s.skills.map(x=>'<div class="preset" style="font-size:12px" onclick="openSkill(\''+x.name+'\')"><span><b style="color:var(--text)">'+x.name+'</b> <span class="muted">— '+x.desc+'</span></span><button onclick="event.stopPropagation();openSkill(\''+x.name+'\')" style="margin:0;padding:2px 8px;font-size:11px">edit</button></div>').join('')
|
? s.skills.map(x=>'<div class="preset" style="font-size:12px" onclick="openSkill(\''+x.name+'\')"><span><b style="color:var(--text)">'+x.name+'</b> <span class="muted">— '+x.desc+'</span></span><button onclick="event.stopPropagation();openSkill(\''+x.name+'\')" style="margin:0;padding:2px 8px;font-size:11px">edit</button></div>').join('')
|
||||||
: '<div class="muted">(aucun skill)</div>';
|
: '<div class="muted">(aucun skill)</div>';
|
||||||
}
|
}
|
||||||
async function toggleSkills(){
|
// alias : plusieurs appelants rafraîchissent juste la liste des skills
|
||||||
const on=document.getElementById('skills-toggle').checked;
|
const loadSkills = loadAgent;
|
||||||
await jpost('/api/skills/toggle',{on}); loadSkills();
|
async function toggleAgent(){
|
||||||
|
const on=document.getElementById('agent-toggle').checked;
|
||||||
|
if(on && !confirm("Activer le mode agent ? L'IA aura un accès shell complet au serveur et pourra exécuter ses skills.")){ document.getElementById('agent-toggle').checked=false; return; }
|
||||||
|
await jpost('/api/agent/toggle',{on});
|
||||||
|
loadAgent();
|
||||||
}
|
}
|
||||||
async function loadTools(){
|
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent()]); }
|
||||||
const t=await jget('/api/tools');
|
|
||||||
document.getElementById('tools-toggle').checked = t.enabled;
|
|
||||||
document.getElementById('tools-badge').innerHTML = t.enabled
|
|
||||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
|
||||||
}
|
|
||||||
async function toggleTools(){
|
|
||||||
const on=document.getElementById('tools-toggle').checked;
|
|
||||||
if(on && !confirm("Activer l'accès machine ? L'IA aura un accès shell complet au serveur.")){ document.getElementById('tools-toggle').checked=false; return; }
|
|
||||||
await jpost('/api/tools/toggle',{on}); loadTools();
|
|
||||||
}
|
|
||||||
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadSkills(),loadTools()]); }
|
|
||||||
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||||
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
|
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
|
||||||
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
|
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }
|
||||||
|
|||||||
+3
-3
@@ -3,13 +3,13 @@
|
|||||||
"FileVersion": {
|
"FileVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 2,
|
"Minor": 2,
|
||||||
"Patch": 9,
|
"Patch": 10,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 2,
|
"Minor": 2,
|
||||||
"Patch": 9,
|
"Patch": 10,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||||
"OriginalFilename": "jean.exe",
|
"OriginalFilename": "jean.exe",
|
||||||
"ProductName": "Jean",
|
"ProductName": "Jean",
|
||||||
"ProductVersion": "0.2.9",
|
"ProductVersion": "0.2.10",
|
||||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
@@ -83,13 +83,18 @@ func newWebMux() *http.ServeMux {
|
|||||||
api("/api/preset", handlePreset)
|
api("/api/preset", handlePreset)
|
||||||
api("/api/preset/save", handlePresetSave)
|
api("/api/preset/save", handlePresetSave)
|
||||||
api("/api/preset/delete", handlePresetDelete)
|
api("/api/preset/delete", handlePresetDelete)
|
||||||
api("/api/skills", handleSkills)
|
api("/api/agent", handleAgent)
|
||||||
api("/api/skills/toggle", handleSkillsToggle)
|
api("/api/agent/toggle", handleAgentToggle)
|
||||||
|
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
|
||||||
|
// encore l'agent via /api/tools* et /api/skills/toggle à travers le tunnel E2E.
|
||||||
|
// On les mappe sur le mode agent unifié le temps que le portail soit mis à jour.
|
||||||
|
api("/api/tools", handleAgent)
|
||||||
|
api("/api/tools/toggle", handleAgentToggle)
|
||||||
|
api("/api/skills", handleAgent)
|
||||||
|
api("/api/skills/toggle", handleAgentToggle)
|
||||||
api("/api/skill", handleSkill)
|
api("/api/skill", handleSkill)
|
||||||
api("/api/skill/save", handleSkillSave)
|
api("/api/skill/save", handleSkillSave)
|
||||||
api("/api/skill/delete", handleSkillDelete)
|
api("/api/skill/delete", handleSkillDelete)
|
||||||
api("/api/tools", handleTools)
|
|
||||||
api("/api/tools/toggle", handleToolsToggle)
|
|
||||||
api("/api/switch", handleSwitch)
|
api("/api/switch", handleSwitch)
|
||||||
api("/api/start", svcHandler("start"))
|
api("/api/start", svcHandler("start"))
|
||||||
api("/api/stop", svcHandler("stop"))
|
api("/api/stop", svcHandler("stop"))
|
||||||
@@ -419,25 +424,27 @@ func handlePresetDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, 200, map[string]any{"ok": true, "modelDeleted": modelDeleted, "modelError": modelErr})
|
sendJSON(w, 200, map[string]any{"ok": true, "modelDeleted": modelDeleted, "modelError": modelErr})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSkills(w http.ResponseWriter, r *http.Request) {
|
// handleAgent renvoie l'état du mode agent ET la liste des skills (qui sont des
|
||||||
|
// outils gérés sous ce même interrupteur) — un seul aller-retour pour l'UI.
|
||||||
|
func handleAgent(w http.ResponseWriter, r *http.Request) {
|
||||||
sk := ListSkills()
|
sk := ListSkills()
|
||||||
out := []map[string]any{}
|
out := []map[string]any{}
|
||||||
for _, s := range sk {
|
for _, s := range sk {
|
||||||
out = append(out, map[string]any{"name": s.Name, "desc": s.Desc})
|
out = append(out, map[string]any{"name": s.Name, "desc": s.Desc})
|
||||||
}
|
}
|
||||||
sendJSON(w, 200, map[string]any{"enabled": skillsEnabled(), "skills": out})
|
sendJSON(w, 200, map[string]any{"enabled": agentEnabled(), "skills": out})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSkillsToggle(w http.ResponseWriter, r *http.Request) {
|
func handleAgentToggle(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
On bool `json:"on"`
|
On bool `json:"on"`
|
||||||
}
|
}
|
||||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||||
if err := setSkillsEnabled(req.On); err != nil {
|
if err := setAgentEnabled(req.On); err != nil {
|
||||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendJSON(w, 200, map[string]any{"ok": true, "enabled": skillsEnabled()})
|
sendJSON(w, 200, map[string]any{"ok": true, "enabled": agentEnabled()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSkill(w http.ResponseWriter, r *http.Request) {
|
func handleSkill(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -480,22 +487,6 @@ func handleSkillDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, 200, map[string]any{"ok": true})
|
sendJSON(w, 200, map[string]any{"ok": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleTools(w http.ResponseWriter, r *http.Request) {
|
|
||||||
sendJSON(w, 200, map[string]any{"enabled": toolsEnabled()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleToolsToggle(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req struct {
|
|
||||||
On bool `json:"on"`
|
|
||||||
}
|
|
||||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err := setToolsEnabled(req.On); err != nil {
|
|
||||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendJSON(w, 200, map[string]any{"ok": true, "enabled": toolsEnabled()})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSwitch(w http.ResponseWriter, r *http.Request) {
|
func handleSwitch(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
N int `json:"n"`
|
N int `json:"n"`
|
||||||
@@ -576,9 +567,11 @@ func handleBenchLast(w http.ResponseWriter, r *http.Request) {
|
|||||||
type chatReq struct {
|
type chatReq struct {
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
// Optional per-request capability overrides (used by ajean.link agents,
|
// Optional per-request override of the agent mode (used by ajean.link
|
||||||
// which carry their own tools/skills toggles). nil = inherit the
|
// agents, which carry their own toggle). nil = inherit the machine's
|
||||||
// machine's global config.
|
// global config. Tools/Skills sont conservés pour la rétro-compat des
|
||||||
|
// anciens clients relais : l'un OU l'autre à true active le mode agent.
|
||||||
|
Agent *bool `json:"agent"`
|
||||||
Tools *bool `json:"tools"`
|
Tools *bool `json:"tools"`
|
||||||
Skills *bool `json:"skills"`
|
Skills *bool `json:"skills"`
|
||||||
}
|
}
|
||||||
@@ -623,11 +616,11 @@ func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any)
|
|||||||
body.Temperature = 0.7
|
body.Temperature = 0.7
|
||||||
}
|
}
|
||||||
caps := globalCaps()
|
caps := globalCaps()
|
||||||
if body.Tools != nil {
|
if body.Agent != nil {
|
||||||
caps.Tools = *body.Tools
|
caps.Agent = *body.Agent
|
||||||
}
|
} else if body.Tools != nil || body.Skills != nil {
|
||||||
if body.Skills != nil {
|
// rétro-compat : anciens clients qui envoyaient deux drapeaux séparés
|
||||||
caps.Skills = *body.Skills
|
caps.Agent = (body.Tools != nil && *body.Tools) || (body.Skills != nil && *body.Skills)
|
||||||
}
|
}
|
||||||
msgs := InjectSkills(body.Messages, caps)
|
msgs := InjectSkills(body.Messages, caps)
|
||||||
extra, _ := runChat(ctx, msgs, body.Temperature, caps, func(ev StreamEvent) bool {
|
extra, _ := runChat(ctx, msgs, body.Temperature, caps, func(ev StreamEvent) bool {
|
||||||
|
|||||||
Reference in new issue
Block a user