feat: add agent prompt improvement action

This commit is contained in:
thibaud-lclr 2026-04-15 18:01:58 +02:00
parent 8de5a328a1
commit 9ef1220650
4 changed files with 183 additions and 6 deletions

View file

@ -1,9 +1,89 @@
use crate::error::AppError;
use crate::models::agent::{Agent, AgentRole, AgentTool};
use crate::services::agent_runtime;
use crate::AppState;
use rusqlite::params;
use tauri::State;
const PROMPT_IMPROVEMENT_TIMEOUT_SECS: u64 = 300;
fn agent_role_label(role: &AgentRole) -> &'static str {
match role {
AgentRole::Analyst => "analyste",
AgentRole::Developer => "developpeur",
}
}
fn agent_tool_label(tool: &AgentTool) -> &'static str {
match tool {
AgentTool::Codex => "Codex",
AgentTool::ClaudeCode => "Claude Code",
}
}
fn build_prompt_improvement_request(
name: &str,
role: &AgentRole,
tool: &AgentTool,
custom_prompt: &str,
) -> String {
let trimmed_name = name.trim();
let normalized_name = if trimmed_name.is_empty() {
"Agent sans nom"
} else {
trimmed_name
};
let current_prompt = custom_prompt.trim();
let current_prompt = if current_prompt.is_empty() {
"(vide)"
} else {
current_prompt
};
format!(
r#"Tu es un expert en prompt engineering pour agents logiciels assistes par LLM.
Tu dois ameliorer le prompt complementaire d'un agent Orchai.
Ce texte sera ajoute a un prompt systeme deja fourni par l'application.
## Contexte agent
- Nom: {name}
- Role: {role}
- Outil cible: {tool}
## Regles
1. Preserve l'intention du prompt existant.
2. Rends les instructions plus claires, plus specifiques et plus facilement executables par un LLM.
3. Evite de dupliquer le contexte deja apporte par le prompt systeme integre.
4. Garde un style concis, direct et oriente action.
5. Si le prompt actuel est vide, propose un prompt complementaire court et utile pour ce role.
6. Reponds uniquement avec le prompt final, sans explication, sans markdown, sans balises de code.
## Prompt actuel
{current_prompt}"#,
name = normalized_name,
role = agent_role_label(role),
tool = agent_tool_label(tool),
current_prompt = current_prompt,
)
}
fn normalize_improved_prompt(output: &str) -> String {
let trimmed = output.trim();
let lines: Vec<&str> = trimmed.lines().collect();
if lines.len() >= 3
&& lines
.first()
.is_some_and(|line| line.trim_start().starts_with("```"))
&& lines.last().is_some_and(|line| line.trim() == "```")
{
return lines[1..lines.len() - 1].join("\n").trim().to_string();
}
trimmed.to_string()
}
#[tauri::command]
pub fn create_agent(
state: State<'_, AppState>,
@ -96,3 +176,62 @@ pub fn delete_agent(state: State<'_, AppState>, id: String) -> Result<(), AppErr
Agent::delete(&db, &id)?;
Ok(())
}
#[tauri::command]
pub async fn improve_agent_prompt(
name: String,
role: AgentRole,
tool: AgentTool,
custom_prompt: String,
) -> Result<String, AppError> {
let request = build_prompt_improvement_request(&name, &role, &tool, &custom_prompt);
let working_dir = std::env::temp_dir().to_string_lossy().to_string();
let args: Vec<String> = Vec::new();
let improved = agent_runtime::run_agent_command(
tool.to_command(),
&args,
&request,
&working_dir,
PROMPT_IMPROVEMENT_TIMEOUT_SECS,
)
.await
.map_err(AppError::from)?;
let normalized = normalize_improved_prompt(&improved);
if normalized.is_empty() {
return Err(AppError::from(
"L'agent n'a renvoye aucun prompt exploitable".to_string(),
));
}
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_prompt_improvement_request_contains_context() {
let prompt = build_prompt_improvement_request(
"Analyste Tuleap",
&AgentRole::Analyst,
&AgentTool::Codex,
"Priorise la cause racine.",
);
assert!(prompt.contains("Analyste Tuleap"));
assert!(prompt.contains("analyste"));
assert!(prompt.contains("Codex"));
assert!(prompt.contains("Priorise la cause racine."));
}
#[test]
fn test_normalize_improved_prompt_removes_code_fences() {
let normalized =
normalize_improved_prompt("```text\nSois precis.\nListe les hypotheses.\n```");
assert_eq!(normalized, "Sois precis.\nListe les hypotheses.");
}
}

View file

@ -59,6 +59,7 @@ pub fn run() {
commands::agent::get_agent,
commands::agent::update_agent,
commands::agent::delete_agent,
commands::agent::improve_agent_prompt,
commands::project::create_project,
commands::project::list_projects,
commands::project::get_project,

View file

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { createAgent, getAgent, updateAgent } from "../../lib/api";
import { createAgent, getAgent, improveAgentPrompt, updateAgent } from "../../lib/api";
import { getErrorMessage } from "../../lib/errors";
import type { AgentRole, AgentTool } from "../../lib/types";
@ -17,6 +17,7 @@ export default function AgentForm() {
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [initializing, setInitializing] = useState(false);
const [improvingPrompt, setImprovingPrompt] = useState(false);
useEffect(() => {
async function loadAgent() {
@ -59,6 +60,20 @@ export default function AgentForm() {
}
}
async function handleImprovePrompt() {
setImprovingPrompt(true);
setError(null);
try {
const improvedPrompt = await improveAgentPrompt(name, role, tool, customPrompt);
setCustomPrompt(improvedPrompt);
} catch (err: unknown) {
setError(getErrorMessage(err));
} finally {
setImprovingPrompt(false);
}
}
return (
<div className="mx-auto max-w-2xl p-8">
<h2 className="mb-6 text-xl font-bold">{isEditing ? "Edit agent" : "New agent"}</h2>
@ -110,16 +125,30 @@ export default function AgentForm() {
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700">
Script / custom prompt (appended to built-in prompt)
</label>
<div className="mb-1 flex items-center justify-between gap-3">
<label className="block text-sm font-medium text-gray-700">
Script / custom prompt (appended to built-in prompt)
</label>
<button
type="button"
onClick={() => void handleImprovePrompt()}
disabled={loading || initializing || improvingPrompt}
className="rounded border border-emerald-300 bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700 hover:bg-emerald-100 disabled:cursor-not-allowed disabled:opacity-50"
>
{improvingPrompt ? "Amélioration..." : "Amélioration"}
</button>
</div>
<textarea
rows={12}
value={customPrompt}
onChange={(e) => setCustomPrompt(e.target.value)}
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading || initializing || improvingPrompt}
className="w-full rounded border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50"
placeholder="Extra instructions for this agent..."
/>
<p className="mt-1 text-xs text-gray-500">
Réécrit le prompt avec l&apos;outil sélectionné pour le rendre plus clair pour un LLM.
</p>
</div>
{error && (
@ -131,7 +160,7 @@ export default function AgentForm() {
<div className="flex gap-2">
<button
type="submit"
disabled={loading || initializing}
disabled={loading || initializing || improvingPrompt}
className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
{loading ? "Saving..." : isEditing ? "Save" : "Create"}

View file

@ -75,6 +75,14 @@ export async function updateAgent(
): Promise<void> {
return invoke("update_agent", { id, name, role, tool, customPrompt });
}
export async function improveAgentPrompt(
name: string,
role: AgentRole,
tool: AgentTool,
customPrompt: string
): Promise<string> {
return invoke("improve_agent_prompt", { name, role, tool, customPrompt });
}
export async function deleteAgent(id: string): Promise<void> {
return invoke("delete_agent", { id });
}