mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-28 03:30:06 +00:00
Merge branch 'main' into fix_install_logic
This commit is contained in:
commit
4eaf7a2e4c
30 changed files with 485 additions and 159 deletions
|
|
@ -23,14 +23,21 @@ def set_user_env_path(env_path: str | None = None):
|
|||
Set user-specific environment path for current thread.
|
||||
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):
|
||||
_thread_local.env_path = env_path
|
||||
# Load user-specific environment variables
|
||||
load_dotenv(dotenv_path=env_path, override=True)
|
||||
traceroot_logger.info("User-specific environment loaded", extra={"env_path": env_path})
|
||||
else:
|
||||
# Clear thread-local env_path to fall back to global
|
||||
if hasattr(_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:
|
||||
|
|
@ -54,8 +61,8 @@ def env(key: str, default: Any) -> Any: ...
|
|||
|
||||
def env(key: str, default=None):
|
||||
"""
|
||||
Get environment variable.
|
||||
First checks thread-local user-specific environment,
|
||||
Get environment variable.
|
||||
First checks thread-local user-specific environment,
|
||||
then falls back to global environment.
|
||||
"""
|
||||
# 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
|
||||
user_env_values = dotenv_values(_thread_local.env_path)
|
||||
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
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
logger = traceroot.get_logger("health_controller")
|
||||
|
||||
router = APIRouter(tags=["Health"])
|
||||
|
||||
|
|
@ -12,5 +15,8 @@ class HealthResponse(BaseModel):
|
|||
@router.get("/health", name="health check", response_model=HealthResponse)
|
||||
async def health_check():
|
||||
"""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
|
||||
|
||||
|
|
|
|||
|
|
@ -362,6 +362,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
# Update the sync_step with 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)
|
||||
# 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})
|
||||
|
||||
|
|
@ -385,32 +388,25 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
context_for_coordinator
|
||||
)
|
||||
|
||||
if not task_lock.summary_generated:
|
||||
summary_task_agent = task_summary_agent(options)
|
||||
try:
|
||||
summary_task_content = await asyncio.wait_for(
|
||||
summary_task(summary_task_agent, camel_task), timeout=10
|
||||
)
|
||||
task_lock.summary_generated = True
|
||||
logger.info("Generated summary for first task", extra={"project_id": options.project_id})
|
||||
except asyncio.TimeoutError:
|
||||
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_name = "Task"
|
||||
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
|
||||
if content_preview is None:
|
||||
content_preview = ""
|
||||
fallback_summary = (
|
||||
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
)
|
||||
summary_task_content = f"{fallback_name}|{fallback_summary}"
|
||||
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})
|
||||
summary_task_agent = task_summary_agent(options)
|
||||
try:
|
||||
summary_task_content = await asyncio.wait_for(
|
||||
summary_task(summary_task_agent, camel_task), timeout=10
|
||||
)
|
||||
task_lock.summary_generated = True
|
||||
logger.info("Generated summary for task", extra={"project_id": options.project_id})
|
||||
except asyncio.TimeoutError:
|
||||
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_name = "Task"
|
||||
content_preview = camel_task.content if hasattr(camel_task, "content") else ""
|
||||
if content_preview is None:
|
||||
content_preview = ""
|
||||
fallback_summary = (
|
||||
(content_preview[:80] + "...") if len(content_preview) > 80 else content_preview
|
||||
)
|
||||
summary_task_content = f"{fallback_name}|{fallback_summary}"
|
||||
task_lock.summary_generated = True
|
||||
|
||||
yield to_sub_tasks(camel_task, summary_task_content)
|
||||
# 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_result = item.data.get('result', '')
|
||||
|
||||
|
||||
assert camel_task is not None
|
||||
if camel_task is 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_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
|
||||
)
|
||||
|
||||
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}"
|
||||
# Generate proper LLM summary for multi-turn tasks instead of hardcoded fallback
|
||||
try:
|
||||
multi_turn_summary_agent = task_summary_agent(options)
|
||||
new_summary_content = await asyncio.wait_for(
|
||||
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
|
||||
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()
|
||||
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
|
||||
final_result: str = await get_task_result_with_optional_summary(camel_task, options)
|
||||
|
||||
task_lock.last_task_result = final_result
|
||||
|
||||
task_content: str = camel_task.content
|
||||
if "=== CURRENT TASK ===" in task_content:
|
||||
task_content = task_content.split("=== CURRENT TASK ===")[-1].strip()
|
||||
|
||||
# Handle task content - use fallback if camel_task is None
|
||||
if camel_task is not None:
|
||||
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_content': task_content,
|
||||
'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
|
||||
if camel_task is None:
|
||||
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:
|
||||
assert camel_task is not None
|
||||
task_lock.status = Status.processing
|
||||
camel_task.add_subtask(
|
||||
Task(
|
||||
|
|
|
|||
|
|
@ -283,30 +283,39 @@ class TaskLock:
|
|||
self.question_agent = 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):
|
||||
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)
|
||||
|
||||
async def get_queue(self):
|
||||
self.last_accessed = datetime.now()
|
||||
logger.debug("Getting item from task queue", extra={"task_id": self.id})
|
||||
return await self.queue.get()
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
def add_background_task(self, task: asyncio.Task) -> None:
|
||||
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)
|
||||
task.add_done_callback(lambda t: self.background_tasks.discard(t))
|
||||
|
||||
async def cleanup(self):
|
||||
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):
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
|
@ -315,9 +324,11 @@ class TaskLock:
|
|||
except asyncio.CancelledError:
|
||||
pass
|
||||
self.background_tasks.clear()
|
||||
logger.info("Task lock cleanup completed", extra={"task_id": self.id})
|
||||
|
||||
def add_conversation(self, role: str, content: str | dict):
|
||||
"""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({
|
||||
'role': role,
|
||||
'content': content,
|
||||
|
|
@ -344,7 +355,9 @@ task_index: dict[str, weakref.ref[Task]] = {}
|
|||
|
||||
def get_task_lock(id: str) -> TaskLock:
|
||||
if id not in task_locks:
|
||||
logger.error("Task lock not found", extra={"task_id": id})
|
||||
raise ProgramException("Task not found")
|
||||
logger.debug("Task lock retrieved", extra={"task_id": 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"""
|
||||
task_lock = get_task_lock(project_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:
|
||||
if id in task_locks:
|
||||
logger.warning("Attempting to create task lock that already exists", extra={"task_id": id})
|
||||
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={})
|
||||
|
||||
# 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():
|
||||
# _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]
|
||||
|
||||
|
||||
def get_or_create_task_lock(id: str) -> TaskLock:
|
||||
"""Get existing task lock or create a new one if it doesn't exist"""
|
||||
if id in task_locks:
|
||||
logger.debug("Using existing task lock", extra={"task_id": id})
|
||||
return task_locks[id]
|
||||
logger.info("Task lock not found, creating new one", extra={"task_id": id})
|
||||
return create_task_lock(id)
|
||||
|
||||
|
||||
async def delete_task_lock(id: str):
|
||||
if id not in task_locks:
|
||||
logger.warning("Attempting to delete non-existent task lock", extra={"task_id": id})
|
||||
raise ProgramException("Task not found")
|
||||
|
||||
# Clean up background tasks before deletion
|
||||
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()
|
||||
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,13 @@ class SingleAgentWorker(BaseSingleAgentWorker):
|
|||
context_utility: ContextUtility | None = None,
|
||||
enable_workflow_memory: bool = False,
|
||||
) -> 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__(
|
||||
description=description,
|
||||
worker=worker,
|
||||
|
|
@ -61,6 +68,12 @@ class SingleAgentWorker(BaseSingleAgentWorker):
|
|||
worker_agent = await self._get_worker_agent()
|
||||
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 = ""
|
||||
final_response = None
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -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.toolkit.abstract_toolkit import AbstractToolkit
|
||||
from app.service.task import process_task
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
logger = traceroot.get_logger("terminal_toolkit")
|
||||
|
||||
|
||||
@auto_listen_toolkit(BaseTerminalToolkit)
|
||||
|
|
@ -37,11 +40,22 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
self.agent_name = agent_name
|
||||
if working_directory is None:
|
||||
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:
|
||||
TerminalToolkit._thread_pool = ThreadPoolExecutor(
|
||||
max_workers=1,
|
||||
thread_name_prefix="terminal_toolkit"
|
||||
)
|
||||
logger.debug("Created terminal toolkit thread pool")
|
||||
|
||||
super().__init__(
|
||||
timeout=timeout,
|
||||
working_directory=working_directory,
|
||||
|
|
@ -62,6 +76,11 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
|
|||
"""
|
||||
# Convert ANSI escape sequences to plain text
|
||||
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))
|
||||
|
||||
def _update_terminal_output(self, output: str):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ from app.service.task import (
|
|||
Action,
|
||||
ActionAssignTaskData,
|
||||
ActionEndData,
|
||||
ActionNewTaskStateData,
|
||||
ActionTaskStateData,
|
||||
get_camel_task,
|
||||
get_task_lock,
|
||||
|
|
@ -45,6 +44,14 @@ class Workforce(BaseWorkforce):
|
|||
use_structured_output_handler: bool = True,
|
||||
) -> None:
|
||||
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__(
|
||||
description=description,
|
||||
children=children,
|
||||
|
|
@ -65,13 +72,20 @@ class Workforce(BaseWorkforce):
|
|||
coordinator_context: Optional context ONLY for coordinator agent during decomposition.
|
||||
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):
|
||||
task.state = TaskState.FAILED
|
||||
task.result = "Task failed: Invalid or empty content provided"
|
||||
logger.warning(
|
||||
f"Task {task.id} rejected: Invalid or empty content. Content preview: '{task.content[:50]}...'"
|
||||
)
|
||||
logger.warning("Task rejected: Invalid or empty content", extra={
|
||||
"task_id": task.id,
|
||||
"content_preview": task.content[:50] + "..." if len(task.content) > 50 else task.content
|
||||
})
|
||||
raise UserException(code.error, task.result)
|
||||
|
||||
self.reset()
|
||||
|
|
@ -81,11 +95,19 @@ class Workforce(BaseWorkforce):
|
|||
task.state = TaskState.OPEN
|
||||
|
||||
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
|
||||
|
||||
async def eigent_start(self, subtasks: list[Task]):
|
||||
"""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))
|
||||
# Save initial snapshot
|
||||
self.save_snapshot("Initial task decomposition")
|
||||
|
|
@ -93,7 +115,10 @@ class Workforce(BaseWorkforce):
|
|||
try:
|
||||
await self.start()
|
||||
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
|
||||
raise
|
||||
finally:
|
||||
|
|
@ -294,18 +319,11 @@ class Workforce(BaseWorkforce):
|
|||
"failure_count": task.failure_count,
|
||||
}
|
||||
|
||||
if self._task_is_new(task_data):
|
||||
await task_lock.put_queue(
|
||||
ActionNewTaskStateData(
|
||||
data=task_data
|
||||
)
|
||||
)
|
||||
else:
|
||||
await task_lock.put_queue(
|
||||
ActionTaskStateData(
|
||||
data=task_data
|
||||
)
|
||||
await task_lock.put_queue(
|
||||
ActionTaskStateData(
|
||||
data=task_data
|
||||
)
|
||||
)
|
||||
|
||||
return await super()._handle_completed_task(task)
|
||||
|
||||
|
|
@ -339,36 +357,6 @@ class Workforce(BaseWorkforce):
|
|||
|
||||
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:
|
||||
super().stop()
|
||||
task_lock = get_task_lock(self.api_task_id)
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ exports.default = async function notarizing(context) {
|
|||
|
||||
// Validate required environment variables
|
||||
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.error("Required: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID");
|
||||
throw new Error("Notarization failed: Missing required environment variables");
|
||||
console.warn("Missing Apple environment variables for notarization");
|
||||
console.warn("Skipping notarization. Required: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID");
|
||||
return;
|
||||
}
|
||||
|
||||
return notarize({
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
|
||||
// Check if file exists and has size > 0
|
||||
try {
|
||||
|
||||
if (fs.existsSync(destinationPath)) {
|
||||
const stats = fs.statSync(destinationPath)
|
||||
if (stats.size === 0) {
|
||||
|
|
@ -89,6 +90,7 @@ export async function downloadWithRedirects(url, destinationPath) {
|
|||
}
|
||||
} catch (err) {
|
||||
safeReject(new Error(`Failed to verify download: ${err.message}`))
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
from sqlmodel import Session, create_engine
|
||||
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(
|
||||
env_or_fail("database_url"),
|
||||
|
|
@ -8,11 +16,19 @@ engine = create_engine(
|
|||
pool_size=36,
|
||||
)
|
||||
|
||||
logger.info("Database engine initialized successfully")
|
||||
|
||||
|
||||
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():
|
||||
logger.debug("Creating database session context")
|
||||
with Session(engine) as session:
|
||||
logger.debug("Database session context established")
|
||||
yield session
|
||||
logger.debug("Database session context closed")
|
||||
|
|
|
|||
|
|
@ -5,9 +5,13 @@ from fastapi import APIRouter, FastAPI
|
|||
from dotenv import load_dotenv
|
||||
import importlib
|
||||
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()
|
||||
logger.info("Environment variables loaded successfully")
|
||||
|
||||
|
||||
@overload
|
||||
|
|
@ -23,20 +27,26 @@ def env(key: str, default: Any) -> Any: ...
|
|||
|
||||
|
||||
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):
|
||||
value = env(key)
|
||||
if value is None:
|
||||
logger.error("Required environment variable missing", extra={"key": key})
|
||||
raise Exception("can't get env config value.")
|
||||
logger.debug("Required environment variable retrieved", extra={"key": key})
|
||||
return value
|
||||
|
||||
|
||||
def env_not_empty(key: str):
|
||||
value = env(key)
|
||||
if not value:
|
||||
logger.error("Environment variable is empty", extra={"key": key})
|
||||
raise Exception("env config value can't be empty.")
|
||||
logger.debug("Non-empty environment variable retrieved", extra={"key": key})
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -71,8 +81,14 @@ def auto_include_routers(api: FastAPI, prefix: str, directory: str):
|
|||
:param prefix: 路由前缀
|
||||
:param directory: 要扫描的目录路径
|
||||
"""
|
||||
logger.info("Starting automatic router registration", extra={
|
||||
"prefix": prefix,
|
||||
"directory": directory
|
||||
})
|
||||
|
||||
# 将目录转换为绝对路径
|
||||
dir_path = Path(directory).resolve()
|
||||
router_count = 0
|
||||
|
||||
# 遍历目录下所有.py文件
|
||||
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
|
||||
|
||||
logger.debug("Processing controller file", extra={
|
||||
"file_name": file_name,
|
||||
"file_path": str(file_path)
|
||||
})
|
||||
|
||||
# 生成模块名称
|
||||
module_name = file_path.stem
|
||||
|
||||
# 使用importlib加载模块
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
if spec is None or spec.loader is None:
|
||||
continue
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
try:
|
||||
# 使用importlib加载模块
|
||||
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||
if spec is None or spec.loader is None:
|
||||
logger.warning("Failed to create module spec", extra={"file_path": str(file_path)})
|
||||
continue
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# 检查模块中是否存在router属性且是APIRouter实例
|
||||
router = getattr(module, "router", None)
|
||||
if isinstance(router, APIRouter):
|
||||
api.include_router(router, prefix=prefix)
|
||||
# 检查模块中是否存在router属性且是APIRouter实例
|
||||
router = getattr(module, "router", None)
|
||||
if isinstance(router, APIRouter):
|
||||
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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ from fastapi_babel import _
|
|||
from app.exception.exception import UserException
|
||||
from app.component.database import engine
|
||||
from convert_case import snake_case
|
||||
from utils import traceroot_wrapper as traceroot
|
||||
|
||||
logger = traceroot.get_logger("abstract_model")
|
||||
|
||||
|
||||
class AbstractModel(SQLModel):
|
||||
|
|
@ -27,6 +30,13 @@ class AbstractModel(SQLModel):
|
|||
options: ExecutableOption | list[ExecutableOption] | None = None,
|
||||
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)
|
||||
if order_by is not None:
|
||||
stmt = stmt.order_by(order_by)
|
||||
|
|
@ -44,8 +54,15 @@ class AbstractModel(SQLModel):
|
|||
*whereclause: ColumnExpressionArgument[bool] | bool,
|
||||
s: Session,
|
||||
) -> bool:
|
||||
logger.debug("Checking if record exists", extra={"model_class": cls.__name__})
|
||||
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
|
||||
def count(
|
||||
|
|
@ -71,11 +88,24 @@ class AbstractModel(SQLModel):
|
|||
*whereclause: ColumnExpressionArgument[bool],
|
||||
s: Session,
|
||||
):
|
||||
logger.info("Deleting records by conditions", extra={"model_class": cls.__name__})
|
||||
stmt = delete(cls).where(*whereclause)
|
||||
s.connection().execute(stmt)
|
||||
result = s.connection().execute(stmt)
|
||||
s.commit()
|
||||
logger.info("Records deleted", extra={
|
||||
"model_class": cls.__name__,
|
||||
"rows_affected": result.rowcount
|
||||
})
|
||||
|
||||
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:
|
||||
with Session(engine, expire_on_commit=False) as s:
|
||||
s.add(self)
|
||||
|
|
@ -84,7 +114,22 @@ class AbstractModel(SQLModel):
|
|||
s.add(self)
|
||||
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):
|
||||
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):
|
||||
self.deleted_at = datetime.now()
|
||||
self.save(s)
|
||||
|
|
@ -92,6 +137,12 @@ class AbstractModel(SQLModel):
|
|||
s.delete(self)
|
||||
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):
|
||||
for k, v in update_dict.items():
|
||||
setattr(self, k, v)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ export interface FloatingActionProps {
|
|||
/** Current task status */
|
||||
status: "running" | "pause" | "pending" | "finished";
|
||||
/** Callback when pause button is clicked */
|
||||
onPause?: () => void;
|
||||
// onPause?: () => void; // Commented out - temporary not needed
|
||||
/** Callback when resume button is clicked */
|
||||
onResume?: () => void;
|
||||
// onResume?: () => void; // Commented out - temporary not needed
|
||||
/** Callback when skip to next is clicked */
|
||||
onSkip?: () => void;
|
||||
/** Loading state for pause/resume actions */
|
||||
|
|
@ -19,14 +19,14 @@ export interface FloatingActionProps {
|
|||
|
||||
export const FloatingAction = ({
|
||||
status,
|
||||
onPause,
|
||||
onResume,
|
||||
// onPause, // Commented out - temporary not needed
|
||||
// onResume, // Commented out - temporary not needed
|
||||
onSkip,
|
||||
loading = false,
|
||||
className,
|
||||
}: FloatingActionProps) => {
|
||||
// Only show when task is running or paused
|
||||
if (status !== "running" && status !== "pause") {
|
||||
// Only show when task is running (removed pause state)
|
||||
if (status !== "running") {
|
||||
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">
|
||||
{/* 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" ? (
|
||||
// State 1: Running - Show Pause button
|
||||
<Button
|
||||
|
|
@ -73,6 +85,7 @@ export const FloatingAction = ({
|
|||
</Button>
|
||||
</>
|
||||
)}
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
|||
|
||||
interface ProjectChatContainerProps {
|
||||
className?: string;
|
||||
onPauseResume: () => void;
|
||||
// onPauseResume: () => void; // Commented out - temporary not needed
|
||||
onSkip: () => void;
|
||||
isPauseResumeLoading: boolean;
|
||||
}
|
||||
|
||||
export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
|
||||
export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
|
||||
className = "",
|
||||
onPauseResume,
|
||||
// onPauseResume, // Commented out - temporary not needed
|
||||
onSkip,
|
||||
isPauseResumeLoading
|
||||
}) => {
|
||||
|
|
@ -165,7 +165,7 @@ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
|
|||
chatStore={chatStore}
|
||||
activeQueryId={activeQueryId}
|
||||
onQueryActive={setActiveQueryId}
|
||||
onPauseResume={onPauseResume}
|
||||
// onPauseResume={onPauseResume} // Commented out - temporary not needed
|
||||
onSkip={onSkip}
|
||||
isPauseResumeLoading={isPauseResumeLoading}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface ProjectSectionProps {
|
|||
chatStore: VanillaChatStore;
|
||||
activeQueryId: string | null;
|
||||
onQueryActive: (queryId: string | null) => void;
|
||||
onPauseResume: () => void;
|
||||
// onPauseResume: () => void; // Commented out - temporary not needed
|
||||
onSkip: () => void;
|
||||
isPauseResumeLoading: boolean;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export const ProjectSection = React.forwardRef<HTMLDivElement, ProjectSectionPro
|
|||
chatStore,
|
||||
activeQueryId,
|
||||
onQueryActive,
|
||||
onPauseResume,
|
||||
// onPauseResume, // Commented out - temporary not needed
|
||||
onSkip,
|
||||
isPauseResumeLoading
|
||||
}, ref) => {
|
||||
|
|
@ -64,8 +64,8 @@ export const ProjectSection = React.forwardRef<HTMLDivElement, ProjectSectionPro
|
|||
{activeTaskId && (
|
||||
<FloatingAction
|
||||
status={task.status}
|
||||
onPause={onPauseResume}
|
||||
onResume={onPauseResume}
|
||||
// onPause={onPauseResume} // Commented out - temporary not needed
|
||||
// onResume={onPauseResume} // Commented out - temporary not needed
|
||||
onSkip={onSkip}
|
||||
loading={isPauseResumeLoading}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ export default function ChatBox(): JSX.Element {
|
|||
const requiresHumanReply = Boolean(task?.activeAsk);
|
||||
const isTaskInProgress = ["running", "pause"].includes(task?.status || "");
|
||||
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
|
||||
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) ||
|
||||
|
|
@ -175,11 +176,14 @@ export default function ChatBox(): JSX.Element {
|
|||
const hasComplexTask = chatStore.tasks[_taskId as string].messages.some(
|
||||
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
|
||||
// OR while or after replaying a project
|
||||
if ((chatStore.tasks[_taskId as string].status === "pending" && !hasSimpleResponse && !hasComplexTask && !isFinished)
|
||||
|| chatStore.tasks[_taskId].type === "replay") {
|
||||
|| chatStore.tasks[_taskId].type === "replay" || hasErrorMessage) {
|
||||
setMessage("");
|
||||
// Pass the message content to startTask instead of adding it to current chatStore
|
||||
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.setIsPending(taskId, false);
|
||||
|
||||
toast.success("Task skipped successfully", {
|
||||
// toast.success("Task skipped successfully", {
|
||||
// closeButton: true,
|
||||
// });
|
||||
toast.success("Task stopped successfully", {
|
||||
closeButton: true,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -671,7 +678,7 @@ export default function ChatBox(): JSX.Element {
|
|||
<div className="w-full h-full flex-1 flex flex-col">
|
||||
{/* New Project Chat Container */}
|
||||
<ProjectChatContainer
|
||||
onPauseResume={handlePauseResume}
|
||||
// onPauseResume={handlePauseResume} // Commented out - temporary not needed
|
||||
onSkip={handleSkip}
|
||||
isPauseResumeLoading={isPauseResumeLoading}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -145,16 +145,16 @@ function HeaderWin() {
|
|||
}
|
||||
};
|
||||
|
||||
//TODO: Mark ChatStore details as completed
|
||||
const handleEndProject = async () => {
|
||||
const taskId = chatStore.activeTaskId;
|
||||
const currentProjectId = projectStore.activeProjectId;
|
||||
|
||||
const projectId = projectStore.activeProjectId;
|
||||
|
||||
if (!taskId) {
|
||||
toast.error(t("layout.no-active-project-to-end"));
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = projectStore.activeProjectId;
|
||||
const historyId = projectId ? projectStore.getHistoryId(projectId) : null;
|
||||
|
||||
try {
|
||||
|
|
@ -167,26 +167,26 @@ function HeaderWin() {
|
|||
});
|
||||
}
|
||||
|
||||
// Delete task from backend if it exists
|
||||
// Stop Workforce
|
||||
try {
|
||||
await fetchDelete(`/chat/${taskId}`);
|
||||
await fetchDelete(`/chat/${projectId}`);
|
||||
} catch (error) {
|
||||
console.log("Task may not exist on backend:", error);
|
||||
}
|
||||
|
||||
// Delete from history using historyId
|
||||
if (historyId) {
|
||||
if (historyId && task.status !== "finished") {
|
||||
try {
|
||||
await proxyFetchDelete(`/api/chat/history/${historyId}`);
|
||||
// Remove from local store
|
||||
chatStore.removeTask(taskId);
|
||||
} catch (error) {
|
||||
console.log("History may not exist:", error);
|
||||
}
|
||||
} 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
|
||||
// This ensures we start fresh without any residual state
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const useChatStoreAdapter = ():{
|
|||
}
|
||||
|
||||
// Subscribe to store changes
|
||||
const unsubscribe = activeChatStore.subscribe((state) => {
|
||||
const unsubscribe = activeChatStore.subscribe((state: ChatStore) => {
|
||||
setChatState(state);
|
||||
});
|
||||
// Set initial state
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "أداة الوكيل",
|
||||
"agent-tool-tooltip": "اختر الأدوات والتكاملات لوكيلك",
|
||||
"resume": "استئناف",
|
||||
"stop-task":"إيقاف المهمة",
|
||||
"next-task": "المهمة التالية",
|
||||
"task-splitting": "تقسيم المهام",
|
||||
"task-running": "تشغيل المهام",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "Agent-Tool",
|
||||
"agent-tool-tooltip": "Wählen Sie Tools und Integrationen für Ihren Agenten",
|
||||
"resume": "Fortsetzen",
|
||||
"stop-task": "Aufgabe stoppen",
|
||||
"next-task": "Nächste Aufgabe",
|
||||
"task-splitting": "Aufgabenteilung",
|
||||
"task-running": "Aufgabe wird ausgeführt",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "Agent Tool",
|
||||
"agent-tool-tooltip": "Select tools and integrations for your agent",
|
||||
"resume": "Resume",
|
||||
"stop-task": "Stop Task",
|
||||
"next-task": "Next Task",
|
||||
"task-splitting": "Task Splitting",
|
||||
"task-running": "Task Running",
|
||||
|
|
@ -114,6 +115,7 @@
|
|||
"project-ended-successfully": "Project ended successfully",
|
||||
"failed-to-end-project": "Failed to end project",
|
||||
"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",
|
||||
"failed-to-skip-task": "Failed to skip task",
|
||||
"no-reply-received-task-continue": "No reply received, task continue",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "Herramienta de Agente",
|
||||
"agent-tool-tooltip": "Selecciona herramientas e integraciones para tu agente",
|
||||
"resume": "Reanudar",
|
||||
"stop-task": "Detener Tarea",
|
||||
"next-task": "Siguiente Tarea",
|
||||
"task-splitting": "División de Tareas",
|
||||
"task-running": "Ejecutando Tarea",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "Outil Agent",
|
||||
"agent-tool-tooltip": "Sélectionnez les outils et intégrations pour votre agent",
|
||||
"resume": "Reprendre",
|
||||
"stop-task": "Arrêter la Tâche",
|
||||
"next-task": "Tâche suivante",
|
||||
"task-splitting": "Division des Tâches",
|
||||
"task-running": "Exécution des Tâches",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "Strumento Agente",
|
||||
"agent-tool-tooltip": "Seleziona strumenti e integrazioni per il tuo agente",
|
||||
"resume": "Riprendi",
|
||||
"stop-task": "Ferma Attività",
|
||||
"next-task": "Prossima Attività",
|
||||
"task-splitting": "Divisione Attività",
|
||||
"task-running": "Esecuzione Attività",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "エージェントツール",
|
||||
"agent-tool-tooltip": "エージェントのツールと統合を選択",
|
||||
"resume": "再開",
|
||||
"stop-task": "タスクを停止",
|
||||
"next-task": "次のタスク",
|
||||
"task-splitting": "タスク分割",
|
||||
"task-running": "タスク実行中",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "에이전트 도구",
|
||||
"agent-tool-tooltip": "에이전트를 위한 도구 및 통합 선택",
|
||||
"resume": "재개",
|
||||
"stop-task": "작업 중지",
|
||||
"next-task": "다음 작업",
|
||||
"task-splitting": "작업 분할",
|
||||
"task-running": "작업 실행",
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
"agent-tool": "Инструмент агента",
|
||||
"agent-tool-tooltip": "Выберите инструменты и интеграции для вашего агента",
|
||||
"resume": "Возобновить",
|
||||
"stop-task": "Остановить задачу",
|
||||
"next-task": "Следующая задача",
|
||||
"task-splitting": "Разделение задачи",
|
||||
"task-running": "Выполнение задачи",
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@
|
|||
"agent-tool": "智能体工具",
|
||||
"agent-tool-tooltip": "为您的智能体选择工具和集成",
|
||||
"resume": "恢复",
|
||||
"stop-task": "停止任务",
|
||||
"next-task": "下一个任务",
|
||||
"task-splitting": "任务拆分",
|
||||
"task-running": "任务运行",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
"agent-tool": "智能體工具",
|
||||
"agent-tool-tooltip": "為您的智能體選擇工具和整合",
|
||||
"resume": "恢復",
|
||||
"stop-task": "停止任務",
|
||||
"next-task": "下一個任務",
|
||||
"task-splitting": "任務拆分",
|
||||
"task-running": "任務執行",
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ export interface ChatStore {
|
|||
|
||||
export type VanillaChatStore = {
|
||||
getState: () => ChatStore;
|
||||
subscribe: (listener: (state: ChatStore) => void) => () => void;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -463,7 +464,27 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
}) : undefined,
|
||||
|
||||
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);
|
||||
const agentNameMap = {
|
||||
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)
|
||||
}
|
||||
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]
|
||||
|
||||
|
|
@ -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'
|
||||
})
|
||||
|
||||
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].toolkitStatus = "completed"
|
||||
}
|
||||
|
|
@ -1206,24 +1235,86 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
}
|
||||
|
||||
if (agentMessages.step === "error") {
|
||||
console.error('Model error:', agentMessages.data)
|
||||
const errorMessage = agentMessages.data.message || 'An error occurred while processing your request';
|
||||
try {
|
||||
console.error('Model error:', agentMessages.data);
|
||||
|
||||
// Create a new task to avoid "Task already exists" error
|
||||
// and completely reset the interface
|
||||
const newTaskId = create();
|
||||
// Prevent showing task skeleton after an error occurs
|
||||
setActiveTaskId(newTaskId);
|
||||
setHasWaitComfirm(newTaskId, true);
|
||||
// Validate that agentMessages.data exists before processing
|
||||
if (agentMessages.data === undefined || agentMessages.data === null) {
|
||||
throw new Error('Invalid error message format: missing data');
|
||||
}
|
||||
|
||||
// Add error message to the new clean task
|
||||
addMessages(newTaskId, {
|
||||
id: generateUniqueId(),
|
||||
role: "agent",
|
||||
content: `❌ **Error**: ${errorMessage}`,
|
||||
});
|
||||
uploadLog(currentTaskId, type)
|
||||
return
|
||||
// Safely extract error message with fallback chain
|
||||
const errorMessage = agentMessages.data?.message ||
|
||||
(typeof agentMessages.data === 'string' ? agentMessages.data : null) ||
|
||||
'An error occurred while processing your request';
|
||||
|
||||
// Mark all incomplete tasks as failed
|
||||
let taskRunning = [...tasks[currentTaskId].taskRunning];
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue