400 lines
15 KiB
Python
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 |