diff --git a/src-tauri/src/commands/agent.rs b/src-tauri/src/commands/agent.rs index 91f8d76..648cdcd 100644 --- a/src-tauri/src/commands/agent.rs +++ b/src-tauri/src/commands/agent.rs @@ -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 { + 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 = 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."); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 710bf66..2222d59 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src/components/agents/AgentForm.tsx b/src/components/agents/AgentForm.tsx index d40cb2e..3c107c8 100644 --- a/src/components/agents/AgentForm.tsx +++ b/src/components/agents/AgentForm.tsx @@ -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(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 (

{isEditing ? "Edit agent" : "New agent"}

@@ -110,16 +125,30 @@ export default function AgentForm() {
- +
+ + +