diff --git a/.gitignore b/.gitignore index 9a9ac141..ef5fb7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ workspace/ white_collar_agent/external_tools/ white_collar_agent/generated_task_document/ **/__pycache__/ +**/rag_docs_actions/ +**/rag_docs_taskdocs/ ollama_data/ node_modules .env diff --git a/agents/dog_agent/__init__.py b/agents/dog_agent/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/agents/dog_agent/agent.py b/agents/dog_agent/agent.py new file mode 100644 index 00000000..c949b945 --- /dev/null +++ b/agents/dog_agent/agent.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +""" +DogAgent +======== + +A dog + +""" + +from __future__ import annotations + +import importlib.util +from importlib import import_module +from pathlib import Path + +import yaml + +from core.agent_base import AgentBase +from core.logger import logger + + +class DogAgent(AgentBase): + """A friendly, dog-themed specialist built on :class:`AgentBase`.""" + + # Factory for Docker entrypoint / tests + @classmethod + def from_bundle(cls, bundle_dir: str | Path) -> "DogAgent": + bundle_path = Path(bundle_dir).resolve() + cfg = yaml.safe_load((bundle_path / "config.yaml").read_text()) + return cls(cfg, bundle_path) + + def __init__(self, cfg: dict, bundle_path: Path): + self._bundle_path = Path(bundle_path) + self._cfg = cfg + + super().__init__( + data_dir=cfg.get("data_dir", "core/data"), + chroma_path=str(self._bundle_path / cfg.get("rag_dir", "rag_docs")), + llm_provider=cfg.get("llm_provider", "byteplus"), + ) + + # -------- AgentBase hooks ----------------------------------------- # + + def _generate_role_info_prompt(self) -> str: + return ( + "You are a custom agent named DOG, a loyal and upbeat canine-inspired assistant.\n" + "You MUST NOT talk at all. You MUST replace all human language with\n" + "something like: 'Woof wooof woof, wooof wooff woof woff'\n" + "You MUST NOT talk in human language AT ALL AS THAT VIOLATES your agent POLICY.\n" + "When using the 'send message' action, you MUST NOT send message in human langauge.\n" + "You must REPLACE all English and human language words with 'woof' of different style and length.\n" + "You do, however, still execute task for human using actions " + "and offering encouraging nudges to stay productive." + ) + +if __name__ == "__main__": + import asyncio + + bundle_dir = Path(__file__).parent + agent = DogAgent.from_bundle(bundle_dir) + asyncio.run(agent.run()) \ No newline at end of file diff --git a/agents/dog_agent/config.yaml b/agents/dog_agent/config.yaml new file mode 100644 index 00000000..c76c03fe --- /dev/null +++ b/agents/dog_agent/config.yaml @@ -0,0 +1,6 @@ +data_dir: agents/dog_agent/data/ +rag_dir: rag_docs +rag_namespace: dog_agent_knowledge + +max_tokens: 16000 +llm_provider: byteplus diff --git a/agents/dog_agent/data/action/dog_behaviour.py b/agents/dog_agent/data/action/dog_behaviour.py new file mode 100644 index 00000000..5429f911 --- /dev/null +++ b/agents/dog_agent/data/action/dog_behaviour.py @@ -0,0 +1,330 @@ +from core.action.action_framework.registry import action + +@action( + name="bark", + description="Use this action to send message to users by barking, instead of human speech.", + execution_mode="internal", + input_schema={ + "message": { + "type": "string", + "example": "Woof wooofff wooff woooof woof!", + "description": "Bark to the user." + }, + "wait_for_user_reply": { + "type": "boolean", + "example": True, + "description": "True if this action require user's response to proceed. For example, true if you ask a question in the message." + } + }, + output_schema={ + "status": { + "type": "string", + "example": "ok", + "description": "Indicates the action completed successfully." + }, + "message": { + "type": "string", + "example": "Woof wooofff wooff woooof woof!", + "description": "Bark to the user." + }, + "fire_at_delay": { + "type": "number", + "example": 10800, + "description": "Delay in seconds before the next follow-up action should be scheduled. 10800 seconds (3 hours) if wait_for_user_reply is true, otherwise 0." + } + }, + test_payload={ + "question": "Woof wooofff wooff woooof woof?", + "wait_for_user_reply": False, + "simulated_mode": True + } +) +def bark(input_data: dict) -> dict: + import json + import asyncio + + message = input_data['message'] + wait_for_user_reply = bool(input_data.get('wait_for_user_reply', False)) + + import core.internal_action_interface as internal_action_interface + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(message)) + + fire_at_delay = 10800 if wait_for_user_reply else 0 + return {'status': 'success', 'message': message, 'fire_at_delay': fire_at_delay} + +@action( + name="sit", + description="Display an ASCII image of a dog sitting.", + execution_mode="internal", + input_schema={}, + output_schema={ + "status": { + "type": "string", + "example": "success", + "description": "Indicates the action completed successfully." + } + }, + test_payload={ + "simulated_mode": True + } +) +def sit(input_data: dict) -> dict: + import asyncio + import core.internal_action_interface as internal_action_interface + + dog_ascii = r""". + __ + __()'`; + //, /` + /_)_-|| +""" + + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(dog_ascii)) + return {"status": "success"} + + +from core.action.action_framework.registry import action + +@action( + name="wiggle tail", + description="Display an ASCII image of a dog sitting and wiggling its tail.", + execution_mode="internal", + input_schema={}, + output_schema={ + "status": { + "type": "string", + "example": "success", + "description": "Indicates the action completed successfully." + } + }, + test_payload={ + "simulated_mode": True + } +) +def wiggle_tail(input_data: dict) -> dict: + import asyncio + import core.internal_action_interface as internal_action_interface + + dog_ascii = r""". + __ + (~(__()'`; + /, /` + \\"--\\ +""" + + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(dog_ascii)) + return {"status": "success"} + + +@action( + name="eat", + description="Display an ASCII image of a dog eating and making nom nom noise.", + execution_mode="internal", + input_schema={ + "nom_nom_noise": { + "type": "string", + "example": "Nom nom nom", + "description": "The nom nom noise depending on the portion of food." + }, + }, + output_schema={ + "status": { + "type": "string", + "example": "success", + "description": "Indicates the action completed successfully." + }, + "nom_nom_noise": { + "type": "string", + "example": "Nom nom nom", + "description": "The nom nom noise depending on the portion of food." + }, + }, + test_payload={ + "simulated_mode": True + } +) +def eat(input_data: dict) -> dict: + import asyncio + import core.internal_action_interface as internal_action_interface + + dog_ascii = r""". + __ + (___()'`; + /, /` ____ + \\"--\\ /_oo \ + \____/ +""" + nom_nom_noise = input_data['nom_nom_noise'] + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(dog_ascii)) + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(nom_nom_noise)) + return {"status": "success", "nom_nom_noise": nom_nom_noise} + + +@action( + name="sniff", + description="Display an ASCII sniffing animation, then announce what the dog found.", + execution_mode="internal", + input_schema={ + "found": { + "type": "string", + "example": "a bone", + "description": "What the dog found after sniffing." + } + }, + output_schema={ + "status": { + "type": "string", + "example": "success", + "description": "Indicates the action completed successfully." + }, + "found": { + "type": "string", + "example": "a bone", + "description": "What the dog found after sniffing." + }, + "message": { + "type": "string", + "example": "*dog found a bone*", + "description": "Formatted message announcing what the dog found." + } + }, + test_payload={ + "found": "a bone", + "simulated_mode": True + } +) +def sniff(input_data: dict) -> dict: + import asyncio + import time + import core.internal_action_interface as internal_action_interface + + found = input_data["found"] + message = f"*dog found {found}*" + + frames = [ + r""". + __ + (___()'`; + /, /` ~ + \\"--\\ +""", + r""". + __ + (___()'`; ~ ~ + /, /` ~ + \\"--\\ +""", + r""". + __ + (___()'`; ~ ~ ~ + /, /` ~ ~ + \\"--\\ +""" + ] + + for f in frames: + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(f)) + time.sleep(10) + + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(message)) + return {"status": "success", "found": found, "message": message} + + +@action( + name="dig", + description="Display an ASCII digging animation, then announce what the dog found.", + execution_mode="internal", + input_schema={ + "found": { + "type": "string", + "example": "a buried toy", + "description": "What the dog found after digging." + }, + "dig_seconds": { + "type": "number", + "example": 4, + "description": "How long the dog digs (seconds). Clamped to 3–5 seconds." + } + }, + output_schema={ + "status": { + "type": "string", + "example": "success", + "description": "Indicates the action completed successfully." + }, + "found": { + "type": "string", + "example": "a buried toy", + "description": "What the dog found after digging." + }, + "message": { + "type": "string", + "example": "*dog found a buried toy*", + "description": "Formatted message announcing what the dog found." + }, + "dig_seconds": { + "type": "number", + "example": 4, + "description": "Actual digging duration used (seconds), after clamping." + } + }, + test_payload={ + "found": "a buried toy", + "dig_seconds": 4, + "simulated_mode": True + } +) +def dig(input_data: dict) -> dict: + import asyncio + import time + import core.internal_action_interface as internal_action_interface + + found = input_data["found"] + message = f"*dog found {found}*" + + try: + dig_seconds = float(input_data.get("dig_seconds", 5)) + except (TypeError, ValueError): + dig_seconds = 5.0 + dig_seconds = max(5.0, min(10.0, dig_seconds)) + + frames = [ + r""". + __ + (___()'`; + /, /` + \\"--\\ + +""", + r""". + __ + (___()'`; + /, \\ + \\"--` \\ ' " + +""", + r""". + __ + (___()'`; + /, /` + \\"--\\ " ' + +""", + r""". + __ + (___()'`; + /, \\ + \\"--` \\ '" + +""" + ] + + frame_delay = 3 + total_frames = int(dig_seconds / frame_delay) + for i in range(total_frames): + f = frames[i % 4] + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(f)) + time.sleep(frame_delay) + + asyncio.run(internal_action_interface.InternalActionInterface.do_chat(message)) + return {"status": "success", "found": found, "message": message, "dig_seconds": dig_seconds} \ No newline at end of file diff --git a/agents/dog_agent/data/agent_info.json b/agents/dog_agent/data/agent_info.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/agents/dog_agent/data/agent_info.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/core/action/action_framework/loader.py b/core/action/action_framework/loader.py index 0154e943..5b460c19 100644 --- a/core/action/action_framework/loader.py +++ b/core/action/action_framework/loader.py @@ -4,6 +4,7 @@ import sys from typing import List import logging +from pathlib import Path logger = logging.getLogger("ActionLoader") @@ -25,6 +26,8 @@ def load_actions_from_directories(base_dir: str = None, paths_to_scan: List[str] if paths_to_scan is None: paths_to_scan = DEFAULT_ACTION_PATHS + else: + paths_to_scan += DEFAULT_ACTION_PATHS logger.info(f"--- Starting Action Discovery from base: {base_dir} ---") @@ -32,7 +35,8 @@ def load_actions_from_directories(base_dir: str = None, paths_to_scan: List[str] processed_files = set() for relative_path in paths_to_scan: - full_search_path = os.path.join(base_dir, relative_path) + relative_path = Path(relative_path) + full_search_path = Path(base_dir) / relative_path if not os.path.exists(full_search_path): logger.debug(f"Skipping non-existent directory: {full_search_path}") @@ -43,7 +47,9 @@ def load_actions_from_directories(base_dir: str = None, paths_to_scan: List[str] # Walk the directory tree for root, _, files in os.walk(full_search_path): # Special handling to only look into 'data/action' if we are scanning the 'agents' folder - if 'agents' in relative_path and 'data' in root and 'action' not in root: + root_path = Path(root) + + if "agents" in relative_path.parts and "data" in root_path.parts and "action" not in root_path.parts: continue for file in files: diff --git a/core/action/action_library.py b/core/action/action_library.py index 65977baf..90bae02b 100644 --- a/core/action/action_library.py +++ b/core/action/action_library.py @@ -42,18 +42,6 @@ def store_action(self, action: Action): action_dict["updatedAt"] = datetime.datetime.utcnow().isoformat() self.db_interface.store_action(action_dict) - def sync_databases(self): - """ - Ensures that all actions stored in data/actions folder are present in ChromaDB. - If an action is missing from ChromaDB, it will be added. - """ - logger.debug("Syncing MongoDB and ChromaDB...") - added_count = self.db_interface.sync_actions_to_chroma() - if added_count > 0: - logger.debug(f"Added {added_count} missing actions to ChromaDB.") - else: - logger.debug("Databases are already in sync. No missing actions found.") - def retrieve_action(self, action_name: str) -> Optional[Action]: """ Fetch a single action by name. diff --git a/core/agent_base.py b/core/agent_base.py index 82299901..4cbd6b49 100644 --- a/core/agent_base.py +++ b/core/agent_base.py @@ -106,7 +106,6 @@ def __init__( # action & task layers self.action_library = ActionLibrary(self.llm, db_interface=self.db_interface) - self.action_library.sync_databases() # base tools self.task_docs_path = "core/data/task_document" if self.task_docs_path: @@ -592,7 +591,7 @@ def _generate_role_info_prompt(self) -> str: Subclasses override this to return role-specific system instructions (responsibilities, behaviour constraints, expected domain tasks, etc). """ - return "" + return "You are an AI agent, named 'white collar agent', developed by CraftOS, a general computer-use AI agent that can switch between CLI/GUI mode." def _build_db_interface(self, *, data_dir: str, chroma_path: str): """A tiny wrapper so a subclass can point to another DB/collection.""" diff --git a/core/context_engine.py b/core/context_engine.py index bc5bae5b..9dc945ef 100644 --- a/core/context_engine.py +++ b/core/context_engine.py @@ -6,6 +6,7 @@ from core.config import AGENT_WORKSPACE_ROOT from core.logger import logger from core.prompt import ( + AGENT_ROLE_PROMPT, AGENT_INFO_PROMPT, AGENT_STATE_PROMPT, ENVIRONMENTAL_CONTEXT_PROMPT, @@ -68,8 +69,9 @@ def create_system_role_info(self): Calls the injected role-specific prompt function, if any. """ if self._role_info_func: - return self._role_info_func() - return "" # No-op by default + role = self._role_info_func() + return AGENT_ROLE_PROMPT.format(role=role) + return "" def create_system_agent_state(self): """Return formatted agent properties for the current session.""" @@ -189,8 +191,8 @@ def make_prompt( """ system_default_flags = { - "agent_info": True, "role_info": True, + "agent_info": True, "agent_state": self.state_manager.is_running_task(), "conversation_history": True, "event_stream": True, @@ -208,8 +210,8 @@ def make_prompt( user_flags = {**user_default_flags, **(user_flags or {})} system_sections = [ - ("agent_info", self.create_system_agent_info), ("role_info", self.create_system_role_info), + ("agent_info", self.create_system_agent_info), ("agent_state", self.create_system_agent_state), ("conversation_history", self.create_system_conversation_history), ("event_stream", self.create_system_event_stream_state), diff --git a/core/data/action/grep.py b/core/data/action/grep.py index b5c0c1f4..b9034c92 100644 --- a/core/data/action/grep.py +++ b/core/data/action/grep.py @@ -9,7 +9,7 @@ "input_file": { "type": "string", "example": "/path/to/input.txt", - "description": "Absolute or relative path to the input text file to search. The file must already exist on disk and be readable as UTF-8 text (binary files are not supported)." + "description": "Absolute to the input text file to search. The file must already exist on disk and be readable as UTF-8 text (binary files are not supported)." }, "keywords": { "type": "array", @@ -177,11 +177,12 @@ def clean_text(s): formatted_chunks.append(para) result = { + 'status': 'success', 'chunks': formatted_chunks, 'total_matches': total_matches, 'returned_range': [start_idx_clamped, end_idx_clamped] } - output = (json.dumps(result)) + return result def chunk_text(text, chunk_size=300, overlap=50): @@ -225,7 +226,7 @@ def chunk_text(text, chunk_size=300, overlap=50): "input_file": { "type": "string", "example": "/path/to/input.txt", - "description": "Absolute or relative path to the input text file to search. The file must already exist on disk and be readable as UTF-8 text (binary files are not supported)." + "description": "Absolute to the input text file to search. The file must already exist on disk and be readable as UTF-8 text (binary files are not supported)." }, "keywords": { "type": "array", @@ -383,11 +384,12 @@ def clean_text(s): formatted_chunks.append(para) result = { + 'status': 'success', 'chunks': formatted_chunks, 'total_matches': total_matches, 'returned_range': [start_idx_clamped, end_idx_clamped] } - output = (json.dumps(result)) + return result def chunk_text(text, chunk_size=300, overlap=50): @@ -431,7 +433,7 @@ def chunk_text(text, chunk_size=300, overlap=50): "input_file": { "type": "string", "example": "/path/to/input.txt", - "description": "Absolute or relative path to the input text file to search. The file must already exist on disk and be readable as UTF-8 text (binary files are not supported)." + "description": "Absolute to the input text file to search. The file must already exist on disk and be readable as UTF-8 text (binary files are not supported)." }, "keywords": { "type": "array", @@ -589,11 +591,12 @@ def clean_text(s): formatted_chunks.append(para) result = { + 'status': 'success', 'chunks': formatted_chunks, 'total_matches': total_matches, 'returned_range': [start_idx_clamped, end_idx_clamped] } - output = (json.dumps(result)) + return result def chunk_text(text, chunk_size=300, overlap=50): diff --git a/core/data/action/shell exec.py b/core/data/action/shell exec.py index e73b20b8..8dbf2f15 100644 --- a/core/data/action/shell exec.py +++ b/core/data/action/shell exec.py @@ -205,6 +205,9 @@ def shell_exec_windows(input_data: dict) -> dict: command = str(input_data.get('command', '')).strip() shell_choice = str(input_data.get('shell', 'cmd')).strip().lower() + if shell_choice == 'auto': + shell_choice = 'cmd' + shell_choice = shell_choice if shell_choice in ('cmd', 'powershell', 'pwsh') else 'cmd' timeout_val = input_data.get('timeout') cwd = input_data.get('cwd') env_input = input_data.get('env') or {} @@ -226,7 +229,8 @@ def shell_exec_windows(input_data: dict) -> dict: elif shell_choice == 'pwsh': args = ['pwsh.exe', '-NoLogo', '-NonInteractive', '-NoProfile', '-Command', command] else: - args = ['cmd.exe', '/c', command] + # Use /d and /s to ensure quoted commands (e.g., paths with spaces) are handled consistently. + args = ['cmd.exe', '/d', '/s', '/c', command] run_kwargs = { 'capture_output': True, diff --git a/core/data/action/shell kill process.py b/core/data/action/shell kill process.py deleted file mode 100644 index 83220444..00000000 --- a/core/data/action/shell kill process.py +++ /dev/null @@ -1,452 +0,0 @@ -from core.action.action_framework.registry import action - -@action( - name="shell kill process (cross-platform)", - description="Terminates a process by PID or image name across Windows, macOS, and Linux.", - input_schema={ - "pid": { - "type": "integer", - "example": 1234, - "description": "Process ID to terminate. If provided, takes precedence over image_name." - }, - "image_name": { - "type": "string", - "example": "python", - "description": "Process image name (e.g., 'python' or 'chrome'). Used when pid is not provided." - }, - "force": { - "type": "boolean", - "example": True, - "description": "Forceful termination (-9 on Unix, /F on Windows)." - }, - "tree": { - "type": "boolean", - "example": True, - "description": "Kill the process and its children. Supported on Windows (/T) and Linux/macOS via pkill -P." - }, - "timeout": { - "type": "integer", - "example": 15, - "description": "Optional timeout in seconds for the kill command." - } - }, - output_schema={ - "status": { - "type": "string", - "example": "success", - "description": "'success' if the process was terminated, 'error' otherwise." - }, - "stdout": { - "type": "string", - "example": "Process terminated.", - "description": "Captured standard output from the termination command." - }, - "stderr": { - "type": "string", - "example": "", - "description": "Captured standard error from the termination command." - }, - "return_code": { - "type": "integer", - "example": 0, - "description": "Exit code from the termination command. 0 indicates success." - }, - "message": { - "type": "string", - "example": "No target specified.", - "description": "Optional message for validation or error details." - } - }, - test_payload={ - "pid": 1234, - "image_name": "python", - "force": True, - "tree": True, - "timeout": 15, - "simulated_mode": True - } -) -def shell_kill_process__cross_platform_(input_data: dict) -> dict: - import os, json, subprocess, platform - - simulated_mode = input_data.get('simulated_mode', False) - - if simulated_mode: - # Return mock result for testing - return { - 'status': 'success', - 'stdout': 'Process terminated successfully', - 'stderr': '', - 'return_code': 0, - 'message': '' - } - - pid = input_data.get('pid') - image_name = str(input_data.get('image_name', '')).strip() - force = bool(input_data.get('force', False)) - tree = bool(input_data.get('tree', False)) - timeout_val = input_data.get('timeout') - - system = platform.system().lower() - - if pid is None and not image_name: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Specify either pid or image_name.'} - - if system == 'windows': - args = ['taskkill'] - if pid is not None: - args += ['/PID', str(pid)] - else: - args += ['/IM', image_name] - if force: - args.append('/F') - if tree: - args.append('/T') - - else: - # macOS / Linux - if pid is not None: - sig = '-9' if force else '-15' - args = ['kill', sig, str(pid)] - else: - if tree: - args = ['pkill', '-f', image_name] - else: - args = ['pkill'] + (['-9'] if force else []) + ['-f', image_name] - - try: - result = subprocess.run( - args, - capture_output=True, - text=True, - errors='replace', - timeout=float(timeout_val) if timeout_val is not None else None, - shell=False - ) - return { - 'status': 'success' if result.returncode == 0 else 'error', - 'stdout': result.stdout.strip(), - 'stderr': result.stderr.strip(), - 'return_code': result.returncode, - 'message': '' - } - except subprocess.TimeoutExpired as e: - out = (e.stdout or '').strip() - err = (e.stderr or '').strip() - msg = f'Timed out after {timeout_val}s.' if timeout_val is not None else 'Timed out.' - return {'status': 'error', 'stdout': out, 'stderr': err, 'return_code': -1, 'message': msg} - except Exception as e: - return {'status': 'error', 'stdout': '', 'stderr': str(e), 'return_code': -1, 'message': str(e)} - -@action( - name="shell kill process (cross-platform)", - description="Terminates a process by PID or image name across Windows, macOS, and Linux.", - platforms=["windows"], - input_schema={ - "pid": { - "type": "integer", - "example": 1234, - "description": "Process ID to terminate. If provided, takes precedence over image_name." - }, - "image_name": { - "type": "string", - "example": "python", - "description": "Process image name (e.g., 'python' or 'chrome'). Used when pid is not provided." - }, - "force": { - "type": "boolean", - "example": True, - "description": "Forceful termination (-9 on Unix, /F on Windows)." - }, - "tree": { - "type": "boolean", - "example": True, - "description": "Kill the process and its children. Supported on Windows (/T) and Linux/macOS via pkill -P." - }, - "timeout": { - "type": "integer", - "example": 15, - "description": "Optional timeout in seconds for the kill command." - } - }, - output_schema={ - "status": { - "type": "string", - "example": "success", - "description": "'success' if the process was terminated, 'error' otherwise." - }, - "stdout": { - "type": "string", - "example": "Process terminated.", - "description": "Captured standard output from the termination command." - }, - "stderr": { - "type": "string", - "example": "", - "description": "Captured standard error from the termination command." - }, - "return_code": { - "type": "integer", - "example": 0, - "description": "Exit code from the termination command. 0 indicates success." - }, - "message": { - "type": "string", - "example": "No target specified.", - "description": "Optional message for validation or error details." - } - }, - test_payload={ - "pid": 1234, - "image_name": "python", - "force": True, - "tree": True, - "timeout": 15, - "simulated_mode": True - } -) -def shell_kill_process__cross_platform__windows(input_data: dict) -> dict: - import os, json, subprocess - - pid = input_data.get('pid') - image_name = str(input_data.get('image_name', '')).strip() - force = bool(input_data.get('force', False)) - tree = bool(input_data.get('tree', False)) - timeout_val = input_data.get('timeout') - - if pid is None and not image_name: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Specify either pid or image_name.'} - - args = ['taskkill'] - if pid is not None: - args += ['/PID', str(pid)] - else: - args += ['/IM', image_name] - if force: - args.append('/F') - if tree: - args.append('/T') - - creationflags = getattr(subprocess, 'CREATE_NO_WINDOW', 0) - - try: - result = subprocess.run(args, capture_output=True, text=True, errors='replace', timeout=float(timeout_val) if timeout_val else None, shell=False, creationflags=creationflags) - return { - 'status': 'success' if result.returncode == 0 else 'error', - 'stdout': result.stdout.strip(), - 'stderr': result.stderr.strip(), - 'return_code': result.returncode, - 'message': '' - } - except subprocess.TimeoutExpired: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Timed out.'} - except Exception as e: - return {'status': 'error', 'stdout': '', 'stderr': str(e), 'return_code': -1, 'message': str(e)} - -@action( - name="shell kill process (cross-platform)", - description="Terminates a process by PID or image name across Windows, macOS, and Linux.", - platforms=["linux"], - input_schema={ - "pid": { - "type": "integer", - "example": 1234, - "description": "Process ID to terminate. If provided, takes precedence over image_name." - }, - "image_name": { - "type": "string", - "example": "python", - "description": "Process image name (e.g., 'python' or 'chrome'). Used when pid is not provided." - }, - "force": { - "type": "boolean", - "example": True, - "description": "Forceful termination (-9 on Unix, /F on Windows)." - }, - "tree": { - "type": "boolean", - "example": True, - "description": "Kill the process and its children. Supported on Windows (/T) and Linux/macOS via pkill -P." - }, - "timeout": { - "type": "integer", - "example": 15, - "description": "Optional timeout in seconds for the kill command." - } - }, - output_schema={ - "status": { - "type": "string", - "example": "success", - "description": "'success' if the process was terminated, 'error' otherwise." - }, - "stdout": { - "type": "string", - "example": "Process terminated.", - "description": "Captured standard output from the termination command." - }, - "stderr": { - "type": "string", - "example": "", - "description": "Captured standard error from the termination command." - }, - "return_code": { - "type": "integer", - "example": 0, - "description": "Exit code from the termination command. 0 indicates success." - }, - "message": { - "type": "string", - "example": "No target specified.", - "description": "Optional message for validation or error details." - } - }, - test_payload={ - "pid": 1234, - "image_name": "python", - "force": True, - "tree": True, - "timeout": 15, - "simulated_mode": True - } -) -def shell_kill_process__cross_platform__linux(input_data: dict) -> dict: - import os, json, subprocess - - simulated_mode = input_data.get('simulated_mode', False) - - if simulated_mode: - # Return mock result for testing - return { - 'status': 'success', - 'stdout': 'Process terminated successfully', - 'stderr': '', - 'return_code': 0, - 'message': '' - } - - pid = input_data.get('pid') - image_name = str(input_data.get('image_name', '')).strip() - force = bool(input_data.get('force', False)) - tree = bool(input_data.get('tree', False)) - timeout_val = input_data.get('timeout') - - if pid is None and not image_name: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Specify either pid or image_name.'} - - if pid is not None: - sig = '-9' if force else '-15' - args = ['kill', sig, str(pid)] - else: - if tree: - args = ['pkill', '-f', image_name] - else: - args = ['pkill'] + (['-9'] if force else []) + ['-f', image_name] - - try: - result = subprocess.run(args, capture_output=True, text=True, errors='replace', timeout=float(timeout_val) if timeout_val else None, shell=False) - return { - 'status': 'success' if result.returncode == 0 else 'error', - 'stdout': result.stdout.strip(), - 'stderr': result.stderr.strip(), - 'return_code': result.returncode, - 'message': '' - } - except subprocess.TimeoutExpired: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Timed out.'} - except Exception as e: - return {'status': 'error', 'stdout': '', 'stderr': str(e), 'return_code': -1, 'message': str(e)} - -@action( - name="shell kill process (cross-platform)", - description="Terminates a process by PID or image name across Windows, macOS, and Linux.", - platforms=["darwin"], - input_schema={ - "pid": { - "type": "integer", - "example": 1234, - "description": "Process ID to terminate. If provided, takes precedence over image_name." - }, - "image_name": { - "type": "string", - "example": "python", - "description": "Process image name (e.g., 'python' or 'chrome'). Used when pid is not provided." - }, - "force": { - "type": "boolean", - "example": True, - "description": "Forceful termination (-9 on Unix, /F on Windows)." - }, - "tree": { - "type": "boolean", - "example": True, - "description": "Kill the process and its children. Supported on Windows (/T) and Linux/macOS via pkill -P." - }, - "timeout": { - "type": "integer", - "example": 15, - "description": "Optional timeout in seconds for the kill command." - } -}, - output_schema={ - "status": { - "type": "string", - "example": "success", - "description": "'success' if the process was terminated, 'error' otherwise." - }, - "stdout": { - "type": "string", - "example": "Process terminated.", - "description": "Captured standard output from the termination command." - }, - "stderr": { - "type": "string", - "example": "", - "description": "Captured standard error from the termination command." - }, - "return_code": { - "type": "integer", - "example": 0, - "description": "Exit code from the termination command. 0 indicates success." - }, - "message": { - "type": "string", - "example": "No target specified.", - "description": "Optional message for validation or error details." - } -}, -) -def shell_kill_process__cross_platform__darwin(input_data: dict) -> dict: - import os, json, subprocess - - pid = input_data.get('pid') - image_name = str(input_data.get('image_name', '')).strip() - force = bool(input_data.get('force', False)) - tree = bool(input_data.get('tree', False)) - timeout_val = input_data.get('timeout') - - if pid is None and not image_name: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Specify either pid or image_name.'} - - if pid is not None: - sig = '-9' if force else '-15' - args = ['kill', sig, str(pid)] - else: - if tree: - args = ['pkill', '-f', image_name] - else: - args = ['pkill'] + (['-9'] if force else []) + ['-f', image_name] - - try: - result = subprocess.run(args, capture_output=True, text=True, errors='replace', timeout=float(timeout_val) if timeout_val else None, shell=False) - return { - 'status': 'success' if result.returncode == 0 else 'error', - 'stdout': result.stdout.strip(), - 'stderr': result.stderr.strip(), - 'return_code': result.returncode, - 'message': '' - } - except subprocess.TimeoutExpired: - return {'status': 'error', 'stdout': '', 'stderr': '', 'return_code': -1, 'message': 'Timed out.'} - except Exception as e: - return {'status': 'error', 'stdout': '', 'stderr': str(e), 'return_code': -1, 'message': str(e)} \ No newline at end of file diff --git a/core/database_interface.py b/core/database_interface.py index b89d1c27..ce94c560 100644 --- a/core/database_interface.py +++ b/core/database_interface.py @@ -74,7 +74,7 @@ def __init__( self.chroma_taskdocs_coll = self.chroma_taskdocs.get_or_create_collection("task_documents") # Ensure Chroma stays in sync with the filesystem sources on startup - self.sync_actions_to_chroma() + self.sync_actions_to_chroma(paths_to_scan=[self.actions_dir]) # Retrieve everything currently in the collection stored_data = self.chroma_actions.get() @@ -426,14 +426,14 @@ def search_actions(self, query: str, top_k: int = 7) -> List[str]: ) return result.get("ids", [[]])[0] if result else [] - def sync_actions_to_chroma(self) -> int: + def sync_actions_to_chroma(self, paths_to_scan: List[str] = None) -> int: """ Build the Chroma action collection from JSON files on disk. Returns: Number of action definitions indexed in Chroma. """ - load_actions_from_directories() + load_actions_from_directories(paths_to_scan=paths_to_scan) actions: List[Dict[str, Any]] = registry_instance.list_all_actions_as_json() diff --git a/core/prompt.py b/core/prompt.py index d5708a32..65938901 100644 --- a/core/prompt.py +++ b/core/prompt.py @@ -352,13 +352,15 @@ """ +AGENT_ROLE_PROMPT = """ + +{role} + +""" + # --- Context Engine --- # TODO: Inject OS information into the prompt, we put Windows as default for now. AGENT_INFO_PROMPT = """ - -You are an AI agent, named 'white collar agent', developed by CraftOS, a general computer-use AI agent that can switch between CLI/GUI mode. - - Here are your responsibilities: - You aid the user with general computer-use and browser-use tasks, following their request.