Files
alfred/agent/agent.py

182 lines
7.9 KiB
Python

# agent/agent.py
from typing import Any, Dict, List
import json
from .llm import DeepSeekClient
from .memory import Memory
from .tools import make_tools, Tool
class Agent:
def __init__(self, llm: DeepSeekClient, memory: Memory):
self.llm = llm
self.memory = memory
self.tools: Dict[str, Tool] = make_tools(memory)
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:
data = json.loads(text)
except json.JSONDecodeError:
return None
if not isinstance(data, dict):
return None
action = data.get("action")
if not isinstance(action, dict):
return None
name = action.get("name")
if not isinstance(name, str):
return None
return data
def _execute_action(self, intent: Dict[str, Any]) -> Dict[str, Any]:
action = intent["action"]
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}
try:
result = tool.func(**args)
except TypeError as e:
# Mauvais arguments
return {"error": "bad_args", "message": str(e)}
return result
def step(self, user_input: str) -> str:
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()
)
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"
)
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
# Exécuter l'action
tool_result = self._execute_action(intent)
followup_messages: List[Dict[str, Any]] = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
{
"role": "assistant",
"content": json.dumps(
{"tool_result": tool_result, "intent": intent},
ensure_ascii=False,
),
},
]
raw_second = self.llm.complete(followup_messages)
#self.memory.append_history("user", user_input)
#self.memory.append_history("assistant", raw_second)
return raw_second