diff --git a/.gitignore b/.gitignore index 60a37584..35173cf3 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,26 @@ Cargo.lock # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -.DS_Store \ No newline at end of file +# AI AGENTS DELEGATE ACTIONS +# Python cache +ai-agents-delegate-actions/__pycache__/ +ai-agents-delegate-actions/*.pyc + +# Virtual environments +ai-agents-delegate-actions/venv/ +ai-agents-delegate-actions/env/ +ai-agents-delegate-actions/.venv/ +ai-agents-delegate-actions/.env/ + +# Environment variables +ai-agents-delegate-actions/.env + +# OS-specific +.DS_Store + +# Editor files +.idea/ +.vscode/ + +# IGNORE TOOLS FOLDER ENTIRELY +ai-agents-delegate-actions/tools/ diff --git a/AI-agents-delegate-actions/.env.example b/AI-agents-delegate-actions/.env.example new file mode 100644 index 00000000..cff25f11 --- /dev/null +++ b/AI-agents-delegate-actions/.env.example @@ -0,0 +1 @@ +GOOGLE_API_KEY=INSERT_KEY_HERE \ No newline at end of file diff --git a/AI-agents-delegate-actions/.gitignore b/AI-agents-delegate-actions/.gitignore new file mode 100644 index 00000000..48244e2f --- /dev/null +++ b/AI-agents-delegate-actions/.gitignore @@ -0,0 +1,22 @@ +# Python cache +ai-agents-delegate-actions/__pycache__/ +ai-agents-delegate-actions/*.pyc + +# Virtual environments +ai-agents-delegate-actions/venv/ +ai-agents-delegate-actions/env/ +ai-agents-delegate-actions/.venv/ +ai-agents-delegate-actions/.env/ + +# Environment variables +ai-agents-delegate-actions/.env + +# OS-specific +.DS_Store + +# Editor files +.idea/ +.vscode/ + +# IGNORE TOOLS FOLDER ENTIRELY +ai-agents-delegate-actions/tools/ \ No newline at end of file diff --git a/AI-agents-delegate-actions/main2.py b/AI-agents-delegate-actions/main2.py new file mode 100644 index 00000000..a387bb84 --- /dev/null +++ b/AI-agents-delegate-actions/main2.py @@ -0,0 +1,147 @@ +from dotenv import load_dotenv +load_dotenv() + +import os +import json +import google.generativeai as genai +from registry import ToolRegistry + +genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) + +MODEL_NAME = "models/gemini-2.5-flash" + + +# ----------------------------------------------------------- +# Convert FULL JSON tool schema into SAFE plain-text context +# ----------------------------------------------------------- +def convert_tool_to_plain_text(tool): + """ + Convert a tool with nested JSON into a safe plain-text representation. + This avoids JSON parsing failures and finish_reason=2 from Gemini. + """ + + text = f"TOOL: {tool['name']}\n" + text += f"DESCRIPTION: {tool.get('description', '')}\n" + + # Arguments + args = tool.get("arguments", {}) + if isinstance(args, dict): + text += "ARGUMENTS:\n" + for k, v in args.items(): + text += f" - {k}: {v}\n" + else: + text += f"ARGUMENTS: {args}\n" + + # Returns + returns = tool.get("returns", {}) + if isinstance(returns, dict): + text += "RETURNS:\n" + for k, v in returns.items(): + text += f" - {k}: {v}\n" + else: + text += f"RETURNS: {returns}\n" + + # Optional usage/examples + if "usage" in tool: + text += f"USAGE EXAMPLE: {tool['usage']}\n" + + # Optional advanced information + if "details" in tool: + text += f"DETAILS: {tool['details']}\n" + + text += "-" * 40 + "\n" + return text + + +def build_full_plain_context(registry: ToolRegistry): + """Build the FULL context (bruteforce mode) using plain text only.""" + ctx = "=== SINGLE-AGENT MODE — FULL TOOLSET AS PLAIN TEXT ===\n\n" + ctx += "Modelul primește TOATE tool-urile aici în format text simplu.\n" + ctx += "Această variantă este intenționat ineficientă pentru comparație cu MCP.\n\n" + + # Categories + ctx += "=== TOOL CATEGORIES ===\n" + for cat in registry.get_all_categories(): + ctx += f"- {cat}\n" + + ctx += "\n=== ALL TOOL DEFINITIONS (PLAIN TEXT) ===\n" + + # Add each tool in plain text WITHOUT printing them in terminal + for t in registry.tools.values(): + ctx += convert_tool_to_plain_text(t) + + ctx += """ +=== INSTRUCȚIUNI === +Ești un singur agent (fără Sub-Agent). + +1. Utilizezi informațiile de mai sus ca baza completă de tool-uri. +2. Pe baza promptului userului: + a) Identifici dacă e nevoie să folosești tool-uri. + b) Alegi toolurile relevante (din lista completă). + c) Explici ce tooluri ai alege. + d) Generezi răspunsul final. +3. Nu inventa tool-uri. +4. Ai tot contextul în system_instruction — doar un singur call. +""" + + return ctx + + +# ----------------------------------------------------------- +# MAIN — Single Agent Plain Text Bruteforce Mode +# ----------------------------------------------------------- +def main(): + print("=== SINGLE-AGENT (FULL TOOLSET PLAIN TEXT) ===") + + # Load all tools + registry = ToolRegistry() + registry.load_folder("tools") + + # Only show counts, not details + print(f"[INFO] Loaded {len(registry.tools)} total tools.") + print(f"[INFO] Loaded {len(registry.categories)} categories.") + + # Build huge plain-text context + system_prompt = build_full_plain_context(registry) + + # User input + user_prompt = input("\nUser: ") + + # Initialize model with huge context + model = genai.GenerativeModel( + model_name=MODEL_NAME, + system_instruction=system_prompt + ) + + print("\n[INFO] Sending FULL single-agent prompt to Gemini...\n") + + # SAFE generation + response = model.generate_content(user_prompt) + cand = response.candidates[0] + + # Extract response safely + if not cand.content.parts: + print("⚠️ Model returned no text.") + print("finish_reason =", cand.finish_reason) + return + + result = cand.content.parts[0].text + + # 🔥 SAVE RESULT TO FILE (instead of printing) + filename = "raspuns_model_single_agent.md" + with open(filename, "w", encoding="utf-8") as f: + f.write(result) + + print(f"\n=== MODEL RESPONSE SAVED TO {filename} ===") + + # Token usage + usage = response.usage_metadata + + print("\n=== TOKEN USAGE (SINGLE AGENT FULL CONTEXT) ===") + print("Input tokens: ", usage.prompt_token_count) + print("Output tokens:", usage.candidates_token_count) + print("Total tokens: ", usage.total_token_count) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/AI-agents-delegate-actions/main_agent.py b/AI-agents-delegate-actions/main_agent.py new file mode 100644 index 00000000..c1df11de --- /dev/null +++ b/AI-agents-delegate-actions/main_agent.py @@ -0,0 +1,122 @@ +from dotenv import load_dotenv +load_dotenv() + +import json +import os +import google.generativeai as genai + +from sub_agent_modular import SubAgentLLM + +genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) + +MODEL_NAME = "models/gemini-2.5-flash" + + +def gemini_answer_with_tools(prompt, selected_tools): + print("\n[MainAgent] Injecting tool context...") + + FULL_LIMIT = 10 + + if len(selected_tools) <= FULL_LIMIT: + print("[MainAgent] Injecting FULL tool schema (<= 10 tools).") + tool_ctx = "" + for t in selected_tools: + tool_ctx += f"TOOL: {t['name']}\n" + tool_ctx += f"DESCRIPTION: {t.get('description','')}\n" + tool_ctx += f"ARGUMENTS: {json.dumps(t.get('arguments',{}), indent=2)}\n" + tool_ctx += f"RETURNS: {json.dumps(t.get('returns',{}), indent=2)}\n" + tool_ctx += f"USAGE: {t.get('usage','No example')}\n" + tool_ctx += f"DETAILS: {t.get('details','No details')}\n" + tool_ctx += "-----------------------------------------\n" + else: + print("[MainAgent] Too many tools (> 10). Injecting REDUCED schema.") + tool_ctx = "" + for t in selected_tools: + tool_ctx += f"- {t['name']}: {t['description']}\n" + + system_msg = f""" +Ești agentul principal. + +Ai acces la următoarele tool-uri relevante: +{tool_ctx} + +Instrucțiuni: +- Dacă schema este completă, folosește argumentele corecte. +- Dacă schema este redusă, deduci intenția și explici tool-ul. +- Nu inventa tool-uri noi. +- Răspunde pe baza tool-urilor date. +""" + + model = genai.GenerativeModel( + model_name=MODEL_NAME, + system_instruction=system_msg + ) + + response = model.generate_content(prompt) + + # 🔥 returnăm și response.usage_metadata + return response.text, response.usage_metadata + + + +def main(): + print("=== AI Delegate Action System ===") + + user_prompt = input("\nUser: ") + print("\n[MainAgent] Passing prompt to Sub-Agent for classification...") + + sub = SubAgentLLM(model_name=MODEL_NAME) + + use_tools = sub.classify_use_tools(user_prompt) + print(f"[SubAgent] USE_TOOLS = {use_tools}") + + if not use_tools: + print("\n[MainAgent] No tools needed. Gemini answering normally...\n") + model = genai.GenerativeModel(MODEL_NAME) + resp = model.generate_content(user_prompt) + + usage = resp.usage_metadata + print("\n=== MAIN AGENT TOKEN USAGE ===") + print(f"Input tokens (system + user): {usage.prompt_token_count}") + print(f"Output tokens: {usage.candidates_token_count}") + print(f"Total tokens: {usage.total_token_count}") + + print("\n=== FINAL ANSWER ===\n") + print(resp.text) + return + + print("\n[MainAgent] Asking Sub-Agent for category detection...") + categories = sub.detect_categories(user_prompt) + print("[SubAgent] Categories:", categories) + + if not categories: + print("[MainAgent] No categories found.") + return + + print("\n[MainAgent] Asking Sub-Agent for specific tool selection...") + minimal_defs = sub.discover_and_minimize(user_prompt, categories) + + if not minimal_defs: + print("[MainAgent] No tools selected.") + return + + print("\n[MainAgent] Sending final minimal tool context to Gemini...\n") + result_text, usage = gemini_answer_with_tools(user_prompt, minimal_defs) + + + print("\n=== FINAL ANSWER SAVED TO FILE ===") + +# scrie răspunsul în fișier + with open("raspuns_model_mcp.md", "w", encoding="utf-8") as f: + f.write(result_text) + + print("File created: raspuns_model_mcp.md") + print("\n=== MAIN AGENT TOKEN USAGE ===") + print(f"Input tokens (system + user): {usage.prompt_token_count}") + print(f"Output tokens: {usage.candidates_token_count}") + print(f"Total tokens: {usage.total_token_count}") + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/AI-agents-delegate-actions/registry.py b/AI-agents-delegate-actions/registry.py new file mode 100644 index 00000000..ce6b0897 --- /dev/null +++ b/AI-agents-delegate-actions/registry.py @@ -0,0 +1,56 @@ +import json +import os + +class ToolRegistry: + def __init__(self): + self.tools = {} # name → tool schema + self.categories = set() # e.g. "cdl.calendar", "dogedit.core" + + def load_folder(self, folder): + for file in os.listdir(folder): + if file.endswith(".json"): + with open(os.path.join(folder, file)) as f: + for tool in json.load(f): + name = tool["name"] + self.tools[name] = tool + + # extract category prefix: "cdl.calendar" + parts = name.split(".") + if len(parts) >= 2: + cat = parts[0] + "." + parts[1] + self.categories.add(cat) + + def get_all_categories(self): + """Return sorted list of unique categories.""" + return sorted(list(self.categories)) + + def get_all_tool_names(self): + """Return list of all tool names (for classification).""" + return list(self.tools.keys()) + + def get_tools_by_category(self, category_prefix): + """Return all tools starting with that prefix.""" + return [ + tool for tool in self.tools.values() + if tool["name"].startswith(category_prefix) + ] + + def get_tools_by_names(self, names): + """Return tool definitions for given names (if they exist).""" + return [ + self.tools[name] + for name in names + if name in self.tools + ] + + def get_minimal_tool_info(self, names): + """Return minimal (name + description) for MCP-style injection.""" + result = [] + for n in names: + if n in self.tools: + t = self.tools[n] + result.append({ + "name": t["name"], + "description": t["description"] + }) + return result \ No newline at end of file diff --git a/AI-agents-delegate-actions/sub_agent_modular.py b/AI-agents-delegate-actions/sub_agent_modular.py new file mode 100644 index 00000000..63aa3484 --- /dev/null +++ b/AI-agents-delegate-actions/sub_agent_modular.py @@ -0,0 +1,196 @@ +import google.generativeai as genai +import json +from registry import ToolRegistry + + +class SubAgentLLM: + def __init__(self, model_name="models/gemini-2.5-flash"): + self.model_name = model_name + self.registry = ToolRegistry() + self.registry.load_folder("tools") + + self.categories = self.registry.get_all_categories() + + print("[SubAgent] Loaded categories:", self.categories) + print("[SubAgent] Loaded total tools:", len(self.registry.tools)) + + # ---------------------------------------------------- + # 1) Should we use tools? + # ---------------------------------------------------- + def classify_use_tools(self, prompt): + print("\n[SubAgent] classify_use_tools()") + print("[SubAgent] Prompt:", prompt) + + # MCP LOGIC: use ONLY categories here + categories_text = "\n".join(f"- {c}" for c in self.categories) + + system_msg = f""" +Ai următoarele CATEGORII de tool-uri: + +{categories_text} + +Analizează cererea utilizatorului și decide dacă +cererea NECESITĂ vreun tool din ORICARE din aceste categorii. + +Răspunde STRICT: +USE_TOOLS +sau +NO_TOOLS + +Nu explica, nu adăuga text suplimentar. +""" + + model = genai.GenerativeModel( + model_name=self.model_name, + system_instruction=system_msg + ) + + response = model.generate_content(prompt) + result = response.text.strip().upper() + + print("[SubAgent] Classifier raw output:", result) + return "USE_TOOLS" in result + + # ---------------------------------------------------- + # 2) Detect categories (with Markdown JSON cleanup) + # ---------------------------------------------------- + def detect_categories(self, prompt): + print("\n[SubAgent] detect_categories()") + + categories_text = "\n".join(self.categories) + + system_msg = f""" + Ai următoarele categorii: + + {categories_text} + + Selectează TOATE categoriile relevante pentru cererea utilizatorului. + Returnează STRICT un JSON list. + + Exemplu: + ["cdl.calendar", "dogedit.core"] + """ + + model = genai.GenerativeModel( + model_name=self.model_name, + system_instruction=system_msg + ) + + response = model.generate_content(prompt) + + raw = response.text.strip() + print("[SubAgent] Category detection raw:", raw) + + # ----------------------------- + # Clean Gemini's ```json output + # ----------------------------- + cleaned = raw + + # remove triple backticks + if cleaned.startswith("```"): + cleaned = cleaned.strip("`").strip() + # remove optional json prefix + if cleaned.lower().startswith("json"): + cleaned = cleaned[4:].strip() + + cleaned = cleaned.replace("```", "").strip() + + print("[SubAgent] Cleaned JSON:", cleaned) + + # ----------------------------- + # JSON PARSING + # ----------------------------- + try: + parsed = json.loads(cleaned) + if isinstance(parsed, list): + return parsed + print("[SubAgent] Parsed output is not a list.") + return [] + except Exception as e: + print("[SubAgent] JSON parse error:", e) + return [] + + # ---------------------------------------------------- + # 3) Discover tools + return FULL or MINIMAL (MCP-style) + # ---------------------------------------------------- + def discover_and_minimize(self, prompt, categories): + print("\n[SubAgent] discover_and_minimize()") + print("[SubAgent] Categories:", categories) + + # 1. Combine tool candidates + candidates = [] + for c in categories: + candidates += self.registry.get_tools_by_category(c) + + print("[SubAgent] Candidate tools:", len(candidates)) + + # Build minimal tool list text (for LLM selection) + tool_list_text = "\n".join( + f"- {t['name']}: {t['description']}" + for t in candidates + ) + + system_msg = f""" +Ai aceste tool-uri candidate: + +{tool_list_text} + +Selectează DOAR tool-urile RELEVANTE pentru acest prompt. +Returnează STRICT un JSON list cu numele tool-urilor, de forma: +["cdl.calendar.event_create"] + +NU explica. +NU adăuga text suplimentar. +""" + + model = genai.GenerativeModel( + model_name=self.model_name, + system_instruction=system_msg + ) + + response = model.generate_content(prompt) + raw_text = response.text.strip() + + print("\n[SubAgent] Tool discovery raw response:") + print(raw_text) + + # ----------------------------- + # Clean Gemini Markdown JSON + # ----------------------------- + cleaned = raw_text + + if cleaned.startswith("```"): + cleaned = cleaned.strip("`").strip() + if cleaned.lower().startswith("json"): + cleaned = cleaned[4:].strip() + cleaned = cleaned.replace("```", "").strip() + + print("\n[SubAgent] Cleaned JSON for parsing:") + print(cleaned) + + # ----------------------------- + # Parse JSON list + # ----------------------------- + try: + selected = json.loads(cleaned) + if not isinstance(selected, list): + print("[SubAgent] Parsed output is not a list. Returning empty.") + return [] + except Exception as e: + print("[SubAgent] JSON parsing failed:", e) + return [] + + print("[SubAgent] Selected tool names:", selected) + + # ----------------------------- + # Return FULL or MINIMAL based on count + # ----------------------------- + if len(selected) <= 10: + print("[SubAgent] Returning FULL schema (<= 10 tools).") + full_defs = self.registry.get_tools_by_names(selected) + return full_defs + + else: + print("[SubAgent] Returning MINIMAL schema (> 10 tools).") + minimal_defs = self.registry.get_minimal_tool_info(selected) + return minimal_defs \ No newline at end of file