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.