Files
Mark-XXXIX-OR/agent/executor.py
T
2026-05-10 17:23:10 +03:00

400 lines
15 KiB
Python

import json
import re
import sys
import threading
import subprocess
import tempfile
import os
from pathlib import Path
from typing import Callable
from agent.planner import create_plan, replan
from agent.error_handler import analyze_error, generate_fix, ErrorDecision
def get_base_dir() -> Path:
if getattr(sys, "frozen", False):
return Path(sys.executable).parent
return Path(__file__).resolve().parent.parent
BASE_DIR = get_base_dir()
API_CONFIG_PATH = BASE_DIR / "config" / "api_keys.json"
def _get_api_key() -> str:
with open(API_CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)["gemini_api_key"]
def _run_generated_code(description: str, speak: Callable | None = None) -> str:
import google.generativeai as genai
if speak:
speak("Writing custom code for this task, sir.")
home = Path.home()
desktop = home / "Desktop"
downloads = home / "Downloads"
documents = home / "Documents"
if not desktop.exists():
try:
import winreg
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
desktop = Path(winreg.QueryValueEx(key, "Desktop")[0])
except Exception:
pass
genai.configure(api_key=_get_api_key())
model = genai.GenerativeModel(
model_name="gemini-2.5-flash",
system_instruction=(
"You are an expert Python developer. "
"Write clean, complete, working Python code. "
"Use standard library + common packages. "
"Install missing packages with subprocess + pip if needed. "
"Return ONLY the Python code. No explanation, no markdown, no backticks.\n\n"
f"SYSTEM PATHS:\n"
f" Desktop = r'{desktop}'\n"
f" Downloads = r'{downloads}'\n"
f" Documents = r'{documents}'\n"
f" Home = r'{home}'\n"
)
)
try:
response = model.generate_content(
f"Write Python code to accomplish this task:\n\n{description}"
)
code = response.text.strip()
code = re.sub(r"```(?:python)?", "", code).strip().rstrip("`").strip()
with tempfile.NamedTemporaryFile(
mode="w", suffix=".py", delete=False, encoding="utf-8"
) as f:
f.write(code)
tmp_path = f.name
print(f"[Executor] 🐍 Running generated code: {tmp_path}")
result = subprocess.run(
[sys.executable, tmp_path],
capture_output=True, text=True,
timeout=120, cwd=str(Path.home())
)
try:
os.unlink(tmp_path)
except Exception:
pass
output = result.stdout.strip()
error = result.stderr.strip()
if result.returncode == 0 and output:
return output
elif result.returncode == 0:
return "Task completed successfully."
elif error:
raise RuntimeError(f"Code error: {error[:400]}")
return "Completed."
except subprocess.TimeoutExpired:
raise RuntimeError("Generated code timed out after 120 seconds.")
except RuntimeError:
raise
except Exception as e:
raise RuntimeError(f"Generated code failed: {e}")
def _inject_context(params: dict, tool: str, step_results: dict, goal: str = "") -> dict:
if not step_results:
return params
params = dict(params)
if tool == "file_controller" and params.get("action") in ("write", "create_file"):
content = params.get("content", "")
if not content or len(content) < 50:
all_results = [
v for v in step_results.values()
if v and len(v) > 100 and v not in ("Done.", "Completed.")
]
if all_results:
combined = "\n\n---\n\n".join(all_results)
translated = _translate_to_goal_language(combined, goal)
params["content"] = translated
print(f"[Executor] 💉 Injected + translated content")
return params
def _detect_language(text: str) -> str:
import google.generativeai as genai
genai.configure(api_key=_get_api_key())
model = genai.GenerativeModel("gemini-2.5-flash-lite")
try:
response = model.generate_content(
f"What language is this text written in? "
f"Reply with ONLY the language name in English (e.g. Turkish, English, French).\n\n"
f"Text: {text[:200]}"
)
return response.text.strip()
except Exception:
return "English"
def _translate_to_goal_language(content: str, goal: str) -> str:
if not goal:
return content
try:
import google.generativeai as genai
genai.configure(api_key=_get_api_key())
model = genai.GenerativeModel("gemini-2.5-flash")
target_lang = _detect_language(goal)
print(f"[Executor] 🌐 Translating to: {target_lang}")
prompt = (
f"You are a professional translator. "
f"Translate the following text into {target_lang}.\n"
f"IMPORTANT:\n"
f"- Translate EVERYTHING, leave nothing in English\n"
f"- Keep all facts, numbers, and data intact\n"
f"- Keep the structure and formatting\n"
f"- Output ONLY the translated text, nothing else\n\n"
f"Text to translate:\n{content[:4000]}"
)
response = model.generate_content(prompt)
translated = response.text.strip()
print(f"[Executor] ✅ Translation done ({target_lang})")
return translated
except Exception as e:
print(f"[Executor] ⚠️ Translation failed: {e}")
return content
def _call_tool(tool: str, parameters: dict, speak: Callable | None) -> str:
if tool == "open_app":
from actions.open_app import open_app
return open_app(parameters=parameters, player=None) or "Done."
elif tool == "web_search":
from actions.web_search import web_search
return web_search(parameters=parameters, player=None) or "Done."
elif tool == "game_updater":
from actions.game_updater import game_updater
return game_updater(parameters=parameters, player=None, speak=speak) or "Done."
elif tool == "browser_control":
from actions.browser_control import browser_control
return browser_control(parameters=parameters, player=None) or "Done."
elif tool == "file_controller":
from actions.file_controller import file_controller
return file_controller(parameters=parameters, player=None) or "Done."
elif tool == "cmd_control":
from actions.cmd_control import cmd_control
return cmd_control(parameters=parameters, player=None) or "Done."
elif tool == "code_helper":
from actions.code_helper import code_helper
return code_helper(parameters=parameters, player=None, speak=speak) or "Done."
elif tool == "dev_agent":
from actions.dev_agent import dev_agent
return dev_agent(parameters=parameters, player=None, speak=speak) or "Done."
elif tool == "screen_process":
from actions.screen_processor import screen_process
screen_process(parameters=parameters, player=None)
return "Screen captured and analyzed."
elif tool == "send_message":
from actions.send_message import send_message
return send_message(parameters=parameters, player=None) or "Done."
elif tool == "reminder":
from actions.reminder import reminder
return reminder(parameters=parameters, player=None) or "Done."
elif tool == "youtube_video":
from actions.youtube_video import youtube_video
return youtube_video(parameters=parameters, player=None) or "Done."
elif tool == "weather_report":
from actions.weather_report import weather_action
return weather_action(parameters=parameters, player=None) or "Done."
elif tool == "computer_settings":
from actions.computer_settings import computer_settings
return computer_settings(parameters=parameters, player=None) or "Done."
elif tool == "desktop_control":
from actions.desktop import desktop_control
return desktop_control(parameters=parameters, player=None) or "Done."
elif tool == "computer_control":
from actions.computer_control import computer_control
return computer_control(parameters=parameters, player=None) or "Done."
elif tool == "generated_code":
description = parameters.get("description", "")
if not description:
raise ValueError("generated_code requires a 'description' parameter.")
return _run_generated_code(description, speak=speak)
elif tool == "flight_finder":
from actions.flight_finder import flight_finder
return flight_finder(parameters=parameters, player=None, speak=speak) or "Done."
else:
print(f"[Executor] ⚠️ Unknown tool '{tool}' — falling back to generated_code")
return _run_generated_code(f"Accomplish this task: {parameters}", speak=speak)
class AgentExecutor:
MAX_REPLAN_ATTEMPTS = 2
def execute(
self,
goal: str,
speak: Callable | None = None,
cancel_flag: threading.Event | None = None,
) -> str:
print(f"\n[Executor] 🎯 Goal: {goal}")
replan_attempts = 0
completed_steps = []
step_results = {}
plan = create_plan(goal)
while True:
steps = plan.get("steps", [])
if not steps:
msg = "I couldn't create a valid plan for this task, sir."
if speak: speak(msg)
return msg
success = True
failed_step = None
failed_error = ""
for step in steps:
if cancel_flag and cancel_flag.is_set():
if speak: speak("Task cancelled, sir.")
return "Task cancelled."
step_num = step.get("step", "?")
tool = step.get("tool", "generated_code")
desc = step.get("description", "")
params = step.get("parameters", {})
params = _inject_context(params, tool, step_results, goal=goal)
print(f"\n[Executor] ▶️ Step {step_num}: [{tool}] {desc}")
attempt = 1
step_ok = False
while attempt <= 3:
if cancel_flag and cancel_flag.is_set():
break
try:
result = _call_tool(tool, params, speak)
step_results[step_num] = result
completed_steps.append(step)
print(f"[Executor] ✅ Step {step_num} done: {str(result)[:100]}")
step_ok = True
break
except Exception as e:
error_msg = str(e)
print(f"[Executor] ❌ Step {step_num} attempt {attempt} failed: {error_msg}")
recovery = analyze_error(step, error_msg, attempt=attempt)
decision = recovery["decision"]
user_msg = recovery.get("user_message", "")
if speak and user_msg:
speak(user_msg)
if decision == ErrorDecision.RETRY:
attempt += 1
import time; time.sleep(2)
continue
elif decision == ErrorDecision.SKIP:
print(f"[Executor] ⏭️ Skipping step {step_num}")
completed_steps.append(step)
step_ok = True
break
elif decision == ErrorDecision.ABORT:
msg = f"Task aborted, sir. {recovery.get('reason', '')}"
if speak: speak(msg)
return msg
else:
fix_suggestion = recovery.get("fix_suggestion", "")
if fix_suggestion and tool != "generated_code":
try:
fixed_step = generate_fix(step, error_msg, fix_suggestion)
if speak: speak("Trying an alternative approach, sir.")
res = _call_tool(
fixed_step["tool"],
fixed_step["parameters"],
speak
)
step_results[step_num] = res
completed_steps.append(step)
step_ok = True
break
except Exception as fix_err:
print(f"[Executor] ⚠️ Fix failed: {fix_err}")
failed_step = step
failed_error = error_msg
success = False
break
if not step_ok and not failed_step:
failed_step = step
failed_error = "Max retries exceeded"
success = False
if not success:
break
if success:
return self._summarize(goal, completed_steps, speak)
if replan_attempts >= self.MAX_REPLAN_ATTEMPTS:
msg = f"Task failed after {replan_attempts} replan attempts, sir."
if speak: speak(msg)
return msg
if speak: speak("Adjusting my approach, sir.")
replan_attempts += 1
plan = replan(goal, completed_steps, failed_step, failed_error)
def _summarize(self, goal: str, completed_steps: list, speak: Callable | None) -> str:
fallback = f"All done, sir. Completed {len(completed_steps)} steps for: {goal[:60]}."
try:
import google.generativeai as genai
genai.configure(api_key=_get_api_key())
model = genai.GenerativeModel(model_name="gemini-2.5-flash-lite")
steps_str = "\n".join(f"- {s.get('description', '')}" for s in completed_steps)
prompt = (
f'User goal: "{goal}"\n'
f"Completed steps:\n{steps_str}\n\n"
"Write a single natural sentence summarizing what was accomplished. "
"Address the user as 'sir'. Be direct and positive."
)
response = model.generate_content(prompt)
summary = response.text.strip()
if speak: speak(summary)
return summary
except Exception:
if speak: speak(fallback)
return fallback