Cleaned and improved

This commit is contained in:
2025-11-28 22:09:26 +01:00
parent 4d2cfb5db1
commit c07527c6c9
21 changed files with 1431 additions and 365 deletions
+63 -115
View File
@@ -4,39 +4,17 @@ import json
from .llm import DeepSeekClient
from .memory import Memory
from .tools import make_tools, Tool
from .registry import make_tools, Tool
from .prompts import PromptBuilder
class Agent:
def __init__(self, llm: DeepSeekClient, memory: Memory):
def __init__(self, llm: DeepSeekClient, memory: Memory, max_tool_iterations: int = 5):
self.llm = llm
self.memory = memory
self.tools: Dict[str, Tool] = make_tools(memory)
self.prompt_builder = PromptBuilder(self.tools)
self.max_tool_iterations = max_tool_iterations
def _build_system_prompt(self) -> str:
ctx = {"project_root": self.memory.get_project_root()}
tools_desc = "\n".join(
f"- {t.name}: {t.description}" for t in self.tools.values()
)
return (
"Tu es un agent IA qui aide un développeur senior à gérer son projet local.\n"
"Tu peux demander des informations de base (comme le chemin du projet)\n"
"et tu peux utiliser des outils pour interagir avec le système de fichiers.\n\n"
"Contexte utilisateur (JSON):\n"
f"{json.dumps(ctx, ensure_ascii=False)}\n\n"
"Règles IMPORTANTES pour les outils:\n"
"1. Si tu ne connais pas la valeur d'un argument (par exemple project_root), "
"TU NE DOIS PAS mettre null ou une valeur inventée.\n"
" À la place, tu dois poser une question à l'utilisateur pour obtenir l'information.\n"
"2. Ne propose set_user_profile QUE lorsque l'utilisateur a donné un chemin de projet explicite.\n"
"3. Quand tu veux utiliser un outil, réponds STRICTEMENT avec un JSON de la forme :\n"
'{ "thought": "...", "action": { "name": "tool_name", "args": { ... } } }\n'
" - Pas de texte avant/après.\n"
" - Les args doivent être COMPLETS et NON nuls.\n"
"4. Quand JE (le système) te fournis un object JSON contenant 'tool_result' et 'intent', "
"tu dois ALORS répondre à l'utilisateur en TEXTE NATUREL, et NE PAS renvoyer de JSON d'action.\n\n"
"Tools disponibles:\n"
f"{tools_desc}\n"
)
def _parse_intent(self, text: str) -> Dict[str, Any] | None:
try:
@@ -62,18 +40,6 @@ class Agent:
name: str = action["name"]
args: Dict[str, Any] = action.get("args", {}) or {}
if name == "set_user_profile":
project_root = args.get("project_root")
if not project_root:
return {
"error": "missing_project_root",
"message": (
"Le modèle a demandé set_user_profile sans project_root. "
"Tu dois d'abord demander à l'utilisateur de fournir un chemin "
"de projet valide (ex: /home/francois/mon_projet)."
),
}
tool = self.tools.get(name)
if not tool:
return {"error": "unknown_tool", "tool": name}
@@ -87,95 +53,77 @@ class Agent:
return result
def step(self, user_input: str) -> str:
"""
Execute one agent step with iterative tool execution:
- Build system prompt
- Query LLM
- Loop: If JSON intent -> execute tool, add result to conversation, query LLM again
- Continue until LLM responds with text (no tool call) or max iterations reached
- Return final text response
"""
print("Starting a new step...")
"""
Un 'tour' d'agent :
- construit le prompt system
- interroge DeepSeek
- si JSON d'intent -> exécute tool, refait un appel, renvoie réponse finale
- sinon -> renvoie texte brut
"""
print("User input:", user_input)
root = self.memory.data.get("project_root")
print("Current project_root in memory:", root)
# Unified system prompt that always allows tools
tools_desc = "\n".join(
f"- {t.name}: {t.description}\n Paramètres: {json.dumps(t.parameters, ensure_ascii=False)}"
for t in self.tools.values()
)
print("Current memory state:", self.memory.data)
if root is None:
print("No project_root set - asking user and allowing tool use")
system_prompt = (
"Tu es un agent IA qui aide un développeur à gérer son projet local.\n\n"
"CONTEXTE ACTUEL:\n"
f"- project_root: {root} (NON DÉFINI)\n\n"
"RÈGLES IMPORTANTES:\n"
"1. Le project_root n'est pas encore défini. Tu DOIS d'abord demander à l'utilisateur "
"le chemin absolu de son projet (ex: /home/user/mon_projet).\n"
"2. Quand l'utilisateur te donne un chemin, tu DOIS immédiatement utiliser l'outil "
"'set_project_root' pour le sauvegarder.\n"
"3. Pour utiliser un outil, réponds STRICTEMENT avec ce format JSON:\n"
' { "thought": "explication", "action": { "name": "nom_outil", "args": { "arg": "valeur" } } }\n'
"4. Si tu réponds en texte (pas d'outil), réponds normalement en français.\n"
"5. Quand le système te renvoie un tool_result, réponds à l'utilisateur en TEXTE NATUREL.\n\n"
"OUTILS DISPONIBLES:\n"
f"{tools_desc}\n"
)
else:
print("Project_root is set - normal operation mode")
system_prompt = (
"Tu es un agent IA qui aide un développeur à gérer son projet local.\n\n"
"CONTEXTE ACTUEL:\n"
f"- project_root: {root}\n\n"
"RÈGLES IMPORTANTES:\n"
"1. Le project_root est défini. Tu peux utiliser les outils disponibles.\n"
"2. Pour utiliser un outil, réponds STRICTEMENT avec ce format JSON:\n"
' { "thought": "explication", "action": { "name": "nom_outil", "args": { "param": "valeur" } } }\n'
" EXEMPLE pour lister le dossier 'src':\n"
' { "thought": "L\'utilisateur veut voir le contenu de src", "action": { "name": "list_directory", "args": { "path": "src" } } }\n'
" EXEMPLE pour lister la racine du projet:\n"
' { "thought": "L\'utilisateur veut voir la racine", "action": { "name": "list_directory", "args": { "path": "." } } }\n'
"3. Si tu réponds en texte (pas d'outil), réponds normalement en français.\n"
"4. Quand le système te renvoie un tool_result, réponds à l'utilisateur en TEXTE NATUREL.\n"
"5. IMPORTANT: Extrais le chemin demandé par l'utilisateur et passe-le comme argument 'path'.\n\n"
"OUTILS DISPONIBLES:\n"
f"{tools_desc}\n"
)
# Build system prompt using PromptBuilder
system_prompt = self.prompt_builder.build_system_prompt(self.memory.data)
# Initialize conversation with user input
messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
]
raw_first = self.llm.complete(messages)
intent = self._parse_intent(raw_first)
print("raw_first:", raw_first)
print("Intent:", intent)
if not intent:
# Réponse texte simple
#self.memory.append_history("user", user_input)
#self.memory.append_history("assistant", raw_first)
return raw_first
# Tool execution loop
iteration = 0
while iteration < self.max_tool_iterations:
print(f"\n--- Iteration {iteration + 1} ---")
# Exécuter l'action
tool_result = self._execute_action(intent)
# Get LLM response
llm_response = self.llm.complete(messages)
print("LLM response:", llm_response)
followup_messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
{
# Try to parse as tool intent
intent = self._parse_intent(llm_response)
if not intent:
# No tool call - this is the final text response
print("No tool intent detected, returning final response")
# Save to history
self.memory.append_history("user", user_input)
self.memory.append_history("assistant", llm_response)
return llm_response
# Tool call detected - execute it
print("Intent detected:", intent)
tool_result = self._execute_action(intent)
print("Tool result:", tool_result)
# Add assistant's tool call and result to conversation
messages.append({
"role": "assistant",
"content": json.dumps(intent, ensure_ascii=False)
})
messages.append({
"role": "user",
"content": json.dumps(
{"tool_result": tool_result, "intent": intent},
ensure_ascii=False,
),
},
]
{"tool_result": tool_result},
ensure_ascii=False
)
})
raw_second = self.llm.complete(followup_messages)
iteration += 1
#self.memory.append_history("user", user_input)
#self.memory.append_history("assistant", raw_second)
return raw_second
# Max iterations reached - ask LLM for final response
print(f"\n--- Max iterations ({self.max_tool_iterations}) reached, requesting final response ---")
messages.append({
"role": "user",
"content": "Merci pour ces résultats. Peux-tu maintenant me donner une réponse finale en texte naturel ?"
})
final_response = self.llm.complete(messages)
# Save to history
self.memory.append_history("user", user_input)
self.memory.append_history("assistant", final_response)
return final_response