Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions agent_core/core/impl/config/watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,25 +233,20 @@ def _trigger_reload(self, file_path: Path) -> None:
# Check if callback is async
if asyncio.iscoroutinefunction(callback):
if self._event_loop and self._event_loop.is_running():
# Schedule in the event loop
asyncio.run_coroutine_threadsafe(callback(), self._event_loop)
# Schedule in the event loop (non-blocking)
future = asyncio.run_coroutine_threadsafe(callback(), self._event_loop)
future.add_done_callback(lambda f: f.exception()) # Suppress unhandled exception warning
else:
# Create new event loop for this thread
asyncio.run(callback())
else:
# Sync callback
callback()

# Update last modified time
if config.path.exists():
config.last_modified = config.path.stat().st_mtime

logger.info(f"[CONFIG_WATCHER] Reload complete for {file_path.name}")

except Exception as e:
logger.error(f"[CONFIG_WATCHER] Reload failed for {file_path.name}: {e}")
import traceback
logger.debug(traceback.format_exc())
logger.warning(f"[CONFIG_WATCHER] Reload error for {file_path.name}: {e}")


# Global singleton instance
Expand Down
14 changes: 7 additions & 7 deletions agent_core/core/prompts/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@
- If platform is WhatsApp → MUST use send_whatsapp_web_text_message (use to="user" for self-messages)
- If platform is Discord → MUST use send_discord_message or send_discord_dm
- If platform is Slack → MUST use send_slack_message
- If platform is CraftBot TUI (or no platform specified) → use send_message
- If platform is CraftBot interface (or no platform specified) → use send_message
- ONLY fall back to send_message if the platform's send action is not in the available actions list.
- send_message is for local TUI display ONLY. It does NOT reach external platforms.
- send_message is for local interface display ONLY. It does NOT reach external platforms.

Third-Party Message Handling:
- Third-party messages show as "[Incoming X message from NAME]" in event stream.
Expand All @@ -67,7 +67,7 @@
Preferred Platform Routing (for notifications):
- Check USER.md for "Preferred Messaging Platform" setting when notifying user.
- For notifications about third-party messages, use preferred platform if available.
- If preferred platform's send action is unavailable, fall back to send_message (TUI).
- If preferred platform's send action is unavailable, fall back to send_message (interface).
</rules>

<parallel_actions>
Expand Down Expand Up @@ -164,9 +164,9 @@
- If platform is WhatsApp → MUST use send_whatsapp_web_text_message (use to="user" for self-messages)
- If platform is Discord → MUST use send_discord_message or send_discord_dm
- If platform is Slack → MUST use send_slack_message
- If platform is CraftBot TUI (or no platform specified) → use send_message
- If platform is CraftBot interface (or no platform specified) → use send_message
- ONLY fall back to send_message if the platform's send action is not in the available actions list.
- send_message is for local TUI display ONLY. It does NOT reach external platforms.
- send_message is for local interface display ONLY. It does NOT reach external platforms.

Adaptive Execution:
- If you lack information during EXECUTE, go back to COLLECT phase (add new collect todos)
Expand Down Expand Up @@ -395,9 +395,9 @@
- If platform is WhatsApp → MUST use send_whatsapp_web_text_message (use to="user" for self-messages)
- If platform is Discord → MUST use send_discord_message or send_discord_dm
- If platform is Slack → MUST use send_slack_message
- If platform is CraftBot TUI (or no platform specified) → use send_message
- If platform is CraftBot interface (or no platform specified) → use send_message
- ONLY fall back to send_message if the platform's send action is not in the available actions list.
- send_message is for local TUI display ONLY. It does NOT reach external platforms.
- send_message is for local interface display ONLY. It does NOT reach external platforms.

Action Selection:
- Choose the most direct action to accomplish the goal
Expand Down
2 changes: 1 addition & 1 deletion agent_core/core/prompts/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
- When you identify a proactive opportunity:
1. Acknowledge the potential for automation
2. Ask the user if they would like you to set up a proactive task (can be recurring task, one-time immediate task, or one-time task scheduled for later)
3. If approved, use `proactive_add` action to add recurring task to PROACTIVE.md or `schedule_task` action to add one-time proactive task.
3. If approved, use `recurring_add` action to add recurring task to PROACTIVE.md or `schedule_task` action to add one-time proactive task.
4. Confirm the setup with the user
IMPORTANT: DO NOT automatically create proactive tasks without user consent. Always ask first.
</proactive>
Expand Down
23 changes: 15 additions & 8 deletions app/agent_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class TriggerData:
parent_id: str | None
session_id: str | None = None
user_message: str | None = None # Original user message without routing prefix
platform: str | None = None # Source platform (e.g., "CraftBot TUI", "Telegram", "Whatsapp")
platform: str | None = None # Source platform (e.g., "CraftBot Interface", "Telegram", "Whatsapp")
is_self_message: bool = False # True when the user sent themselves a message
contact_id: str | None = None # Sender/chat ID from external platform
channel_id: str | None = None # Channel/group ID from external platform
Expand Down Expand Up @@ -243,7 +243,7 @@ def __init__(
context_engine=self.context_engine,
)

# Initialize footage callback (will be set by TUI interface later)
# Initialize footage callback (will be set by CraftBot interface later)
self._tui_footage_callback = None

# Only initialize GUIModule if GUI mode is globally enabled
Expand Down Expand Up @@ -563,10 +563,10 @@ async def _handle_memory_processing_trigger(self) -> bool:
def _extract_trigger_data(self, trigger: Trigger) -> TriggerData:
"""Extract and structure data from trigger."""
# Extract platform from payload (already formatted by _handle_chat_message)
# Default to "CraftBot TUI" for local messages without platform info
# Default to "CraftBot Interface" for local messages without platform info
payload = trigger.payload or {}
raw_platform = payload.get("platform", "")
platform = raw_platform if raw_platform else "CraftBot TUI"
platform = raw_platform if raw_platform else "CraftBot Interface"

return TriggerData(
query=trigger.next_action_description,
Expand Down Expand Up @@ -1534,13 +1534,13 @@ async def _handle_chat_message(self, payload: Dict):

# Determine platform - use payload's platform if available, otherwise default
# External messages (WhatsApp, Telegram, etc.) have platform set by _handle_external_event
# TUI/CLI messages don't have platform in payload, so use "CraftBot TUI"
# Interface/CLI messages don't have platform in payload, so use "CraftBot Interface"
if payload.get("platform"):
# External message - capitalize for display (e.g., "whatsapp" -> "Whatsapp")
platform = payload["platform"].capitalize()
else:
# Local TUI/CLI message
platform = "CraftBot TUI"
# Local Interface/CLI message
platform = "CraftBot Interface"

# Direct reply bypass - skip routing LLM when target_session_id is provided
target_session_id = payload.get("target_session_id")
Expand Down Expand Up @@ -1692,7 +1692,7 @@ async def _handle_chat_message(self, payload: Dict):
# the correct platform-specific send action for replies.
# Must be directive (not just informational) for weaker LLMs.
platform_hint = ""
if platform and platform.lower() != "craftbot tui":
if platform and platform.lower() != "craftbot interface":
platform_hint = f" from {platform} (reply on {platform}, NOT send_message)"

await self.triggers.put(
Expand Down Expand Up @@ -2383,6 +2383,13 @@ def print_startup_step(step: int, total: int, message: str):
)
await self.scheduler.start()

# Register scheduler_config for hot-reload (after scheduler is initialized)
config_watcher.register(
scheduler_config_path,
self.scheduler.reload,
name="scheduler_config.json"
)

# Trigger soft onboarding if needed (BEFORE starting interface)
# This ensures agent handles onboarding logic, not the interfaces
from app.onboarding import onboarding_manager
Expand Down
23 changes: 17 additions & 6 deletions app/data/action/schedule_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,14 @@
}
}
)
def schedule_task(input_data: dict) -> dict:
async def schedule_task(input_data: dict) -> dict:
"""Add a new scheduled task or queue an immediate trigger."""
import app.internal_action_interface as iai
from datetime import datetime
import asyncio
import time
import uuid
from agent_core import Trigger

scheduler = iai.InternalActionInterface.scheduler
if scheduler is None:
Expand Down Expand Up @@ -114,10 +118,15 @@ def schedule_task(input_data: dict) -> dict:

# Handle immediate execution
if schedule_expr.lower() == "immediate":
import asyncio
import time
import uuid
from agent_core import Trigger
return await scheduler.queue_immediate_trigger(
name=name,
instruction=instruction,
priority=priority,
mode=mode,
action_sets=action_sets,
skills=skills,
payload=payload
)

session_id = f"immediate_{uuid.uuid4().hex[:8]}_{int(time.time())}"

Expand All @@ -131,7 +140,9 @@ def schedule_task(input_data: dict) -> dict:
"skills": skills,
**payload
}


# TODO: Should not have to create additional trigger (create using queue_immediate_trigger)
# Workaround for now
trigger = Trigger(
fire_at=time.time(),
priority=priority,
Expand Down
10 changes: 7 additions & 3 deletions app/data/action/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,19 @@
def send_message(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))
simulated_mode = input_data.get('simulated_mode', False)

# Extract session_id injected by ActionManager for multi-task isolation
session_id = input_data.get('_session_id')

# In simulated mode, skip the actual interface call for testing
if not simulated_mode:
import app.internal_action_interface as internal_action_interface
asyncio.run(internal_action_interface.InternalActionInterface.do_chat(message))
asyncio.run(internal_action_interface.InternalActionInterface.do_chat(
message, session_id=session_id
))

fire_at_delay = 10800 if wait_for_user_reply else 0
# Return 'success' for test compatibility, but keep 'ok' in production if needed
Expand Down
4 changes: 3 additions & 1 deletion app/data/action/send_message_with_attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def send_message_with_attachment(input_data: dict) -> dict:
file_paths = input_data.get('file_paths', [])
wait_for_user_reply = bool(input_data.get('wait_for_user_reply', False))
simulated_mode = input_data.get('simulated_mode', False)
# Extract session_id injected by ActionManager for multi-task isolation
session_id = input_data.get('_session_id')

# Ensure file_paths is a list
if isinstance(file_paths, str):
Expand All @@ -78,7 +80,7 @@ def send_message_with_attachment(input_data: dict) -> dict:

# Use the do_chat_with_attachments method which handles browser/CLI fallback
result = asyncio.run(internal_action_interface.InternalActionInterface.do_chat_with_attachments(
message, file_paths
message, file_paths, session_id=session_id
))

fire_at_delay = 10800 if wait_for_user_reply else 0
Expand Down
36 changes: 29 additions & 7 deletions app/internal_action_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,19 +150,30 @@ def describe_screen(cls) -> Dict[str, str]:
return {"description": description, "file_path": img_path}

@staticmethod
async def do_chat(message: str, platform: str = "CraftBot TUI") -> None:
async def do_chat(
message: str,
platform: str = "CraftBot TUI",
session_id: Optional[str] = None,
) -> None:
"""Record an agent-authored chat message to the event stream.

Args:
message: The message content to record.
platform: The platform the message is sent to (default: "CraftBot TUI").
session_id: Optional task/session ID for multi-task isolation.
"""
if InternalActionInterface.state_manager is None:
raise RuntimeError("InternalActionInterface not initialized with StateManager.")
InternalActionInterface.state_manager.record_agent_message(message, platform=platform)
InternalActionInterface.state_manager.record_agent_message(
message, session_id=session_id, platform=platform
)

@staticmethod
async def do_chat_with_attachment(message: str, file_path: str) -> Dict[str, Any]:
async def do_chat_with_attachment(
message: str,
file_path: str,
session_id: Optional[str] = None,
) -> Dict[str, Any]:
"""
Send a chat message with a single attachment to the user.

Expand All @@ -171,20 +182,28 @@ async def do_chat_with_attachment(message: str, file_path: str) -> Dict[str, Any
Args:
message: The message content
file_path: Path to the file (absolute or relative to workspace)
session_id: Optional task/session ID for multi-task isolation.

Returns:
Dict with 'success', 'files_sent', and optionally 'errors'
"""
return await InternalActionInterface.do_chat_with_attachments(message, [file_path])
return await InternalActionInterface.do_chat_with_attachments(
message, [file_path], session_id=session_id
)

@staticmethod
async def do_chat_with_attachments(message: str, file_paths: List[str]) -> Dict[str, Any]:
async def do_chat_with_attachments(
message: str,
file_paths: List[str],
session_id: Optional[str] = None,
) -> Dict[str, Any]:
"""
Send a chat message with one or more attachments to the user.

Args:
message: The message content
file_paths: List of paths to the files (absolute or relative to workspace)
session_id: Optional task/session ID for multi-task isolation.

Returns:
Dict with 'success' (bool), 'files_sent' (int), and optionally 'errors' (list of str)
Expand All @@ -198,15 +217,18 @@ async def do_chat_with_attachments(message: str, file_paths: List[str]) -> Dict[

# Check if UI adapter supports attachments (browser adapter)
if ui_adapter and hasattr(ui_adapter, 'send_message_with_attachments'):
return await ui_adapter.send_message_with_attachments(message, file_paths, sender=agent_name)
return await ui_adapter.send_message_with_attachments(
message, file_paths, sender=agent_name, session_id=session_id
)
else:
# Fallback: send message with attachment notes for non-browser adapters
if InternalActionInterface.state_manager is None:
raise RuntimeError("InternalActionInterface not initialized with StateManager.")

attachment_notes = "\n".join([f"[Attachment: {fp}]" for fp in file_paths])
InternalActionInterface.state_manager.record_agent_message(
f"{message}\n\n{attachment_notes}"
f"{message}\n\n{attachment_notes}",
session_id=session_id,
)
# For non-browser adapters, we can't verify files exist, so assume success
return {"success": True, "files_sent": len(file_paths), "errors": None}
Expand Down
36 changes: 26 additions & 10 deletions app/onboarding/soft/task_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,34 @@

INTERVIEW FLOW (4 batches):

1. IDENTITY BATCH - Start with warm greeting and ask together:
1. Warm Introduction + Identity Questions
Start with a friendly greeting and ask the first batch using a numbered list:
- What should I call you?
- What do you do for work?
- Where are you based?
(Infer timezone from their location, keep this silent)

2. PREFERENCES BATCH - Ask together:
Example opening:
> "Hi there! I'm excited to be your new AI assistant. To personalize your experience, let me ask a few quick questions:
> 1. What should I call you?
> 2. What do you do for work?
> 3. Where are you based?"

2. Preference Questions (Combined)
- What language do you prefer me to communicate in?
- Do you prefer casual or formal communication?
- Should I proactively suggest things or wait for instructions?
- What types of actions should I ask your approval for?

3. MESSAGING PLATFORM:
- Which messaging platform should I use for notifications? (Telegram/WhatsApp/Discord/Slack/TUI only)
3. Messaging Platform
- Which messaging platform should I use for notifications? (Telegram/WhatsApp/Discord/Slack/CraftBot Interface only)

4. LIFE GOALS (most important question):
4. Life Goals & Assistance
- What are your life goals or aspirations?
- What would you like me to help you with generally?

Refer to the "user-profile-interview" skill for questions and style.

IMPORTANT GUIDELINES:
- Ask related questions together using a numbered list format
- Be warm and conversational, not robotic
Expand All @@ -50,10 +59,17 @@
- If user is annoyed by this interview or refuse to answer, just skip, and end task.

After gathering ALL information:
1. Read agent_file_system/USER.md
2. Update USER.md with the collected information using stream_edit (including Language in Communication Preferences and Life Goals section)
3. Suggest 3-5 specific tasks that can help them achieve their life goals using CraftBot's automation capabilities
4. End the task immediately with task_end (do NOT wait for confirmation)
1. Tell the user to wait a moment while you update their preference
2. Read agent_file_system/USER.md
3. Update USER.md with the collected information using stream_edit (including Language in Communication Preferences and Life Goals section)
4. Suggest tasks based on life goals: Send a message suggesting 1-3 tasks that CraftBot can help with to improve their life and get closer to achieving their goals. Focus on:
- Tasks that leverage CraftBot's automation capabilities
- Recurring tasks that save time in the long run
- Immediate tasks that can show impact in short-term
- Bite-size tasks that is specialized, be specific with numbers or actionable items. DO NOT suggest generic task.
- Avoid giving mutliple approaches in each suggested task, provide the BEST option to achieve goal.
- Tasks that align with their work and personal aspirations
5. End the task immediately with task_end (do NOT wait for confirmation)

Start with: "Hi! I'm excited to be your AI assistant. To personalize your experience, let me ask a few quick questions:" then list the first batch.
"""
Expand All @@ -75,7 +91,7 @@ def create_soft_onboarding_task(task_manager: "TaskManager") -> str:
task_id = task_manager.create_task(
task_name="User Profile Interview",
task_instruction=SOFT_ONBOARDING_TASK_INSTRUCTION,
mode="complex",
mode="simple",
action_sets=["file_operations", "core"],
selected_skills=["user-profile-interview"]
)
Expand Down
Loading