Merge branch 'main' into fix_install_logic

This commit is contained in:
Puzhen Zhang 2025-11-20 17:13:32 +08:00 committed by GitHub
commit 4eaf7a2e4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 485 additions and 159 deletions

View file

@ -23,14 +23,21 @@ def set_user_env_path(env_path: str | None = None):
Set user-specific environment path for current thread. Set user-specific environment path for current thread.
If env_path is None, uses default global environment. If env_path is None, uses default global environment.
""" """
traceroot_logger.info("Setting user environment path", extra={"env_path": env_path, "exists": env_path and os.path.exists(env_path) if env_path else None})
if env_path and os.path.exists(env_path): if env_path and os.path.exists(env_path):
_thread_local.env_path = env_path _thread_local.env_path = env_path
# Load user-specific environment variables # Load user-specific environment variables
load_dotenv(dotenv_path=env_path, override=True) load_dotenv(dotenv_path=env_path, override=True)
traceroot_logger.info("User-specific environment loaded", extra={"env_path": env_path})
else: else:
# Clear thread-local env_path to fall back to global # Clear thread-local env_path to fall back to global
if hasattr(_thread_local, 'env_path'): if hasattr(_thread_local, 'env_path'):
delattr(_thread_local, 'env_path') delattr(_thread_local, 'env_path')
traceroot_logger.info("Reset to default global environment")
if env_path and not os.path.exists(env_path):
traceroot_logger.warning("User environment path does not exist, falling back to global", extra={"env_path": env_path})
def get_current_env_path() -> str: def get_current_env_path() -> str:
@ -54,8 +61,8 @@ def env(key: str, default: Any) -> Any: ...
def env(key: str, default=None): def env(key: str, default=None):
""" """
Get environment variable. Get environment variable.
First checks thread-local user-specific environment, First checks thread-local user-specific environment,
then falls back to global environment. then falls back to global environment.
""" """
# If we have a user-specific environment path, try to reload it to get latest values # If we have a user-specific environment path, try to reload it to get latest values
@ -64,10 +71,14 @@ def env(key: str, default=None):
from dotenv import dotenv_values from dotenv import dotenv_values
user_env_values = dotenv_values(_thread_local.env_path) user_env_values = dotenv_values(_thread_local.env_path)
if key in user_env_values: if key in user_env_values:
return user_env_values[key] or default value = user_env_values[key] or default
traceroot_logger.debug("Environment variable retrieved from user-specific config", extra={"key": key, "env_path": _thread_local.env_path, "has_value": value is not None})
return value
# Fall back to global environment # Fall back to global environment
return os.getenv(key, default) value = os.getenv(key, default)
traceroot_logger.debug("Environment variable retrieved from global config", extra={"key": key, "has_value": value is not None, "using_default": value == default})
return value
def env_or_fail(key: str): def env_or_fail(key: str):

View file

@ -1,5 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
from utils import traceroot_wrapper as traceroot
logger = traceroot.get_logger("health_controller")
router = APIRouter(tags=["Health"]) router = APIRouter(tags=["Health"])
@ -12,5 +15,8 @@ class HealthResponse(BaseModel):
@router.get("/health", name="health check", response_model=HealthResponse) @router.get("/health", name="health check", response_model=HealthResponse)
async def health_check(): async def health_check():
"""Health check endpoint for verifying backend is ready to accept requests.""" """Health check endpoint for verifying backend is ready to accept requests."""
return HealthResponse(status="ok", service="eigent") logger.debug("Health check requested")
response = HealthResponse(status="ok", service="eigent")
logger.debug("Health check completed", extra={"status": response.status, "service": response.service})
return response

View file

@ -362,6 +362,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
# Update the sync_step with new task_id # Update the sync_step with new task_id
if hasattr(item, 'new_task_id') and item.new_task_id: if hasattr(item, 'new_task_id') and item.new_task_id:
set_current_task_id(options.project_id, item.new_task_id) set_current_task_id(options.project_id, item.new_task_id)
# Reset summary generation flag for new tasks to ensure proper summaries
task_lock.summary_generated = False
logger.info("Reset summary_generated flag for new task", extra={"project_id": options.project_id, "new_task_id": item.new_task_id})
yield sse_json("confirmed", {"question": question}) yield sse_json("confirmed", {"question": question})
@ -385,32 +388,25 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
context_for_coordinator context_for_coordinator
) )
if not task_lock.summary_generated: summary_task_agent = task_summary_agent(options)
summary_task_agent = task_summary_agent(options) try:
try: summary_task_content = await asyncio.wait_for(
summary_task_content = await asyncio.wait_for( summary_task(summary_task_agent, camel_task), timeout=10
summary_task(summary_task_agent, camel_task), timeout=10 )
) task_lock.summary_generated = True
task_lock.summary_generated = True logger.info("Generated summary for task", extra={"project_id": options.project_id})
logger.info("Generated summary for first task", extra={"project_id": options.project_id}) except asyncio.TimeoutError:
except asyncio.TimeoutError: logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id})
logger.warning("summary_task timeout", extra={"project_id": options.project_id, "task_id": options.task_id}) # Fallback to a minimal summary to unblock UI
# Fallback to a minimal summary to unblock UI fallback_name = "Task"
fallback_name = "Task" content_preview = camel_task.content if hasattr(camel_task, "content") else ""
content_preview = camel_task.content if hasattr(camel_task, "content") else "" if content_preview is None:
if content_preview is None: content_preview = ""
content_preview = "" fallback_summary = (
fallback_summary = ( (content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview )
) summary_task_content = f"{fallback_name}|{fallback_summary}"
summary_task_content = f"{fallback_name}|{fallback_summary}" task_lock.summary_generated = True
task_lock.summary_generated = True
else:
if len(question) > 100:
summary_task_content = f"Task|{question[:97]}..."
else:
summary_task_content = f"Task|{question}"
logger.info("Skipped summary generation for subsequent task", extra={"project_id": options.project_id})
yield to_sub_tasks(camel_task, summary_task_content) yield to_sub_tasks(camel_task, summary_task_content)
# tracer.stop() # tracer.stop()
@ -514,8 +510,10 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
new_task_state = item.data.get('state', 'unknown') new_task_state = item.data.get('state', 'unknown')
new_task_result = item.data.get('result', '') new_task_result = item.data.get('result', '')
if camel_task is None:
assert camel_task is not None logger.error(f"NEW_TASK_STATE action received but camel_task is None for project {options.project_id}, task {new_task_id}")
yield sse_json("error", {"message": "Cannot process new task state: current task not initialized."})
continue
old_task_content: str = camel_task.content old_task_content: str = camel_task.content
old_task_result: str = await get_task_result_with_optional_summary(camel_task, options) old_task_result: str = await get_task_result_with_optional_summary(camel_task, options)
@ -588,11 +586,29 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
coordinator_context=context_for_multi_turn coordinator_context=context_for_multi_turn
) )
task_content_for_summary = new_task_content # Generate proper LLM summary for multi-turn tasks instead of hardcoded fallback
if len(task_content_for_summary) > 100: try:
new_summary_content = f"Follow-up Task|{task_content_for_summary[:97]}..." multi_turn_summary_agent = task_summary_agent(options)
else: new_summary_content = await asyncio.wait_for(
new_summary_content = f"Follow-up Task|{task_content_for_summary}" summary_task(multi_turn_summary_agent, camel_task), timeout=10
)
logger.info("Generated LLM summary for multi-turn task", extra={"project_id": options.project_id})
except asyncio.TimeoutError:
logger.warning("Multi-turn summary_task timeout", extra={"project_id": options.project_id, "task_id": task_id})
# Fallback to descriptive but not generic summary
task_content_for_summary = new_task_content
if len(task_content_for_summary) > 100:
new_summary_content = f"Follow-up Task|{task_content_for_summary[:97]}..."
else:
new_summary_content = f"Follow-up Task|{task_content_for_summary}"
except Exception as e:
logger.error(f"Error generating multi-turn task summary: {e}")
# Fallback to descriptive but not generic summary
task_content_for_summary = new_task_content
if len(task_content_for_summary) > 100:
new_summary_content = f"Follow-up Task|{task_content_for_summary[:97]}..."
else:
new_summary_content = f"Follow-up Task|{task_content_for_summary}"
# Send the extracted events # Send the extracted events
yield to_sub_tasks(camel_task, new_summary_content) yield to_sub_tasks(camel_task, new_summary_content)
@ -666,16 +682,32 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
) )
workforce.resume() workforce.resume()
elif item.action == Action.end: elif item.action == Action.end:
assert camel_task is not None logger.info(f"Processing END action for project {options.project_id}, task {options.task_id}, camel_task exists: {camel_task is not None}, current status: {task_lock.status}")
# Prevent duplicate end processing
if task_lock.status == Status.done:
logger.warning(f"END action received but task already marked as done for project {options.project_id}, task {options.task_id}. Ignoring duplicate END action.")
continue
if camel_task is None:
logger.warning(f"END action received but camel_task is None for project {options.project_id}, task {options.task_id}. This may indicate multiple END actions or improper task lifecycle management.")
# Use the item data as the final result if camel_task is None
final_result: str = str(item.data) if item.data else "Task completed"
else:
final_result: str = await get_task_result_with_optional_summary(camel_task, options)
task_lock.status = Status.done task_lock.status = Status.done
final_result: str = await get_task_result_with_optional_summary(camel_task, options)
task_lock.last_task_result = final_result task_lock.last_task_result = final_result
task_content: str = camel_task.content # Handle task content - use fallback if camel_task is None
if "=== CURRENT TASK ===" in task_content: if camel_task is not None:
task_content = task_content.split("=== CURRENT TASK ===")[-1].strip() task_content: str = camel_task.content
if "=== CURRENT TASK ===" in task_content:
task_content = task_content.split("=== CURRENT TASK ===")[-1].strip()
else:
task_content: str = f"Task {options.task_id}"
task_lock.add_conversation('task_result', { task_lock.add_conversation('task_result', {
'task_content': task_content, 'task_content': task_content,
'task_result': final_result, 'task_result': final_result,
@ -702,8 +734,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
# Check if this might be a misrouted second question # Check if this might be a misrouted second question
if camel_task is None: if camel_task is None:
logger.warning(f"SUPPLEMENT action received but camel_task is None for project {options.project_id}") logger.warning(f"SUPPLEMENT action received but camel_task is None for project {options.project_id}")
yield sse_json("error", {"message": "Cannot supplement task: task not initialized. Please start a task first."})
continue
else: else:
assert camel_task is not None
task_lock.status = Status.processing task_lock.status = Status.processing
camel_task.add_subtask( camel_task.add_subtask(
Task( Task(

View file

@ -283,30 +283,39 @@ class TaskLock:
self.question_agent = None self.question_agent = None
self.current_task_id = None self.current_task_id = None
logger.info("Task lock initialized", extra={"task_id": id, "created_at": self.created_at.isoformat()})
async def put_queue(self, data: ActionData): async def put_queue(self, data: ActionData):
self.last_accessed = datetime.now() self.last_accessed = datetime.now()
logger.debug("Adding item to task queue", extra={"task_id": self.id, "action": data.action})
await self.queue.put(data) await self.queue.put(data)
async def get_queue(self): async def get_queue(self):
self.last_accessed = datetime.now() self.last_accessed = datetime.now()
logger.debug("Getting item from task queue", extra={"task_id": self.id})
return await self.queue.get() return await self.queue.get()
async def put_human_input(self, agent: str, data: Any = None): async def put_human_input(self, agent: str, data: Any = None):
logger.debug("Adding human input", extra={"task_id": self.id, "agent": agent, "has_data": data is not None})
await self.human_input[agent].put(data) await self.human_input[agent].put(data)
async def get_human_input(self, agent: str): async def get_human_input(self, agent: str):
logger.debug("Getting human input", extra={"task_id": self.id, "agent": agent})
return await self.human_input[agent].get() return await self.human_input[agent].get()
def add_human_input_listen(self, agent: str): def add_human_input_listen(self, agent: str):
logger.debug("Adding human input listener", extra={"task_id": self.id, "agent": agent})
self.human_input[agent] = asyncio.Queue(1) self.human_input[agent] = asyncio.Queue(1)
def add_background_task(self, task: asyncio.Task) -> None: def add_background_task(self, task: asyncio.Task) -> None:
r"""Add a task to track and clean up weak references""" r"""Add a task to track and clean up weak references"""
logger.debug("Adding background task", extra={"task_id": self.id, "background_tasks_count": len(self.background_tasks)})
self.background_tasks.add(task) self.background_tasks.add(task)
task.add_done_callback(lambda t: self.background_tasks.discard(t)) task.add_done_callback(lambda t: self.background_tasks.discard(t))
async def cleanup(self): async def cleanup(self):
r"""Cancel all background tasks and clean up resources""" r"""Cancel all background tasks and clean up resources"""
logger.info("Starting task lock cleanup", extra={"task_id": self.id, "background_tasks_count": len(self.background_tasks)})
for task in list(self.background_tasks): for task in list(self.background_tasks):
if not task.done(): if not task.done():
task.cancel() task.cancel()
@ -315,9 +324,11 @@ class TaskLock:
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
self.background_tasks.clear() self.background_tasks.clear()
logger.info("Task lock cleanup completed", extra={"task_id": self.id})
def add_conversation(self, role: str, content: str | dict): def add_conversation(self, role: str, content: str | dict):
"""Add a conversation entry to history""" """Add a conversation entry to history"""
logger.debug("Adding conversation entry", extra={"task_id": self.id, "role": role, "content_length": len(str(content))})
self.conversation_history.append({ self.conversation_history.append({
'role': role, 'role': role,
'content': content, 'content': content,
@ -344,7 +355,9 @@ task_index: dict[str, weakref.ref[Task]] = {}
def get_task_lock(id: str) -> TaskLock: def get_task_lock(id: str) -> TaskLock:
if id not in task_locks: if id not in task_locks:
logger.error("Task lock not found", extra={"task_id": id})
raise ProgramException("Task not found") raise ProgramException("Task not found")
logger.debug("Task lock retrieved", extra={"task_id": id})
return task_locks[id] return task_locks[id]
@ -357,12 +370,15 @@ def set_current_task_id(project_id: str, task_id: str) -> None:
"""Set the current task ID for a project's task lock""" """Set the current task ID for a project's task lock"""
task_lock = get_task_lock(project_id) task_lock = get_task_lock(project_id)
task_lock.current_task_id = task_id task_lock.current_task_id = task_id
logger.info(f"Updated current_task_id to {task_id} for project {project_id}") logger.info("Updated current task ID", extra={"project_id": project_id, "task_id": task_id})
def create_task_lock(id: str) -> TaskLock: def create_task_lock(id: str) -> TaskLock:
if id in task_locks: if id in task_locks:
logger.warning("Attempting to create task lock that already exists", extra={"task_id": id})
raise ProgramException("Task already exists") raise ProgramException("Task already exists")
logger.info("Creating new task lock", extra={"task_id": id})
task_locks[id] = TaskLock(id=id, queue=asyncio.Queue(), human_input={}) task_locks[id] = TaskLock(id=id, queue=asyncio.Queue(), human_input={})
# Start cleanup task if not running # Start cleanup task if not running
@ -370,26 +386,31 @@ def create_task_lock(id: str) -> TaskLock:
# if _cleanup_task is None or _cleanup_task.done(): # if _cleanup_task is None or _cleanup_task.done():
# _cleanup_task = asyncio.create_task(_periodic_cleanup()) # _cleanup_task = asyncio.create_task(_periodic_cleanup())
logger.info("Task lock created successfully", extra={"task_id": id, "total_task_locks": len(task_locks)})
return task_locks[id] return task_locks[id]
def get_or_create_task_lock(id: str) -> TaskLock: def get_or_create_task_lock(id: str) -> TaskLock:
"""Get existing task lock or create a new one if it doesn't exist""" """Get existing task lock or create a new one if it doesn't exist"""
if id in task_locks: if id in task_locks:
logger.debug("Using existing task lock", extra={"task_id": id})
return task_locks[id] return task_locks[id]
logger.info("Task lock not found, creating new one", extra={"task_id": id})
return create_task_lock(id) return create_task_lock(id)
async def delete_task_lock(id: str): async def delete_task_lock(id: str):
if id not in task_locks: if id not in task_locks:
logger.warning("Attempting to delete non-existent task lock", extra={"task_id": id})
raise ProgramException("Task not found") raise ProgramException("Task not found")
# Clean up background tasks before deletion # Clean up background tasks before deletion
task_lock = task_locks[id] task_lock = task_locks[id]
logger.info("Cleaning up task lock", extra={"task_id": id, "background_tasks": len(task_lock.background_tasks)})
await task_lock.cleanup() await task_lock.cleanup()
del task_locks[id] del task_locks[id]
logger.debug(f"Deleted task lock {id}, remaining locks: {len(task_locks)}") logger.info("Task lock deleted successfully", extra={"task_id": id, "remaining_task_locks": len(task_locks)})
def get_camel_task(id: str, tasks: list[Task]) -> None | Task: def get_camel_task(id: str, tasks: list[Task]) -> None | Task:

View file

@ -26,6 +26,13 @@ class SingleAgentWorker(BaseSingleAgentWorker):
context_utility: ContextUtility | None = None, context_utility: ContextUtility | None = None,
enable_workflow_memory: bool = False, enable_workflow_memory: bool = False,
) -> None: ) -> None:
logger.info("Initializing SingleAgentWorker", extra={
"description": description,
"worker_agent_name": worker.agent_name,
"use_agent_pool": use_agent_pool,
"pool_max_size": pool_max_size,
"enable_workflow_memory": enable_workflow_memory
})
super().__init__( super().__init__(
description=description, description=description,
worker=worker, worker=worker,
@ -61,6 +68,12 @@ class SingleAgentWorker(BaseSingleAgentWorker):
worker_agent = await self._get_worker_agent() worker_agent = await self._get_worker_agent()
worker_agent.process_task_id = task.id # type: ignore rewrite line worker_agent.process_task_id = task.id # type: ignore rewrite line
logger.info("Starting task processing", extra={
"task_id": task.id,
"worker_agent_id": worker_agent.agent_id,
"dependencies_count": len(dependencies)
})
response_content = "" response_content = ""
final_response = None final_response = None
try: try:

View file

@ -11,6 +11,9 @@ from app.service.task import Action, ActionTerminalData, Agents, get_task_lock
from app.utils.listen.toolkit_listen import auto_listen_toolkit from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from app.service.task import process_task from app.service.task import process_task
from utils import traceroot_wrapper as traceroot
logger = traceroot.get_logger("terminal_toolkit")
@auto_listen_toolkit(BaseTerminalToolkit) @auto_listen_toolkit(BaseTerminalToolkit)
@ -37,11 +40,22 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
self.agent_name = agent_name self.agent_name = agent_name
if working_directory is None: if working_directory is None:
working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/")) working_directory = env("file_save_path", os.path.expanduser("~/.eigent/terminal/"))
logger.info("Initializing TerminalToolkit", extra={
"api_task_id": api_task_id,
"agent_name": self.agent_name,
"working_directory": working_directory,
"safe_mode": safe_mode,
"use_docker_backend": use_docker_backend
})
if TerminalToolkit._thread_pool is None: if TerminalToolkit._thread_pool is None:
TerminalToolkit._thread_pool = ThreadPoolExecutor( TerminalToolkit._thread_pool = ThreadPoolExecutor(
max_workers=1, max_workers=1,
thread_name_prefix="terminal_toolkit" thread_name_prefix="terminal_toolkit"
) )
logger.debug("Created terminal toolkit thread pool")
super().__init__( super().__init__(
timeout=timeout, timeout=timeout,
working_directory=working_directory, working_directory=working_directory,
@ -62,6 +76,11 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
""" """
# Convert ANSI escape sequences to plain text # Convert ANSI escape sequences to plain text
super()._write_to_log(log_file, content) super()._write_to_log(log_file, content)
logger.debug("Terminal output logged", extra={
"api_task_id": self.api_task_id,
"log_file": log_file,
"content_length": len(content)
})
self._update_terminal_output(_to_plain(content)) self._update_terminal_output(_to_plain(content))
def _update_terminal_output(self, output: str): def _update_terminal_output(self, output: str):

View file

@ -19,7 +19,6 @@ from app.service.task import (
Action, Action,
ActionAssignTaskData, ActionAssignTaskData,
ActionEndData, ActionEndData,
ActionNewTaskStateData,
ActionTaskStateData, ActionTaskStateData,
get_camel_task, get_camel_task,
get_task_lock, get_task_lock,
@ -45,6 +44,14 @@ class Workforce(BaseWorkforce):
use_structured_output_handler: bool = True, use_structured_output_handler: bool = True,
) -> None: ) -> None:
self.api_task_id = api_task_id self.api_task_id = api_task_id
logger.info("Initializing workforce", extra={
"api_task_id": api_task_id,
"description": description[:100] + "..." if len(description) > 100 else description,
"children_count": len(children) if children else 0,
"graceful_shutdown_timeout": graceful_shutdown_timeout,
"share_memory": share_memory,
"use_structured_output_handler": use_structured_output_handler
})
super().__init__( super().__init__(
description=description, description=description,
children=children, children=children,
@ -65,13 +72,20 @@ class Workforce(BaseWorkforce):
coordinator_context: Optional context ONLY for coordinator agent during decomposition. coordinator_context: Optional context ONLY for coordinator agent during decomposition.
This context will NOT be passed to subtasks or worker agents. This context will NOT be passed to subtasks or worker agents.
""" """
logger.info("Starting task decomposition", extra={
"api_task_id": self.api_task_id,
"task_id": task.id,
"task_content": task.content[:200] + "..." if len(task.content) > 200 else task.content,
"has_coordinator_context": bool(coordinator_context)
})
if not validate_task_content(task.content, task.id): if not validate_task_content(task.content, task.id):
task.state = TaskState.FAILED task.state = TaskState.FAILED
task.result = "Task failed: Invalid or empty content provided" task.result = "Task failed: Invalid or empty content provided"
logger.warning( logger.warning("Task rejected: Invalid or empty content", extra={
f"Task {task.id} rejected: Invalid or empty content. Content preview: '{task.content[:50]}...'" "task_id": task.id,
) "content_preview": task.content[:50] + "..." if len(task.content) > 50 else task.content
})
raise UserException(code.error, task.result) raise UserException(code.error, task.result)
self.reset() self.reset()
@ -81,11 +95,19 @@ class Workforce(BaseWorkforce):
task.state = TaskState.OPEN task.state = TaskState.OPEN
subtasks = asyncio.run(self.handle_decompose_append_task(task)) subtasks = asyncio.run(self.handle_decompose_append_task(task))
logger.info("Task decomposition completed", extra={
"api_task_id": self.api_task_id,
"task_id": task.id,
"subtasks_count": len(subtasks)
})
return subtasks return subtasks
async def eigent_start(self, subtasks: list[Task]): async def eigent_start(self, subtasks: list[Task]):
"""start the workforce""" """start the workforce"""
logger.debug(f"start the workforce {subtasks=}") logger.info("Starting workforce execution", extra={
"api_task_id": self.api_task_id,
"subtasks_count": len(subtasks)
})
self._pending_tasks.extendleft(reversed(subtasks)) self._pending_tasks.extendleft(reversed(subtasks))
# Save initial snapshot # Save initial snapshot
self.save_snapshot("Initial task decomposition") self.save_snapshot("Initial task decomposition")
@ -93,7 +115,10 @@ class Workforce(BaseWorkforce):
try: try:
await self.start() await self.start()
except Exception as e: except Exception as e:
logger.error(f"Error in workforce execution: {e}") logger.error("Error in workforce execution", extra={
"api_task_id": self.api_task_id,
"error": str(e)
}, exc_info=True)
self._state = WorkforceState.STOPPED self._state = WorkforceState.STOPPED
raise raise
finally: finally:
@ -294,18 +319,11 @@ class Workforce(BaseWorkforce):
"failure_count": task.failure_count, "failure_count": task.failure_count,
} }
if self._task_is_new(task_data): await task_lock.put_queue(
await task_lock.put_queue( ActionTaskStateData(
ActionNewTaskStateData( data=task_data
data=task_data
)
)
else:
await task_lock.put_queue(
ActionTaskStateData(
data=task_data
)
) )
)
return await super()._handle_completed_task(task) return await super()._handle_completed_task(task)
@ -339,36 +357,6 @@ class Workforce(BaseWorkforce):
return result return result
def _task_is_new(self, item:dict) -> bool:
# Validate the task state data object first
assert isinstance(item, dict)
task_id = item.get("task_id", "")
state = item.get("state", "")
result = item.get("result", "")
failure_count = item.get("failure_count", 0)
# Validate required fields
if not task_id:
logger.error("Missing task_id in task_state data")
return False
elif not state:
logger.error(f"Missing state in task_state data for task {task_id}")
return False
# Ensure failure_count is an integer
try:
failure_count = int(failure_count)
except (ValueError, TypeError):
logger.error(f"Invalid failure_count in task_state data for task {task_id}: {failure_count}")
failure_count = 0 # Default to 0 if invalid
should_send_new_task_state = (
state == "FAILED" or
(failure_count == 0 and result.strip() == "")
)
return should_send_new_task_state
def stop(self) -> None: def stop(self) -> None:
super().stop() super().stop()
task_lock = get_task_lock(self.api_task_id) task_lock = get_task_lock(self.api_task_id)

View file

@ -11,9 +11,9 @@ exports.default = async function notarizing(context) {
// Validate required environment variables // Validate required environment variables
if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) { if (!process.env.APPLE_ID || !process.env.APPLE_APP_SPECIFIC_PASSWORD || !process.env.APPLE_TEAM_ID) {
console.error("Missing required environment variables for notarization"); console.warn("Missing Apple environment variables for notarization");
console.error("Required: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID"); console.warn("Skipping notarization. Required: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID");
throw new Error("Notarization failed: Missing required environment variables"); return;
} }
return notarize({ return notarize({

View file

@ -76,6 +76,7 @@ export async function downloadWithRedirects(url, destinationPath) {
// Check if file exists and has size > 0 // Check if file exists and has size > 0
try { try {
if (fs.existsSync(destinationPath)) { if (fs.existsSync(destinationPath)) {
const stats = fs.statSync(destinationPath) const stats = fs.statSync(destinationPath)
if (stats.size === 0) { if (stats.size === 0) {
@ -89,6 +90,7 @@ export async function downloadWithRedirects(url, destinationPath) {
} }
} catch (err) { } catch (err) {
safeReject(new Error(`Failed to verify download: ${err.message}`)) safeReject(new Error(`Failed to verify download: ${err.message}`))
} }
}) })
}) })

View file

@ -1,6 +1,14 @@
from sqlmodel import Session, create_engine from sqlmodel import Session, create_engine
from app.component.environment import env, env_or_fail from app.component.environment import env, env_or_fail
from utils import traceroot_wrapper as traceroot
logger = traceroot.get_logger("database")
logger.info("Initializing database engine", extra={
"database_url_prefix": env_or_fail("database_url")[:20] + "...",
"debug_mode": env("debug") == "on",
"pool_size": 36
})
engine = create_engine( engine = create_engine(
env_or_fail("database_url"), env_or_fail("database_url"),
@ -8,11 +16,19 @@ engine = create_engine(
pool_size=36, pool_size=36,
) )
logger.info("Database engine initialized successfully")
def session_make(): def session_make():
return Session(engine) logger.debug("Creating new database session")
session = Session(engine)
logger.debug("Database session created successfully")
return session
def session(): def session():
logger.debug("Creating database session context")
with Session(engine) as session: with Session(engine) as session:
logger.debug("Database session context established")
yield session yield session
logger.debug("Database session context closed")

View file

@ -5,9 +5,13 @@ from fastapi import APIRouter, FastAPI
from dotenv import load_dotenv from dotenv import load_dotenv
import importlib import importlib
from typing import Any, overload from typing import Any, overload
from utils import traceroot_wrapper as traceroot
logger = traceroot.get_logger("environment")
logger.info("Loading environment variables from .env file")
load_dotenv() load_dotenv()
logger.info("Environment variables loaded successfully")
@overload @overload
@ -23,20 +27,26 @@ def env(key: str, default: Any) -> Any: ...
def env(key: str, default=None): def env(key: str, default=None):
return os.getenv(key, default) value = os.getenv(key, default)
logger.debug("Environment variable accessed", extra={"key": key, "has_value": value is not None, "using_default": value == default})
return value
def env_or_fail(key: str): def env_or_fail(key: str):
value = env(key) value = env(key)
if value is None: if value is None:
logger.error("Required environment variable missing", extra={"key": key})
raise Exception("can't get env config value.") raise Exception("can't get env config value.")
logger.debug("Required environment variable retrieved", extra={"key": key})
return value return value
def env_not_empty(key: str): def env_not_empty(key: str):
value = env(key) value = env(key)
if not value: if not value:
logger.error("Environment variable is empty", extra={"key": key})
raise Exception("env config value can't be empty.") raise Exception("env config value can't be empty.")
logger.debug("Non-empty environment variable retrieved", extra={"key": key})
return value return value
@ -71,8 +81,14 @@ def auto_include_routers(api: FastAPI, prefix: str, directory: str):
:param prefix: 路由前缀 :param prefix: 路由前缀
:param directory: 要扫描的目录路径 :param directory: 要扫描的目录路径
""" """
logger.info("Starting automatic router registration", extra={
"prefix": prefix,
"directory": directory
})
# 将目录转换为绝对路径 # 将目录转换为绝对路径
dir_path = Path(directory).resolve() dir_path = Path(directory).resolve()
router_count = 0
# 遍历目录下所有.py文件 # 遍历目录下所有.py文件
for root, _, files in os.walk(dir_path): for root, _, files in os.walk(dir_path):
@ -81,17 +97,44 @@ def auto_include_routers(api: FastAPI, prefix: str, directory: str):
# 构造完整文件路径 # 构造完整文件路径
file_path = Path(root) / file_name file_path = Path(root) / file_name
logger.debug("Processing controller file", extra={
"file_name": file_name,
"file_path": str(file_path)
})
# 生成模块名称 # 生成模块名称
module_name = file_path.stem module_name = file_path.stem
# 使用importlib加载模块 try:
spec = importlib.util.spec_from_file_location(module_name, file_path) # 使用importlib加载模块
if spec is None or spec.loader is None: spec = importlib.util.spec_from_file_location(module_name, file_path)
continue if spec is None or spec.loader is None:
module = importlib.util.module_from_spec(spec) logger.warning("Failed to create module spec", extra={"file_path": str(file_path)})
spec.loader.exec_module(module) continue
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# 检查模块中是否存在router属性且是APIRouter实例 # 检查模块中是否存在router属性且是APIRouter实例
router = getattr(module, "router", None) router = getattr(module, "router", None)
if isinstance(router, APIRouter): if isinstance(router, APIRouter):
api.include_router(router, prefix=prefix) api.include_router(router, prefix=prefix)
router_count += 1
logger.debug("Router registered successfully", extra={
"module_name": module_name,
"prefix": prefix
})
else:
logger.debug("No valid router found in module", extra={"module_name": module_name})
except Exception as e:
logger.error("Failed to load controller module", extra={
"module_name": module_name,
"file_path": str(file_path),
"error": str(e)
}, exc_info=True)
logger.info("Automatic router registration completed", extra={
"prefix": prefix,
"directory": directory,
"routers_registered": router_count
})

View file

@ -10,6 +10,9 @@ from fastapi_babel import _
from app.exception.exception import UserException from app.exception.exception import UserException
from app.component.database import engine from app.component.database import engine
from convert_case import snake_case from convert_case import snake_case
from utils import traceroot_wrapper as traceroot
logger = traceroot.get_logger("abstract_model")
class AbstractModel(SQLModel): class AbstractModel(SQLModel):
@ -27,6 +30,13 @@ class AbstractModel(SQLModel):
options: ExecutableOption | list[ExecutableOption] | None = None, options: ExecutableOption | list[ExecutableOption] | None = None,
s: Session, s: Session,
): ):
logger.debug("Executing query by conditions", extra={
"model_class": cls.__name__,
"has_order_by": order_by is not None,
"limit": limit,
"offset": offset,
"has_options": options is not None
})
stmt = select(cls).where(*whereclause) stmt = select(cls).where(*whereclause)
if order_by is not None: if order_by is not None:
stmt = stmt.order_by(order_by) stmt = stmt.order_by(order_by)
@ -44,8 +54,15 @@ class AbstractModel(SQLModel):
*whereclause: ColumnExpressionArgument[bool] | bool, *whereclause: ColumnExpressionArgument[bool] | bool,
s: Session, s: Session,
) -> bool: ) -> bool:
logger.debug("Checking if record exists", extra={"model_class": cls.__name__})
res = s.exec(select(func.count("*")).where(*whereclause)).first() res = s.exec(select(func.count("*")).where(*whereclause)).first()
return res is not None and res > 0 result = res is not None and res > 0
logger.debug("Record existence check result", extra={
"model_class": cls.__name__,
"exists": result,
"count": res
})
return result
@classmethod @classmethod
def count( def count(
@ -71,11 +88,24 @@ class AbstractModel(SQLModel):
*whereclause: ColumnExpressionArgument[bool], *whereclause: ColumnExpressionArgument[bool],
s: Session, s: Session,
): ):
logger.info("Deleting records by conditions", extra={"model_class": cls.__name__})
stmt = delete(cls).where(*whereclause) stmt = delete(cls).where(*whereclause)
s.connection().execute(stmt) result = s.connection().execute(stmt)
s.commit() s.commit()
logger.info("Records deleted", extra={
"model_class": cls.__name__,
"rows_affected": result.rowcount
})
def save(self, s: Session | None = None): def save(self, s: Session | None = None):
model_id = getattr(self, 'id', None)
is_new = model_id is None
logger.info("Saving model", extra={
"model_class": self.__class__.__name__,
"model_id": model_id,
"is_new_record": is_new
})
if s is None: if s is None:
with Session(engine, expire_on_commit=False) as s: with Session(engine, expire_on_commit=False) as s:
s.add(self) s.add(self)
@ -84,7 +114,22 @@ class AbstractModel(SQLModel):
s.add(self) s.add(self)
s.commit() s.commit()
logger.info("Model saved successfully", extra={
"model_class": self.__class__.__name__,
"model_id": getattr(self, 'id', None),
"was_new_record": is_new
})
def delete(self, s: Session): def delete(self, s: Session):
model_id = getattr(self, 'id', None)
is_soft_delete = isinstance(self, DefaultTimes)
logger.info("Deleting model", extra={
"model_class": self.__class__.__name__,
"model_id": model_id,
"is_soft_delete": is_soft_delete
})
if isinstance(self, DefaultTimes): if isinstance(self, DefaultTimes):
self.deleted_at = datetime.now() self.deleted_at = datetime.now()
self.save(s) self.save(s)
@ -92,6 +137,12 @@ class AbstractModel(SQLModel):
s.delete(self) s.delete(self)
s.commit() s.commit()
logger.info("Model deleted successfully", extra={
"model_class": self.__class__.__name__,
"model_id": model_id,
"was_soft_delete": is_soft_delete
})
def update_fields(self, update_dict: dict): def update_fields(self, update_dict: dict):
for k, v in update_dict.items(): for k, v in update_dict.items():
setattr(self, k, v) setattr(self, k, v)

View file

@ -6,9 +6,9 @@ export interface FloatingActionProps {
/** Current task status */ /** Current task status */
status: "running" | "pause" | "pending" | "finished"; status: "running" | "pause" | "pending" | "finished";
/** Callback when pause button is clicked */ /** Callback when pause button is clicked */
onPause?: () => void; // onPause?: () => void; // Commented out - temporary not needed
/** Callback when resume button is clicked */ /** Callback when resume button is clicked */
onResume?: () => void; // onResume?: () => void; // Commented out - temporary not needed
/** Callback when skip to next is clicked */ /** Callback when skip to next is clicked */
onSkip?: () => void; onSkip?: () => void;
/** Loading state for pause/resume actions */ /** Loading state for pause/resume actions */
@ -19,14 +19,14 @@ export interface FloatingActionProps {
export const FloatingAction = ({ export const FloatingAction = ({
status, status,
onPause, // onPause, // Commented out - temporary not needed
onResume, // onResume, // Commented out - temporary not needed
onSkip, onSkip,
loading = false, loading = false,
className, className,
}: FloatingActionProps) => { }: FloatingActionProps) => {
// Only show when task is running or paused // Only show when task is running (removed pause state)
if (status !== "running" && status !== "pause") { if (status !== "running") {
return null; return null;
} }
@ -38,6 +38,18 @@ export const FloatingAction = ({
)} )}
> >
<div className="pointer-events-auto flex items-center gap-2 bg-bg-surface-primary/95 backdrop-blur-md rounded-full p-1 shadow-[0px_4px_16px_rgba(0,0,0,0.12)] border border-border-default"> <div className="pointer-events-auto flex items-center gap-2 bg-bg-surface-primary/95 backdrop-blur-md rounded-full p-1 shadow-[0px_4px_16px_rgba(0,0,0,0.12)] border border-border-default">
{/* Always show Stop Task button when running (removed pause/resume logic) */}
<Button
variant="outline"
size="sm"
onClick={onSkip}
disabled={loading}
className="gap-1.5 rounded-full"
>
<span className="text-sm font-semibold">Stop Task</span>
</Button>
{/* Commented out pause/resume functionality
{status === "running" ? ( {status === "running" ? (
// State 1: Running - Show Pause button // State 1: Running - Show Pause button
<Button <Button
@ -73,6 +85,7 @@ export const FloatingAction = ({
</Button> </Button>
</> </>
)} )}
*/}
</div> </div>
</div> </div>
); );

View file

@ -5,14 +5,14 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
interface ProjectChatContainerProps { interface ProjectChatContainerProps {
className?: string; className?: string;
onPauseResume: () => void; // onPauseResume: () => void; // Commented out - temporary not needed
onSkip: () => void; onSkip: () => void;
isPauseResumeLoading: boolean; isPauseResumeLoading: boolean;
} }
export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
className = "", className = "",
onPauseResume, // onPauseResume, // Commented out - temporary not needed
onSkip, onSkip,
isPauseResumeLoading isPauseResumeLoading
}) => { }) => {
@ -165,7 +165,7 @@ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
chatStore={chatStore} chatStore={chatStore}
activeQueryId={activeQueryId} activeQueryId={activeQueryId}
onQueryActive={setActiveQueryId} onQueryActive={setActiveQueryId}
onPauseResume={onPauseResume} // onPauseResume={onPauseResume} // Commented out - temporary not needed
onSkip={onSkip} onSkip={onSkip}
isPauseResumeLoading={isPauseResumeLoading} isPauseResumeLoading={isPauseResumeLoading}
/> />

View file

@ -9,7 +9,7 @@ interface ProjectSectionProps {
chatStore: VanillaChatStore; chatStore: VanillaChatStore;
activeQueryId: string | null; activeQueryId: string | null;
onQueryActive: (queryId: string | null) => void; onQueryActive: (queryId: string | null) => void;
onPauseResume: () => void; // onPauseResume: () => void; // Commented out - temporary not needed
onSkip: () => void; onSkip: () => void;
isPauseResumeLoading: boolean; isPauseResumeLoading: boolean;
} }
@ -19,7 +19,7 @@ export const ProjectSection = React.forwardRef<HTMLDivElement, ProjectSectionPro
chatStore, chatStore,
activeQueryId, activeQueryId,
onQueryActive, onQueryActive,
onPauseResume, // onPauseResume, // Commented out - temporary not needed
onSkip, onSkip,
isPauseResumeLoading isPauseResumeLoading
}, ref) => { }, ref) => {
@ -64,8 +64,8 @@ export const ProjectSection = React.forwardRef<HTMLDivElement, ProjectSectionPro
{activeTaskId && ( {activeTaskId && (
<FloatingAction <FloatingAction
status={task.status} status={task.status}
onPause={onPauseResume} // onPause={onPauseResume} // Commented out - temporary not needed
onResume={onPauseResume} // onResume={onPauseResume} // Commented out - temporary not needed
onSkip={onSkip} onSkip={onSkip}
loading={isPauseResumeLoading} loading={isPauseResumeLoading}
/> />

View file

@ -103,7 +103,8 @@ export default function ChatBox(): JSX.Element {
const requiresHumanReply = Boolean(task?.activeAsk); const requiresHumanReply = Boolean(task?.activeAsk);
const isTaskInProgress = ["running", "pause"].includes(task?.status || ""); const isTaskInProgress = ["running", "pause"].includes(task?.status || "");
const isTaskBusy = ( const isTaskBusy = (
(task.status === 'running' && !task.hasMessages) || task.status === 'pause' || // running or paused counts as busy
(task.status === 'running' && task.hasMessages) || task.status === 'pause' ||
// splitting phase: has to_sub_tasks not confirmed OR skeleton computing // splitting phase: has to_sub_tasks not confirmed OR skeleton computing
task.messages.some(m => m.step === 'to_sub_tasks' && !m.isConfirm) || task.messages.some(m => m.step === 'to_sub_tasks' && !m.isConfirm) ||
((!task.messages.find(m => m.step === 'to_sub_tasks') && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl) || ((!task.messages.find(m => m.step === 'to_sub_tasks') && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl) ||
@ -175,11 +176,14 @@ export default function ChatBox(): JSX.Element {
const hasComplexTask = chatStore.tasks[_taskId as string].messages.some( const hasComplexTask = chatStore.tasks[_taskId as string].messages.some(
m => m.step === "to_sub_tasks" m => m.step === "to_sub_tasks"
); );
const hasErrorMessage = chatStore.tasks[_taskId as string].messages.some(
m => m.role === "agent" && m.content.startsWith("❌ **Error**:")
);
// Only start a new task if: pending, no messages processed yet // Only start a new task if: pending, no messages processed yet
// OR while or after replaying a project // OR while or after replaying a project
if ((chatStore.tasks[_taskId as string].status === "pending" && !hasSimpleResponse && !hasComplexTask && !isFinished) if ((chatStore.tasks[_taskId as string].status === "pending" && !hasSimpleResponse && !hasComplexTask && !isFinished)
|| chatStore.tasks[_taskId].type === "replay") { || chatStore.tasks[_taskId].type === "replay" || hasErrorMessage) {
setMessage(""); setMessage("");
// Pass the message content to startTask instead of adding it to current chatStore // Pass the message content to startTask instead of adding it to current chatStore
const attachesToSend = JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) || []; const attachesToSend = JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) || [];
@ -423,7 +427,10 @@ export default function ChatBox(): JSX.Element {
chatStore.setStatus(taskId, 'finished'); chatStore.setStatus(taskId, 'finished');
chatStore.setIsPending(taskId, false); chatStore.setIsPending(taskId, false);
toast.success("Task skipped successfully", { // toast.success("Task skipped successfully", {
// closeButton: true,
// });
toast.success("Task stopped successfully", {
closeButton: true, closeButton: true,
}); });
} catch (error) { } catch (error) {
@ -671,7 +678,7 @@ export default function ChatBox(): JSX.Element {
<div className="w-full h-full flex-1 flex flex-col"> <div className="w-full h-full flex-1 flex flex-col">
{/* New Project Chat Container */} {/* New Project Chat Container */}
<ProjectChatContainer <ProjectChatContainer
onPauseResume={handlePauseResume} // onPauseResume={handlePauseResume} // Commented out - temporary not needed
onSkip={handleSkip} onSkip={handleSkip}
isPauseResumeLoading={isPauseResumeLoading} isPauseResumeLoading={isPauseResumeLoading}
/> />

View file

@ -145,16 +145,16 @@ function HeaderWin() {
} }
}; };
//TODO: Mark ChatStore details as completed
const handleEndProject = async () => { const handleEndProject = async () => {
const taskId = chatStore.activeTaskId; const taskId = chatStore.activeTaskId;
const currentProjectId = projectStore.activeProjectId; const projectId = projectStore.activeProjectId;
if (!taskId) { if (!taskId) {
toast.error(t("layout.no-active-project-to-end")); toast.error(t("layout.no-active-project-to-end"));
return; return;
} }
const projectId = projectStore.activeProjectId;
const historyId = projectId ? projectStore.getHistoryId(projectId) : null; const historyId = projectId ? projectStore.getHistoryId(projectId) : null;
try { try {
@ -167,26 +167,26 @@ function HeaderWin() {
}); });
} }
// Delete task from backend if it exists // Stop Workforce
try { try {
await fetchDelete(`/chat/${taskId}`); await fetchDelete(`/chat/${projectId}`);
} catch (error) { } catch (error) {
console.log("Task may not exist on backend:", error); console.log("Task may not exist on backend:", error);
} }
// Delete from history using historyId // Delete from history using historyId
if (historyId) { if (historyId && task.status !== "finished") {
try { try {
await proxyFetchDelete(`/api/chat/history/${historyId}`); await proxyFetchDelete(`/api/chat/history/${historyId}`);
// Remove from local store
chatStore.removeTask(taskId);
} catch (error) { } catch (error) {
console.log("History may not exist:", error); console.log("History may not exist:", error);
} }
} else { } else {
console.warn("No historyId found for project, skipping history deletion"); console.warn("No historyId found for project or task finished, skipping history deletion");
} }
// Remove from local store
chatStore.removeTask(taskId);
// Create a completely new project instead of just a new task // Create a completely new project instead of just a new task
// This ensures we start fresh without any residual state // This ensures we start fresh without any residual state

View file

@ -24,7 +24,7 @@ const useChatStoreAdapter = ():{
} }
// Subscribe to store changes // Subscribe to store changes
const unsubscribe = activeChatStore.subscribe((state) => { const unsubscribe = activeChatStore.subscribe((state: ChatStore) => {
setChatState(state); setChatState(state);
}); });
// Set initial state // Set initial state

View file

@ -72,6 +72,7 @@
"agent-tool": "أداة الوكيل", "agent-tool": "أداة الوكيل",
"agent-tool-tooltip": "اختر الأدوات والتكاملات لوكيلك", "agent-tool-tooltip": "اختر الأدوات والتكاملات لوكيلك",
"resume": "استئناف", "resume": "استئناف",
"stop-task":"إيقاف المهمة",
"next-task": "المهمة التالية", "next-task": "المهمة التالية",
"task-splitting": "تقسيم المهام", "task-splitting": "تقسيم المهام",
"task-running": "تشغيل المهام", "task-running": "تشغيل المهام",

View file

@ -72,6 +72,7 @@
"agent-tool": "Agent-Tool", "agent-tool": "Agent-Tool",
"agent-tool-tooltip": "Wählen Sie Tools und Integrationen für Ihren Agenten", "agent-tool-tooltip": "Wählen Sie Tools und Integrationen für Ihren Agenten",
"resume": "Fortsetzen", "resume": "Fortsetzen",
"stop-task": "Aufgabe stoppen",
"next-task": "Nächste Aufgabe", "next-task": "Nächste Aufgabe",
"task-splitting": "Aufgabenteilung", "task-splitting": "Aufgabenteilung",
"task-running": "Aufgabe wird ausgeführt", "task-running": "Aufgabe wird ausgeführt",

View file

@ -72,6 +72,7 @@
"agent-tool": "Agent Tool", "agent-tool": "Agent Tool",
"agent-tool-tooltip": "Select tools and integrations for your agent", "agent-tool-tooltip": "Select tools and integrations for your agent",
"resume": "Resume", "resume": "Resume",
"stop-task": "Stop Task",
"next-task": "Next Task", "next-task": "Next Task",
"task-splitting": "Task Splitting", "task-splitting": "Task Splitting",
"task-running": "Task Running", "task-running": "Task Running",
@ -114,6 +115,7 @@
"project-ended-successfully": "Project ended successfully", "project-ended-successfully": "Project ended successfully",
"failed-to-end-project": "Failed to end project", "failed-to-end-project": "Failed to end project",
"message-queued": "Message queued. It will be processed when the current task finishes.", "message-queued": "Message queued. It will be processed when the current task finishes.",
"task-stopped-successfully": "Task stopped successfully",
"task-skipped-successfully": "Task skipped successfully", "task-skipped-successfully": "Task skipped successfully",
"failed-to-skip-task": "Failed to skip task", "failed-to-skip-task": "Failed to skip task",
"no-reply-received-task-continue": "No reply received, task continue", "no-reply-received-task-continue": "No reply received, task continue",

View file

@ -72,6 +72,7 @@
"agent-tool": "Herramienta de Agente", "agent-tool": "Herramienta de Agente",
"agent-tool-tooltip": "Selecciona herramientas e integraciones para tu agente", "agent-tool-tooltip": "Selecciona herramientas e integraciones para tu agente",
"resume": "Reanudar", "resume": "Reanudar",
"stop-task": "Detener Tarea",
"next-task": "Siguiente Tarea", "next-task": "Siguiente Tarea",
"task-splitting": "División de Tareas", "task-splitting": "División de Tareas",
"task-running": "Ejecutando Tarea", "task-running": "Ejecutando Tarea",

View file

@ -72,6 +72,7 @@
"agent-tool": "Outil Agent", "agent-tool": "Outil Agent",
"agent-tool-tooltip": "Sélectionnez les outils et intégrations pour votre agent", "agent-tool-tooltip": "Sélectionnez les outils et intégrations pour votre agent",
"resume": "Reprendre", "resume": "Reprendre",
"stop-task": "Arrêter la Tâche",
"next-task": "Tâche suivante", "next-task": "Tâche suivante",
"task-splitting": "Division des Tâches", "task-splitting": "Division des Tâches",
"task-running": "Exécution des Tâches", "task-running": "Exécution des Tâches",

View file

@ -72,6 +72,7 @@
"agent-tool": "Strumento Agente", "agent-tool": "Strumento Agente",
"agent-tool-tooltip": "Seleziona strumenti e integrazioni per il tuo agente", "agent-tool-tooltip": "Seleziona strumenti e integrazioni per il tuo agente",
"resume": "Riprendi", "resume": "Riprendi",
"stop-task": "Ferma Attività",
"next-task": "Prossima Attività", "next-task": "Prossima Attività",
"task-splitting": "Divisione Attività", "task-splitting": "Divisione Attività",
"task-running": "Esecuzione Attività", "task-running": "Esecuzione Attività",

View file

@ -72,6 +72,7 @@
"agent-tool": "エージェントツール", "agent-tool": "エージェントツール",
"agent-tool-tooltip": "エージェントのツールと統合を選択", "agent-tool-tooltip": "エージェントのツールと統合を選択",
"resume": "再開", "resume": "再開",
"stop-task": "タスクを停止",
"next-task": "次のタスク", "next-task": "次のタスク",
"task-splitting": "タスク分割", "task-splitting": "タスク分割",
"task-running": "タスク実行中", "task-running": "タスク実行中",

View file

@ -72,6 +72,7 @@
"agent-tool": "에이전트 도구", "agent-tool": "에이전트 도구",
"agent-tool-tooltip": "에이전트를 위한 도구 및 통합 선택", "agent-tool-tooltip": "에이전트를 위한 도구 및 통합 선택",
"resume": "재개", "resume": "재개",
"stop-task": "작업 중지",
"next-task": "다음 작업", "next-task": "다음 작업",
"task-splitting": "작업 분할", "task-splitting": "작업 분할",
"task-running": "작업 실행", "task-running": "작업 실행",

View file

@ -72,6 +72,7 @@
"agent-tool": "Инструмент агента", "agent-tool": "Инструмент агента",
"agent-tool-tooltip": "Выберите инструменты и интеграции для вашего агента", "agent-tool-tooltip": "Выберите инструменты и интеграции для вашего агента",
"resume": "Возобновить", "resume": "Возобновить",
"stop-task": "Остановить задачу",
"next-task": "Следующая задача", "next-task": "Следующая задача",
"task-splitting": "Разделение задачи", "task-splitting": "Разделение задачи",
"task-running": "Выполнение задачи", "task-running": "Выполнение задачи",

View file

@ -73,6 +73,7 @@
"agent-tool": "智能体工具", "agent-tool": "智能体工具",
"agent-tool-tooltip": "为您的智能体选择工具和集成", "agent-tool-tooltip": "为您的智能体选择工具和集成",
"resume": "恢复", "resume": "恢复",
"stop-task": "停止任务",
"next-task": "下一个任务", "next-task": "下一个任务",
"task-splitting": "任务拆分", "task-splitting": "任务拆分",
"task-running": "任务运行", "task-running": "任务运行",

View file

@ -74,6 +74,7 @@
"agent-tool": "智能體工具", "agent-tool": "智能體工具",
"agent-tool-tooltip": "為您的智能體選擇工具和整合", "agent-tool-tooltip": "為您的智能體選擇工具和整合",
"resume": "恢復", "resume": "恢復",
"stop-task": "停止任務",
"next-task": "下一個任務", "next-task": "下一個任務",
"task-splitting": "任務拆分", "task-splitting": "任務拆分",
"task-running": "任務執行", "task-running": "任務執行",

View file

@ -105,6 +105,7 @@ export interface ChatStore {
export type VanillaChatStore = { export type VanillaChatStore = {
getState: () => ChatStore; getState: () => ChatStore;
subscribe: (listener: (state: ChatStore) => void) => () => void;
}; };
@ -463,7 +464,27 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
}) : undefined, }) : undefined,
async onmessage(event: any) { async onmessage(event: any) {
const agentMessages: AgentMessage = JSON.parse(event.data); let agentMessages: AgentMessage;
try {
agentMessages = JSON.parse(event.data);
} catch (error) {
console.error('Failed to parse SSE message:', error);
console.error('Raw event.data:', event.data);
// Create error task to notify user
const currentStore = getCurrentChatStore();
const newTaskId = currentStore.create();
currentStore.setActiveTaskId(newTaskId);
currentStore.setHasWaitComfirm(newTaskId, true);
currentStore.addMessages(newTaskId, {
id: generateUniqueId(),
role: "agent",
content: `**System Error**: Failed to parse server message. The connection may be unstable.\n\nPlease try again or contact support if this persists.`,
});
return;
}
console.log("agentMessages", agentMessages); console.log("agentMessages", agentMessages);
const agentNameMap = { const agentNameMap = {
developer_agent: "Developer Agent", developer_agent: "Developer Agent",
@ -1048,7 +1069,15 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
addWebViewUrl(currentTaskId, agentMessages.data.message as string, agentMessages.data.process_task_id as string) addWebViewUrl(currentTaskId, agentMessages.data.message as string, agentMessages.data.process_task_id as string)
} }
if (agentMessages.data.method_name === 'browser_navigate' && agentMessages.data.message?.startsWith('{"url"')) { if (agentMessages.data.method_name === 'browser_navigate' && agentMessages.data.message?.startsWith('{"url"')) {
addWebViewUrl(currentTaskId, JSON.parse(agentMessages.data.message)?.url as string, agentMessages.data.process_task_id as string) try {
const urlData = JSON.parse(agentMessages.data.message);
if (urlData?.url) {
addWebViewUrl(currentTaskId, urlData.url as string, agentMessages.data.process_task_id as string)
}
} catch (error) {
console.error('Failed to parse browser_navigate URL:', error);
console.error('Raw message:', agentMessages.data.message);
}
} }
let taskRunning = [...tasks[currentTaskId].taskRunning] let taskRunning = [...tasks[currentTaskId].taskRunning]
@ -1101,7 +1130,7 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
return toolkit.toolkitName === agentMessages.data.toolkit_name && toolkit.toolkitMethods === agentMessages.data.method_name && toolkit.toolkitStatus === 'running' return toolkit.toolkitName === agentMessages.data.toolkit_name && toolkit.toolkitMethods === agentMessages.data.method_name && toolkit.toolkitStatus === 'running'
}) })
if (task.toolkits && index !== -1) { if (task.toolkits && index !== -1 && index !== undefined) {
task.toolkits[index].message += '\n' + message.data.message as string task.toolkits[index].message += '\n' + message.data.message as string
task.toolkits[index].toolkitStatus = "completed" task.toolkits[index].toolkitStatus = "completed"
} }
@ -1206,24 +1235,86 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
} }
if (agentMessages.step === "error") { if (agentMessages.step === "error") {
console.error('Model error:', agentMessages.data) try {
const errorMessage = agentMessages.data.message || 'An error occurred while processing your request'; console.error('Model error:', agentMessages.data);
// Create a new task to avoid "Task already exists" error // Validate that agentMessages.data exists before processing
// and completely reset the interface if (agentMessages.data === undefined || agentMessages.data === null) {
const newTaskId = create(); throw new Error('Invalid error message format: missing data');
// Prevent showing task skeleton after an error occurs }
setActiveTaskId(newTaskId);
setHasWaitComfirm(newTaskId, true);
// Add error message to the new clean task // Safely extract error message with fallback chain
addMessages(newTaskId, { const errorMessage = agentMessages.data?.message ||
id: generateUniqueId(), (typeof agentMessages.data === 'string' ? agentMessages.data : null) ||
role: "agent", 'An error occurred while processing your request';
content: `❌ **Error**: ${errorMessage}`,
}); // Mark all incomplete tasks as failed
uploadLog(currentTaskId, type) let taskRunning = [...tasks[currentTaskId].taskRunning];
return let taskAssigning = [...tasks[currentTaskId].taskAssigning];
// Update taskRunning - mark non-completed tasks as failed
taskRunning = taskRunning.map((task) => {
if (task.status !== "completed" && task.status !== "failed") {
task.status = "failed";
}
return task;
});
// Update taskAssigning - mark non-completed tasks as failed
taskAssigning = taskAssigning.map((agent) => {
agent.tasks = agent.tasks.map((task) => {
if (task.status !== "completed" && task.status !== "failed") {
task.status = "failed";
}
return task;
});
return agent;
});
// Apply the updates
setTaskRunning(currentTaskId, taskRunning);
setTaskAssigning(currentTaskId, taskAssigning);
// Complete the current task with error status
setStatus(currentTaskId, 'finished');
setIsPending(currentTaskId, false);
// Add error message to the current task
addMessages(currentTaskId, {
id: generateUniqueId(),
role: "agent",
content: `❌ **Error**: ${errorMessage}`,
});
uploadLog(currentTaskId, type)
// Stop the workforce
try {
await fetchDelete(`/chat/${project_id}`);
} catch (error) {
console.log("Task may not exist on backend:", error);
}
} catch (error) {
console.error('Failed to handle model error:', error);
console.error('Original agentMessages:', agentMessages);
// Fallback: try to create error task with minimal operations
try {
const { create, setActiveTaskId, setHasWaitComfirm, addMessages } = get();
const fallbackTaskId = create();
setActiveTaskId(fallbackTaskId);
setHasWaitComfirm(fallbackTaskId, true);
addMessages(fallbackTaskId, {
id: generateUniqueId(),
role: "agent",
content: `**Critical Error**: An unexpected error occurred while handling a model error. Please refresh the application or contact support.`,
});
} catch (fallbackError) {
console.error('Failed to create fallback error task:', fallbackError);
// Last resort: just log the error without creating UI elements
console.error('Original error that could not be displayed:', agentMessages);
}
}
return;
} }
// Handle add_task events for project store // Handle add_task events for project store