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::error::AppError;
|
||||||
use crate::models::agent::{Agent, AgentRole, AgentTool};
|
use crate::models::agent::{Agent, AgentRole, AgentTool};
|
||||||
|
use crate::services::agent_runtime;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use rusqlite::params;
|
use rusqlite::params;
|
||||||
use tauri::State;
|
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]
|
#[tauri::command]
|
||||||
pub fn create_agent(
|
pub fn create_agent(
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
|
|
@ -96,3 +176,62 @@ pub fn delete_agent(state: State<'_, AppState>, id: String) -> Result<(), AppErr
|
||||||
Agent::delete(&db, &id)?;
|
Agent::delete(&db, &id)?;
|
||||||
Ok(())
|
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::get_agent,
|
||||||
commands::agent::update_agent,
|
commands::agent::update_agent,
|
||||||
commands::agent::delete_agent,
|
commands::agent::delete_agent,
|
||||||
|
commands::agent::improve_agent_prompt,
|
||||||
commands::project::create_project,
|
commands::project::create_project,
|
||||||
commands::project::list_projects,
|
commands::project::list_projects,
|
||||||
commands::project::get_project,
|
commands::project::get_project,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
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 { getErrorMessage } from "../../lib/errors";
|
||||||
import type { AgentRole, AgentTool } from "../../lib/types";
|
import type { AgentRole, AgentTool } from "../../lib/types";
|
||||||
|
|
||||||
|
|
@ -17,6 +17,7 @@ export default function AgentForm() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initializing, setInitializing] = useState(false);
|
const [initializing, setInitializing] = useState(false);
|
||||||
|
const [improvingPrompt, setImprovingPrompt] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadAgent() {
|
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 (
|
return (
|
||||||
<div className="mx-auto max-w-2xl p-8">
|
<div className="mx-auto max-w-2xl p-8">
|
||||||
<h2 className="mb-6 text-xl font-bold">{isEditing ? "Edit agent" : "New agent"}</h2>
|
<h2 className="mb-6 text-xl font-bold">{isEditing ? "Edit agent" : "New agent"}</h2>
|
||||||
|
|
@ -110,16 +125,30 @@ export default function AgentForm() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
<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)
|
Script / custom prompt (appended to built-in prompt)
|
||||||
</label>
|
</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
|
<textarea
|
||||||
rows={12}
|
rows={12}
|
||||||
value={customPrompt}
|
value={customPrompt}
|
||||||
onChange={(e) => setCustomPrompt(e.target.value)}
|
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..."
|
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>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -131,7 +160,7 @@ export default function AgentForm() {
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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"}
|
{loading ? "Saving..." : isEditing ? "Save" : "Create"}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,14 @@ export async function updateAgent(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return invoke("update_agent", { id, name, role, tool, customPrompt });
|
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> {
|
export async function deleteAgent(id: string): Promise<void> {
|
||||||
return invoke("delete_agent", { id });
|
return invoke("delete_agent", { id });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue