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
9 changes: 9 additions & 0 deletions pantheon/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", message="urllib3.*doesn't match a supported version")

import asyncio
import os
import sys

Expand Down Expand Up @@ -121,6 +122,14 @@ def main():

check_and_run_setup()

# Ensure an event loop exists for Fire + async functions (Python 3.10+)
# Python Fire internally calls asyncio.get_event_loop() when handling async functions,
# which raises RuntimeError in Python 3.12+ if no loop exists.
try:
asyncio.get_event_loop()
except RuntimeError:
asyncio.set_event_loop(asyncio.new_event_loop())

# Import REAL functions — Fire reads their signatures for --help
from pantheon.repl.__main__ import start as cli
from pantheon.chatroom.start import start_services as ui
Expand Down
102 changes: 86 additions & 16 deletions pantheon/chatroom/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,14 +656,34 @@ async def proxy_toolset(
f"chatroom proxy_toolset: method_name={method_name}, toolset_name={toolset_name}, args={args}"
)

# Get session_id from args, fallback to _current_chat_id
session_id = (args or {}).get("session_id") or getattr(self, '_current_chat_id', None)

# Inject workdir from project metadata if session_id is available
session_id = (args or {}).get("session_id")
if session_id:
try:
# Read-only: reading project metadata, no need to fix
memory = await run_func(self.memory_manager.get_memory, session_id)
project = memory.extra_data.get("project", {})
workspace_path = project.get("workspace_path") if isinstance(project, dict) else None

# Workspace backfill for historical sessions
if not workspace_path:
settings = get_settings()
session_workspace_dir = settings.pantheon_dir / "workspaces" / session_id
try:
session_workspace_dir.mkdir(parents=True, exist_ok=True)
workspace_path = str(session_workspace_dir)
logger.info(f"Created workspace for historical session {session_id}: {workspace_path}")

# Persist to memory
if not isinstance(project, dict):
project = {}
project["workspace_path"] = workspace_path
memory.extra_data["project"] = project
memory.mark_dirty()
except Exception as e:
logger.warning(f"Failed to create workspace for session {session_id}: {e}")

if workspace_path:
from pantheon.toolset import get_current_context_variables
ctx = get_current_context_variables()
Expand Down Expand Up @@ -798,7 +818,7 @@ async def get_active_agent(self, chat_name: str) -> dict:

@tool
async def create_chat(
self,
self,
chat_name: str | None = None,
project_name: str | None = None,
workspace_path: str | None = None,
Expand All @@ -810,31 +830,44 @@ async def create_chat(
project_name: Optional project name for grouping.
workspace_path: Optional workspace directory path.
"""
# Ensure workspace directory exists if provided
if workspace_path:
memory = await run_func(self.memory_manager.new_memory, chat_name)
memory.extra_data["last_activity_date"] = datetime.now().isoformat()

# Auto-create per-session workspace directory if not provided
if not workspace_path:
settings = get_settings()
session_workspace_dir = settings.pantheon_dir / "workspaces" / memory.id
try:
session_workspace_dir.mkdir(parents=True, exist_ok=True)
workspace_path = str(session_workspace_dir)
logger.info(f"Created session workspace directory: {workspace_path}")
except Exception as e:
logger.warning(f"Failed to create session workspace directory: {e}")
# Continue without workspace_path - will fallback to default
else:
# Ensure custom workspace directory exists
import os
try:
os.makedirs(workspace_path, exist_ok=True)
logger.info(f"Ensured workspace directory exists: {workspace_path}")
except Exception as e:
logger.warning(f"Failed to create workspace directory {workspace_path}: {e}")
# Continue anyway - the directory might be created later or error will surface when used

memory = await run_func(self.memory_manager.new_memory, chat_name)
memory.extra_data["last_activity_date"] = datetime.now().isoformat()

# Set project metadata if provided

# Set project metadata
project = {}
if project_name:
project = {"name": project_name}
if workspace_path:
project["workspace_path"] = workspace_path
project["name"] = project_name
if workspace_path:
project["workspace_path"] = workspace_path
if project:
memory.extra_data["project"] = project

return {
"success": True,
"message": "Chat created successfully",
"chat_name": memory.name,
"chat_id": memory.id,
"workspace_path": workspace_path,
}

@tool
Expand All @@ -844,9 +877,42 @@ async def delete_chat(self, chat_id: str):
Args:
chat_id: The ID of the chat.
"""
import shutil

try:
# Get workspace_path before deleting memory
workspace_path_to_delete = None
try:
memory = await run_func(self.memory_manager.get_memory, chat_id)
project = memory.extra_data.get("project", {})
workspace_path = project.get("workspace_path") if isinstance(project, dict) else None

# Only delete workspace if it's under .pantheon/workspaces/ (auto-created session workspace)
if workspace_path:
settings = get_settings()
workspaces_dir = settings.pantheon_dir / "workspaces"
workspace_path_obj = Path(workspace_path)
# Check if workspace_path is under .pantheon/workspaces/
try:
workspace_path_obj.relative_to(workspaces_dir)
workspace_path_to_delete = workspace_path_obj
except ValueError:
# Not under .pantheon/workspaces/, don't delete (user-specified path)
pass
except Exception as e:
logger.debug(f"Could not get workspace path for chat {chat_id}: {e}")

# Delete the memory
await run_func(self.memory_manager.delete_memory, chat_id)
# File is deleted immediately by delete_memory, no need for save()

# Delete workspace folder if it's an auto-created session workspace
if workspace_path_to_delete and workspace_path_to_delete.exists():
try:
shutil.rmtree(workspace_path_to_delete)
logger.info(f"Deleted session workspace: {workspace_path_to_delete}")
except Exception as e:
logger.warning(f"Failed to delete workspace folder {workspace_path_to_delete}: {e}")

return {"success": True, "message": "Chat deleted successfully"}
except Exception as e:
logger.error(f"Error deleting chat: {e}")
Expand Down Expand Up @@ -921,6 +987,10 @@ async def get_chat_messages(self, chat_id: str, filter_out_images: bool = False)
# Frontend query: skip auto-fix for better performance (5-10x faster)
# Messages will be fixed automatically when agent execution starts
memory = await run_func(self.memory_manager.get_memory, chat_id)

# Sync _current_chat_id to keep backend state aligned with UI
self._current_chat_id = chat_id

# Get full raw history for UI
messages = await run_func(memory.get_messages, _ALL_CONTEXTS, False)

Expand Down
2 changes: 2 additions & 0 deletions pantheon/toolsets/file/file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ async def move_file(self, old_path: str, new_path: str):
if not old_path.exists():
return {"success": False, "error": "Old path does not exist"}
new_path = self._resolve_path(new_path)
# Ensure parent directory exists before moving
new_path.parent.mkdir(parents=True, exist_ok=True)
shutil.move(old_path, new_path)
return {"success": True}

Expand Down
9 changes: 6 additions & 3 deletions pantheon/toolsets/file_transfer/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ async def open_file_for_write(self, file_path: str):
if ".." in file_path:
return {"error": "File path cannot contain '..'"}

path = self.path / file_path
# Use session workdir if available, fallback to self.path
path = self._get_root() / file_path

# Ensure parent directory exists
try:
Expand Down Expand Up @@ -239,7 +240,8 @@ async def open_file_for_read(self, file_path: str):
if ".." in file_path:
return {"success": False, "error": "File path cannot contain '..'"}

path = self.path / file_path
# Use session workdir if available, fallback to self.path
path = self._get_root() / file_path
if not path.exists():
return {"success": False, "error": "File does not exist"}

Expand Down Expand Up @@ -321,7 +323,8 @@ async def read_file(
if ".." in file_path:
return {"success": False, "error": "File path cannot contain '..'"}

path = self.path / file_path
# Use session workdir if available, fallback to self.path
path = self._get_root() / file_path
if not path.exists():
return {"success": False, "error": "File does not exist"}

Expand Down
Loading