feat: add agent prompt improvement action
This commit is contained in:
parent
8de5a328a1
commit
9ef1220650
4 changed files with 183 additions and 6 deletions
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'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"}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue