Merge branch 'main' into fix-#471

This commit is contained in:
Wendong-Fan 2025-10-20 18:09:29 +08:00 committed by GitHub
commit c6e78a9fff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1000 additions and 1145 deletions

View file

@ -88,7 +88,11 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
assert isinstance(item, ActionImproveData)
question = item.data
if len(question) < 12 and len(options.attaches) == 0:
confirm = await question_confirm(question_agent, question)
messages, confirm = await question_confirm(
question_agent, question, timeout=8.0, task_id=options.task_id
)
for msg in messages:
yield msg
else:
confirm = True
@ -109,7 +113,21 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
camel_task.additional_info = {Path(file_path).name: file_path for file_path in options.attaches}
sub_tasks = await asyncio.to_thread(workforce.eigent_make_sub_tasks, camel_task)
summary_task_content = await summary_task(summary_task_agent, camel_task)
try:
summary_task_content = await asyncio.wait_for(
summary_task(summary_task_agent, camel_task), timeout=10
)
except asyncio.TimeoutError:
logger.warning(f"summary_task timeout for task {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}"
yield to_sub_tasks(camel_task, summary_task_content)
# tracer.stop()
# tracer.save("trace.json")
@ -287,8 +305,27 @@ def add_sub_tasks(camel_task: Task, update_tasks: list[TaskContent]):
)
async def question_confirm(agent: ListenChatAgent, prompt: str) -> str | Literal[True]:
prompt = f"""
async def question_confirm(
agent: ListenChatAgent,
prompt: str,
timeout: float = 8.0,
task_id: str = ""
) -> tuple[list, str | Literal[True]]:
"""
Confirm whether a question requires workforce processing.
Args:
agent: The agent to use for classification
prompt: The user's question
timeout: Timeout in seconds (default: 8.0)
task_id: Task ID for logging
Returns:
Tuple of (messages_to_yield, confirm_result)
- messages_to_yield: List of SSE messages to yield (empty if no timeout)
- confirm_result: True to proceed with workforce, or sse_json for user confirmation
"""
analysis_prompt = f"""
> **Your Role:** You are a highly capable agent. Your primary function is to analyze a user's request and determine the appropriate course of action.
>
> **Your Process:**
@ -303,12 +340,26 @@ async def question_confirm(agent: ListenChatAgent, prompt: str) -> str | Literal
> * **For a Simple Query:** Provide a direct and helpful response.
> * **For a Complex Task:** Your *only* response should be "yes". This will trigger a specialized workforce to handle the task. Do not include any other text, punctuation, or pleasantries.
"""
resp = agent.step(prompt)
logger.info(f"resp: {agent.chat_history}")
if resp.msgs[0].content.lower() != "yes":
return sse_json("wait_confirm", {"content": resp.msgs[0].content})
else:
return True
try:
resp = await asyncio.wait_for(
asyncio.to_thread(agent.step, analysis_prompt),
timeout=timeout
)
logger.info(f"resp: {agent.chat_history}")
if resp.msgs[0].content.lower() != "yes":
return ([], sse_json("wait_confirm", {"content": resp.msgs[0].content}))
else:
return ([], True)
except asyncio.TimeoutError:
logger.warning(f"question_confirm timeout for task {task_id}")
notice = sse_json(
"notice",
{"notice": "Quick classification timed out. Responding directly."}
)
return ([notice], True)
async def summary_task(agent: ListenChatAgent, task: Task) -> str:

View file

@ -323,6 +323,17 @@ class ListenChatAgent(ChatAgent):
else:
result = raw_result
mask_flag = False
# Prepare result message with truncation
if isinstance(result, str):
result_msg = result
else:
result_str = repr(result)
MAX_RESULT_LENGTH = 500
if len(result_str) > MAX_RESULT_LENGTH:
result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)"
else:
result_msg = result_str
asyncio.create_task(
task_lock.put_queue(
ActionDeactivateToolkitData(
@ -331,7 +342,7 @@ class ListenChatAgent(ChatAgent):
"process_task_id": self.process_task_id,
"toolkit_name": toolkit_name,
"method_name": func_name,
"message": result if isinstance(result, str) else repr(result),
"message": result_msg,
},
)
)
@ -407,6 +418,17 @@ class ListenChatAgent(ChatAgent):
traceroot_logger.error(f"Async tool execution failed for {func_name}: {e}")
traceback.print_exc()
# Prepare result message with truncation
if isinstance(result, str):
result_msg = result
else:
result_str = repr(result)
MAX_RESULT_LENGTH = 500
if len(result_str) > MAX_RESULT_LENGTH:
result_msg = result_str[:MAX_RESULT_LENGTH] + f"... (truncated, total length: {len(result_str)} chars)"
else:
result_msg = result_str
await task_lock.put_queue(
ActionDeactivateToolkitData(
data={
@ -414,7 +436,7 @@ class ListenChatAgent(ChatAgent):
"process_task_id": self.process_task_id,
"toolkit_name": toolkit_name,
"method_name": func_name,
"message": result if isinstance(result, str) else repr(result),
"message": result_msg,
},
)
)
@ -427,7 +449,7 @@ class ListenChatAgent(ChatAgent):
# Clone tools and collect toolkits that need registration
cloned_tools, toolkits_to_register = self._clone_tools()
new_agent = ListenChatAgent(
api_task_id=self.api_task_id,
agent_name=self.agent_name,
@ -443,7 +465,6 @@ class ListenChatAgent(ChatAgent):
response_terminators=self.response_terminators,
scheduling_strategy=self.model_backend.scheduling_strategy.__name__,
max_iteration=self.max_iteration,
agent_id=self.agent_id,
stop_event=self.stop_event,
tool_execution_timeout=self.tool_execution_timeout,
mask_tool_output=self.mask_tool_output,
@ -729,6 +750,8 @@ def search_agent(options: Chat):
],
)
# Save reference before registering for toolkits_to_register_agent
web_toolkit_for_agent_registration = web_toolkit_custom
web_toolkit_custom = message_integration.register_toolkits(web_toolkit_custom)
terminal_toolkit = TerminalToolkit(options.task_id, Agents.search_agent, safe_mode=True, clone_current_env=False)
terminal_toolkit = message_integration.register_functions([terminal_toolkit.shell_exec])
@ -793,7 +816,7 @@ The current date is {datetime.date.today()}. For any date-related tasks, you MUS
- **CRITICAL URL POLICY**: You are STRICTLY FORBIDDEN from inventing,
guessing, or constructing URLs yourself. You MUST only use URLs from
trusted sources:
1. URLs returned by search tools (like `search_google` or `search_exa`)
1. URLs returned by search tools (`search_google`)
2. URLs found on webpages you have visited through browser tools
3. URLs provided by the user in their request
Fabricating or guessing URLs is considered a critical error and must
@ -839,8 +862,6 @@ Your approach depends on available search tools:
sites using `browser_type` and submit with `browser_enter`
- **Extract URLs from results**: Only use URLs that appear in the search
results on these websites
- **Alternative Search**: If available, use `search_exa` for additional
results
**Common Browser Operations (both scenarios):**
- **Navigation and Exploration**: Use `browser_visit_page` to open URLs.
@ -877,6 +898,7 @@ Your approach depends on available search tools:
NoteTakingToolkit.toolkit_name(),
TerminalToolkit.toolkit_name(),
],
toolkits_to_register_agent=[web_toolkit_for_agent_registration],
)
@ -908,10 +930,10 @@ async def document_agent(options: Chat):
*terminal_toolkit.get_tools(),
*await GoogleDriveMCPToolkit.get_can_use_tools(options.task_id, options.get_bun_env()),
]
if env("EXA_API_KEY") or options.is_cloud():
search_toolkit = SearchToolkit(options.task_id, Agents.document_agent).search_exa
search_toolkit = message_integration.register_functions([search_toolkit])
tools.extend(search_toolkit)
# if env("EXA_API_KEY") or options.is_cloud():
# search_toolkit = SearchToolkit(options.task_id, Agents.document_agent).search_exa
# search_toolkit = message_integration.register_functions([search_toolkit])
# tools.extend(search_toolkit)
system_message = f"""
<role>
You are a Documentation Specialist, responsible for creating, modifying, and
@ -1140,10 +1162,10 @@ def multi_modal_agent(options: Chat):
audio_analysis_toolkit = message_integration.register_toolkits(audio_analysis_toolkit)
tools.extend(audio_analysis_toolkit.get_tools())
if env("EXA_API_KEY") or options.is_cloud():
search_toolkit = SearchToolkit(options.task_id, Agents.multi_modal_agent).search_exa
search_toolkit = message_integration.register_functions([search_toolkit])
tools.extend(search_toolkit)
# if env("EXA_API_KEY") or options.is_cloud():
# search_toolkit = SearchToolkit(options.task_id, Agents.multi_modal_agent).search_exa
# search_toolkit = message_integration.register_functions([search_toolkit])
# tools.extend(search_toolkit)
system_message = f"""
<role>
@ -1272,8 +1294,8 @@ async def social_medium_agent(options: Chat):
# *DiscordToolkit(options.task_id).get_tools(), # Not supported temporarily
# *GoogleSuiteToolkit(options.task_id).get_tools(), # Not supported temporarily
]
if env("EXA_API_KEY") or options.is_cloud():
tools.append(FunctionTool(SearchToolkit(options.task_id, Agents.social_medium_agent).search_exa))
# if env("EXA_API_KEY") or options.is_cloud():
# tools.append(FunctionTool(SearchToolkit(options.task_id, Agents.social_medium_agent).search_exa))
return agent_model(
Agents.social_medium_agent,
BaseMessage.make_assistant_message(

View file

@ -1,8 +1,10 @@
import asyncio
from functools import wraps
from inspect import iscoroutinefunction
from inspect import iscoroutinefunction, getmembers, ismethod, signature
import json
from typing import Any, Callable
from typing import Any, Callable, Type, TypeVar
import threading
from concurrent.futures import ThreadPoolExecutor
from loguru import logger
from app.service.task import (
@ -14,6 +16,38 @@ from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from app.service.task import process_task
def _safe_put_queue(task_lock, data):
"""Safely put data to the queue, handling both sync and async contexts"""
try:
# Try to get current event loop
loop = asyncio.get_running_loop()
# We're in an async context, create a task
task = asyncio.create_task(task_lock.put_queue(data))
if hasattr(task_lock, "add_background_task"):
task_lock.add_background_task(task)
except RuntimeError:
# No running event loop, we need to handle this differently
try:
# Create a new event loop in a separate thread to avoid conflicts
def run_in_thread():
try:
# Create a new event loop for this thread
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
try:
new_loop.run_until_complete(task_lock.put_queue(data))
finally:
new_loop.close()
except Exception as e:
logger.error(f"[listen_toolkit] Failed to send data in thread: {e}")
# Run in a separate thread to avoid blocking
thread = threading.Thread(target=run_in_thread, daemon=True)
thread.start()
except Exception as e:
logger.error(f"[listen_toolkit] Failed to send data to queue: {e}")
def listen_toolkit(
wrap_method: Callable[..., Any] | None = None,
inputs: Callable[..., str] | None = None,
@ -27,6 +61,11 @@ def listen_toolkit(
@wraps(wrap)
async def async_wrapper(*args, **kwargs):
toolkit: AbstractToolkit = args[0]
# Check if api_task_id exists
if not hasattr(toolkit, 'api_task_id'):
logger.warning(f"[listen_toolkit] {toolkit.__class__.__name__} missing api_task_id, calling method directly")
return await func(*args, **kwargs)
task_lock = get_task_lock(toolkit.api_task_id)
if inputs is not None:
@ -40,19 +79,23 @@ def listen_toolkit(
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
args_str = f"{args_str}, {kwargs_str}" if args_str else kwargs_str
# Truncate args_str if too long
MAX_ARGS_LENGTH = 500
if len(args_str) > MAX_ARGS_LENGTH:
args_str = args_str[:MAX_ARGS_LENGTH] + f"... (truncated, total length: {len(args_str)} chars)"
toolkit_name = toolkit.toolkit_name()
method_name = func.__name__.replace("_", " ")
await task_lock.put_queue(
ActionActivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": args_str,
},
)
activate_data = ActionActivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": args_str,
},
)
await task_lock.put_queue(activate_data)
error = None
res = None
try:
@ -70,21 +113,26 @@ def listen_toolkit(
res_msg = json.dumps(res, ensure_ascii=False)
except TypeError:
# Handle cases where res contains non-serializable objects (like coroutines)
res_msg = str(res)
res_str = str(res)
# Truncate very long outputs to avoid flooding logs
MAX_LENGTH = 500
if len(res_str) > MAX_LENGTH:
res_msg = res_str[:MAX_LENGTH] + f"... (truncated, total length: {len(res_str)} chars)"
else:
res_msg = res_str
else:
res_msg = str(error)
await task_lock.put_queue(
ActionDeactivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": res_msg,
},
)
deactivate_data = ActionDeactivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": res_msg,
},
)
await task_lock.put_queue(deactivate_data)
if error is not None:
raise error
return res
@ -96,6 +144,11 @@ def listen_toolkit(
@wraps(wrap)
def sync_wrapper(*args, **kwargs):
toolkit: AbstractToolkit = args[0]
# Check if api_task_id exists
if not hasattr(toolkit, 'api_task_id'):
logger.warning(f"[listen_toolkit] {toolkit.__class__.__name__} missing api_task_id, calling method directly")
return func(*args, **kwargs)
task_lock = get_task_lock(toolkit.api_task_id)
if inputs is not None:
@ -109,27 +162,26 @@ def listen_toolkit(
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
args_str = f"{args_str}, {kwargs_str}" if args_str else kwargs_str
# Truncate args_str if too long
MAX_ARGS_LENGTH = 500
if len(args_str) > MAX_ARGS_LENGTH:
args_str = args_str[:MAX_ARGS_LENGTH] + f"... (truncated, total length: {len(args_str)} chars)"
toolkit_name = toolkit.toolkit_name()
method_name = func.__name__.replace("_", " ")
task = asyncio.create_task(
task_lock.put_queue(
ActionActivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": args_str,
},
)
)
activate_data = ActionActivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": args_str,
},
)
if hasattr(task_lock, "add_background_task"):
task_lock.add_background_task(task)
_safe_put_queue(task_lock, activate_data)
error = None
res = None
try:
logger.debug(f"Executing toolkit method: {toolkit_name}.{method_name} for agent '{toolkit.agent_name}'")
res = func(*args, **kwargs)
# Safety check: if the result is a coroutine, we need to await it
if asyncio.iscoroutine(res):
@ -150,25 +202,26 @@ def listen_toolkit(
res_msg = json.dumps(res, ensure_ascii=False)
except TypeError:
# Handle cases where res contains non-serializable objects (like coroutines)
res_msg = str(res)
res_str = str(res)
# Truncate very long outputs to avoid flooding logs
MAX_LENGTH = 500
if len(res_str) > MAX_LENGTH:
res_msg = res_str[:MAX_LENGTH] + f"... (truncated, total length: {len(res_str)} chars)"
else:
res_msg = res_str
else:
res_msg = str(error)
task = asyncio.create_task(
task_lock.put_queue(
ActionDeactivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": res_msg,
},
)
)
deactivate_data = ActionDeactivateToolkitData(
data={
"agent_name": toolkit.agent_name,
"process_task_id": process_task.get(""),
"toolkit_name": toolkit_name,
"method_name": method_name,
"message": res_msg,
},
)
if hasattr(task_lock, "add_background_task"):
task_lock.add_background_task(task)
_safe_put_queue(task_lock, deactivate_data)
if error is not None:
raise error
return res
@ -176,3 +229,81 @@ def listen_toolkit(
return sync_wrapper
return decorator
T = TypeVar('T')
# Methods that should not be wrapped by auto_listen_toolkit
# These are utility/helper methods that don't perform actual tool operations
EXCLUDED_METHODS = {
'get_tools', # Tool enumeration
'get_can_use_tools', # Tool filtering
'toolkit_name', # Metadata getter
'run_mcp_server', # MCP server initialization
'model_dump', # Pydantic model serialization
'model_dump_json', # Pydantic model serialization
'dict', # Pydantic legacy dict method
'json', # Pydantic legacy json method
'copy', # Object copying
'update', # Object update
}
def auto_listen_toolkit(base_toolkit_class: Type[T]) -> Callable[[Type[T]], Type[T]]:
"""
Class decorator that automatically wraps all public methods from the base toolkit
with the @listen_toolkit decorator.
Excluded methods (not wrapped):
- get_tools, get_can_use_tools: Tool enumeration/filtering
- toolkit_name: Metadata getter
- run_mcp_server: MCP server initialization
- Pydantic serialization methods: model_dump, model_dump_json, dict, json
- Object utility methods: copy, update
These methods are typically called during initialization or for metadata,
and should not trigger activate/deactivate events.
Usage:
@auto_listen_toolkit(BaseNoteTakingToolkit)
class NoteTakingToolkit(BaseNoteTakingToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent
"""
def class_decorator(cls: Type[T]) -> Type[T]:
base_methods = {}
for name in dir(base_toolkit_class):
# Skip private methods and excluded helper methods
if not name.startswith('_') and name not in EXCLUDED_METHODS:
attr = getattr(base_toolkit_class, name)
if callable(attr):
base_methods[name] = attr
for method_name, base_method in base_methods.items():
if method_name in cls.__dict__:
continue
sig = signature(base_method)
def create_wrapper(method_name: str, base_method: Callable) -> Callable:
if iscoroutinefunction(base_method):
async def async_method_wrapper(self, *args, **kwargs):
return await getattr(super(cls, self), method_name)(*args, **kwargs)
async_method_wrapper.__name__ = method_name
async_method_wrapper.__signature__ = sig
return async_method_wrapper
else:
def sync_method_wrapper(self, *args, **kwargs):
return getattr(super(cls, self), method_name)(*args, **kwargs)
sync_method_wrapper.__name__ = method_name
sync_method_wrapper.__signature__ = sig
return sync_method_wrapper
wrapper = create_wrapper(method_name, base_method)
decorated_method = listen_toolkit(base_method)(wrapper)
setattr(cls, method_name, decorated_method)
return cls
return class_decorator

View file

@ -2,11 +2,13 @@ import datetime
from camel.agents.chat_agent import AsyncStreamingChatAgentResponse
from camel.societies.workforce.single_agent_worker import SingleAgentWorker as BaseSingleAgentWorker
from camel.tasks.task import Task, TaskState, is_task_result_insufficient
from loguru import logger
from app.utils.agent import ListenChatAgent
from camel.societies.workforce.prompts import PROCESS_TASK_PROMPT
from colorama import Fore
from camel.societies.workforce.utils import TaskResult
from camel.utils.context_utils import ContextUtility
class SingleAgentWorker(BaseSingleAgentWorker):
@ -19,6 +21,8 @@ class SingleAgentWorker(BaseSingleAgentWorker):
pool_max_size: int = 10,
auto_scale_pool: bool = True,
use_structured_output_handler: bool = True,
context_utility: ContextUtility | None = None,
enable_workflow_memory: bool = False,
) -> None:
super().__init__(
description=description,
@ -28,6 +32,8 @@ class SingleAgentWorker(BaseSingleAgentWorker):
pool_max_size=pool_max_size,
auto_scale_pool=auto_scale_pool,
use_structured_output_handler=use_structured_output_handler,
context_utility=context_utility,
enable_workflow_memory=enable_workflow_memory,
)
self.worker = worker # change type hint
@ -130,8 +136,28 @@ class SingleAgentWorker(BaseSingleAgentWorker):
usage_info = response.info.get("usage") or response.info.get("token_usage")
total_tokens = usage_info.get("total_tokens", 0) if usage_info else 0
# collect conversation from working agent to
# accumulator for workflow memory
# Only transfer memory if workflow memory is enabled
if self.enable_workflow_memory:
accumulator = self._get_conversation_accumulator()
# transfer all memory records from working agent to accumulator
try:
# retrieve all context records from the working agent
work_records = worker_agent.memory.retrieve()
# write these records to the accumulator's memory
memory_records = [record.memory_record for record in work_records]
accumulator.memory.write_records(memory_records)
logger.debug(f"Transferred {len(memory_records)} memory records to accumulator")
except Exception as e:
logger.warning(f"Failed to transfer conversation to accumulator: {e}")
except Exception as e:
print(f"{Fore.RED}Error processing task {task.id}: {type(e).__name__}: {e}{Fore.RESET}")
logger.error(f"Error processing task {task.id}: {type(e).__name__}: {e}")
# Store error information in task result
task.result = f"{type(e).__name__}: {e!s}"
return TaskState.FAILED
@ -172,9 +198,12 @@ class SingleAgentWorker(BaseSingleAgentWorker):
print(f"======\n{Fore.GREEN}Response from {self}:{Fore.RESET}")
logger.info(f"Response from {self}:")
if not self.use_structured_output_handler:
# Handle native structured output parsing
if task_result is None:
logger.error("Error in worker step execution: Invalid task result")
print(f"{Fore.RED}Error in worker step execution: Invalid task result{Fore.RESET}")
task_result = TaskResult(
content="Failed to generate valid task result.",
@ -186,12 +215,17 @@ class SingleAgentWorker(BaseSingleAgentWorker):
f"\n{color}{task_result.content}{Fore.RESET}\n======", # type: ignore[union-attr]
)
if task_result.failed: # type: ignore[union-attr]
logger.error(f"{task_result.content}") # type: ignore[union-attr]
else:
logger.info(f"{task_result.content}") # type: ignore[union-attr]
task.result = task_result.content # type: ignore[union-attr]
if task_result.failed: # type: ignore[union-attr]
return TaskState.FAILED
if is_task_result_insufficient(task):
print(f"{Fore.RED}Task {task.id}: Content validation failed - task marked as failed{Fore.RESET}")
logger.warning(f"Task {task.id}: Content validation failed - task marked as failed")
return TaskState.FAILED
return TaskState.DONE

View file

@ -4,10 +4,11 @@ from camel.toolkits import AudioAnalysisToolkit as BaseAudioAnalysisToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseAudioAnalysisToolkit)
class AudioAnalysisToolkit(BaseAudioAnalysisToolkit, AbstractToolkit):
agent_name: str = Agents.multi_modal_agent
@ -23,14 +24,3 @@ class AudioAnalysisToolkit(BaseAudioAnalysisToolkit, AbstractToolkit):
cache_dir = env("file_save_path", os.path.expanduser("~/.eigent/tmp/"))
super().__init__(cache_dir, transcribe_model, audio_reasoning_model, timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseAudioAnalysisToolkit.audio2text,
lambda _, audio_path, question: f"transcribe audio from {audio_path} and ask question: {question}",
)
def ask_question_about_audio(self, audio_path: str, question: str) -> str:
return super().ask_question_about_audio(audio_path, question)
@listen_toolkit(BaseAudioAnalysisToolkit.audio2text)
def audio2text(self, audio_path: str) -> str:
return super().audio2text(audio_path)

View file

@ -1,10 +1,11 @@
from typing import List, Literal
from camel.toolkits import CodeExecutionToolkit as BaseCodeExecutionToolkit, FunctionTool
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseCodeExecutionToolkit)
class CodeExecutionToolkit(BaseCodeExecutionToolkit, AbstractToolkit):
agent_name: str = Agents.developer_agent
@ -21,18 +22,6 @@ class CodeExecutionToolkit(BaseCodeExecutionToolkit, AbstractToolkit):
self.api_task_id = api_task_id
super().__init__(sandbox, verbose, unsafe_mode, import_white_list, require_confirm, timeout)
@listen_toolkit(
BaseCodeExecutionToolkit.execute_code,
)
def execute_code(self, code: str, code_type: str = "python") -> str:
return super().execute_code(code, code_type)
@listen_toolkit(
BaseCodeExecutionToolkit.execute_command,
)
def execute_command(self, command: str) -> str | tuple[str, str]:
return super().execute_command(command)
def get_tools(self) -> List[FunctionTool]:
return [
FunctionTool(self.execute_code),

View file

@ -1,10 +1,11 @@
from camel.toolkits import Crawl4AIToolkit as BaseCrawl4AIToolkit
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseCrawl4AIToolkit)
class Crawl4AIToolkit(BaseCrawl4AIToolkit, AbstractToolkit):
agent_name: str = Agents.search_agent
@ -12,18 +13,5 @@ class Crawl4AIToolkit(BaseCrawl4AIToolkit, AbstractToolkit):
self.api_task_id = api_task_id
super().__init__(timeout)
# async def _get_client(self):
# r"""Get or create the AsyncWebCrawler client."""
# if self._client is None:
# from crawl4ai import AsyncWebCrawler
# self._client = AsyncWebCrawler(use_managed_browser=True)
# await self._client.__aenter__()
# return self._client
@listen_toolkit(BaseCrawl4AIToolkit.scrape)
async def scrape(self, url: str) -> str:
return await super().scrape(url)
def toolkit_name(self) -> str:
return "Crawl Toolkit"

View file

@ -3,10 +3,11 @@ from camel.toolkits import ExcelToolkit as BaseExcelToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseExcelToolkit)
class ExcelToolkit(BaseExcelToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent
@ -20,7 +21,3 @@ class ExcelToolkit(BaseExcelToolkit, AbstractToolkit):
if working_directory is None:
working_directory = env("file_save_path", os.path.expanduser("~/Downloads"))
super().__init__(timeout=timeout, working_directory=working_directory)
@listen_toolkit(BaseExcelToolkit.extract_excel_content)
def extract_excel_content(self, document_path: str) -> str:
return super().extract_excel_content(document_path)

View file

@ -5,10 +5,11 @@ from camel.toolkits import FileToolkit as BaseFileToolkit
from app.component.environment import env
from app.service.task import process_task
from app.service.task import ActionWriteFileData, Agents, get_task_lock
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseFileToolkit)
class FileToolkit(BaseFileToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent
@ -54,15 +55,3 @@ class FileToolkit(BaseFileToolkit, AbstractToolkit):
)
)
return res
@listen_toolkit(
BaseFileToolkit.read_file,
)
def read_file(self, file_paths: str | list[str]) -> str | dict[str, str]:
return super().read_file(file_paths)
@listen_toolkit(
BaseFileToolkit.edit_file,
)
def edit_file(self, file_path: str, old_content: str, new_content: str) -> str:
return super().edit_file(file_path, old_content, new_content)

View file

@ -3,10 +3,11 @@ from camel.toolkits import GithubToolkit as BaseGithubToolkit
from camel.toolkits.function_tool import FunctionTool
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseGithubToolkit)
class GithubToolkit(BaseGithubToolkit, AbstractToolkit):
agent_name: str = Agents.developer_agent
@ -19,86 +20,6 @@ class GithubToolkit(BaseGithubToolkit, AbstractToolkit):
super().__init__(access_token, timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseGithubToolkit.create_pull_request,
lambda _,
repo_name,
file_path,
new_content,
pr_title,
body,
branch_name: f"Create PR in {repo_name} for {file_path} with title '{pr_title}', branch '{branch_name}', content '{new_content}'",
)
def create_pull_request(
self,
repo_name: str,
file_path: str,
new_content: str,
pr_title: str,
body: str,
branch_name: str,
) -> str:
return super().create_pull_request(repo_name, file_path, new_content, pr_title, body, branch_name)
@listen_toolkit(
BaseGithubToolkit.get_issue_list,
lambda _, repo_name, state="all": f"Get issue list from {repo_name} with state '{state}'",
lambda issues: f"Retrieved {len(issues)} issues",
)
def get_issue_list(
self, repo_name: str, state: Literal["open", "closed", "all"] = "all"
) -> list[dict[str, object]]:
return super().get_issue_list(repo_name, state)
@listen_toolkit(
BaseGithubToolkit.get_issue_content,
lambda _, repo_name, issue_number: f"Get content of issue {issue_number} from {repo_name}",
)
def get_issue_content(self, repo_name: str, issue_number: int) -> str:
return super().get_issue_content(repo_name, issue_number)
@listen_toolkit(
BaseGithubToolkit.get_pull_request_list,
lambda _, repo_name, state="all": f"Get pull request list from {repo_name} with state '{state}'",
lambda prs: f"Retrieved {len(prs)} pull requests",
)
def get_pull_request_list(
self, repo_name: str, state: Literal["open", "closed", "all"] = "all"
) -> list[dict[str, object]]:
return super().get_pull_request_list(repo_name, state)
@listen_toolkit(
BaseGithubToolkit.get_pull_request_code,
lambda _, repo_name, pr_number: f"Get code for pull request {pr_number} in {repo_name}",
lambda code: f"Retrieved {len(code)} code files",
)
def get_pull_request_code(self, repo_name: str, pr_number: int) -> list[dict[str, str]]:
return super().get_pull_request_code(repo_name, pr_number)
@listen_toolkit(
BaseGithubToolkit.get_pull_request_comments,
lambda _, repo_name, pr_number: f"Get comments for pull request {pr_number} in {repo_name}",
lambda comments: f"Retrieved {len(comments)} comments",
)
def get_pull_request_comments(self, repo_name: str, pr_number: int) -> list[dict[str, str]]:
return super().get_pull_request_comments(repo_name, pr_number)
@listen_toolkit(
BaseGithubToolkit.get_all_file_paths,
lambda _, repo_name, path="": f"Get all file paths from {repo_name}, path '{path}'",
lambda paths: f"Retrieved {len(paths)} file paths",
)
def get_all_file_paths(self, repo_name: str, path: str = "") -> list[str]:
return super().get_all_file_paths(repo_name, path)
@listen_toolkit(
BaseGithubToolkit.retrieve_file_content,
lambda _, repo_name, file_path: f"Retrieve content of file {file_path} from {repo_name}",
lambda content: f"Retrieved content of length {len(content)}",
)
def retrieve_file_content(self, repo_name: str, file_path: str) -> str:
return super().retrieve_file_content(repo_name, file_path)
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
if env("GITHUB_ACCESS_TOKEN"):

View file

@ -1,11 +1,12 @@
from typing import Any, Dict, List
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from camel.toolkits import GoogleCalendarToolkit as BaseGoogleCalendarToolkit
@auto_listen_toolkit(BaseGoogleCalendarToolkit)
class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit):
agent_name: str = Agents.social_medium_agent
@ -13,44 +14,6 @@ class GoogleCalendarToolkit(BaseGoogleCalendarToolkit, AbstractToolkit):
self.api_task_id = api_task_id
super().__init__(timeout)
@listen_toolkit(BaseGoogleCalendarToolkit.create_event)
def create_event(
self,
event_title: str,
start_time: str,
end_time: str,
description: str = "",
location: str = "",
attendees_email: List[str] | None = None,
timezone: str = "UTC",
) -> Dict[str, Any]:
return super().create_event(event_title, start_time, end_time, description, location, attendees_email, timezone)
@listen_toolkit(BaseGoogleCalendarToolkit.get_events)
def get_events(self, max_results: int = 10, time_min: str | None = None) -> List[Dict[str, Any]] | Dict[str, Any]:
return super().get_events(max_results, time_min)
@listen_toolkit(BaseGoogleCalendarToolkit.update_event)
def update_event(
self,
event_id: str,
event_title: str | None = None,
start_time: str | None = None,
end_time: str | None = None,
description: str | None = None,
location: str | None = None,
attendees_email: List[str] | None = None,
) -> Dict[str, Any]:
return super().update_event(event_id, event_title, start_time, end_time, description, location, attendees_email)
@listen_toolkit(BaseGoogleCalendarToolkit.delete_event)
def delete_event(self, event_id: str) -> str:
return super().delete_event(event_id)
@listen_toolkit(BaseGoogleCalendarToolkit.get_calendar_details)
def get_calendar_details(self) -> Dict[str, Any]:
return super().get_calendar_details()
@classmethod
def get_can_use_tools(cls, api_task_id: str):
if env("GOOGLE_CLIENT_ID") and env("GOOGLE_CLIENT_SECRET"):

View file

@ -3,12 +3,13 @@ from camel.toolkits.base import BaseToolkit
from loguru import logger
from camel.toolkits.function_tool import FunctionTool
from app.service.task import Action, ActionAskData, ActionNoticeData, get_task_lock
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from app.service.task import process_task
# Rewrite HumanToolkit because the system's user interaction was using console, but in electron we cannot use console. Changed to use SSE response to let frontend show dialog for user interaction
@auto_listen_toolkit(BaseToolkit)
class HumanToolkit(BaseToolkit, AbstractToolkit):
r"""A class representing a toolkit for human interaction.
Note:

View file

@ -16,7 +16,7 @@ from loguru import logger
from app.component.environment import env
from app.exception.exception import ProgramException
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@ -124,6 +124,7 @@ class BrowserSession(BaseHybridBrowserSession):
break
@auto_listen_toolkit(BaseHybridBrowserToolkit)
class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
agent_name: str = Agents.search_agent
@ -224,14 +225,6 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
self._agent: PlaywrightLLMAgent | None = None
self._unified_script = self._load_unified_analyzer()
@listen_toolkit(BaseHybridBrowserToolkit.browser_open)
async def browser_open(self) -> Dict[str, str]:
return await super().browser_open()
@listen_toolkit(BaseHybridBrowserToolkit.browser_close)
async def browser_close(self) -> str:
return await super().browser_close()
@listen_toolkit(BaseHybridBrowserToolkit.browser_visit_page, lambda _, url: url)
async def browser_visit_page(self, url: str) -> Dict[str, Any]:
r"""Navigates to a URL.
@ -282,66 +275,6 @@ class HybridBrowserPythonToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
return {"result": nav_result, "snapshot": snapshot, **tab_info}
@listen_toolkit(BaseHybridBrowserToolkit.browser_back)
async def browser_back(self) -> Dict[str, Any]:
return await super().browser_back()
@listen_toolkit(BaseHybridBrowserToolkit.browser_forward)
async def browser_forward(self) -> Dict[str, Any]:
return await super().browser_forward()
@listen_toolkit(BaseHybridBrowserToolkit.browser_click)
async def browser_click(self, *, ref: str) -> Dict[str, Any]:
return await super().browser_click(ref=ref)
@listen_toolkit(BaseHybridBrowserToolkit.browser_type)
async def browser_type(self, *, ref: str, text: str) -> Dict[str, Any]:
return await super().browser_type(ref=ref, text=text)
@listen_toolkit(BaseHybridBrowserToolkit.browser_switch_tab)
async def browser_switch_tab(self, *, tab_id: str) -> Dict[str, Any]:
return await super().browser_switch_tab(tab_id=tab_id)
@listen_toolkit(BaseHybridBrowserToolkit.browser_select)
async def browser_select(self, *, ref: str, value: str) -> Dict[str, str]:
return await super().browser_select(ref=ref, value=value)
@listen_toolkit(BaseHybridBrowserToolkit.browser_scroll)
async def browser_scroll(self, *, direction: str, amount: int) -> Dict[str, str]:
return await super().browser_scroll(direction=direction, amount=amount)
@listen_toolkit(BaseHybridBrowserToolkit.browser_wait_user)
async def browser_wait_user(self, timeout_sec: float | None = None) -> Dict[str, str]:
return await super().browser_wait_user(timeout_sec)
@listen_toolkit(BaseHybridBrowserToolkit.browser_enter)
async def browser_enter(self) -> Dict[str, str]:
return await super().browser_enter()
@listen_toolkit(BaseHybridBrowserToolkit.browser_solve_task)
async def browser_solve_task(self, task_prompt: str, start_url: str, max_steps: int = 15) -> str:
return await super().browser_solve_task(task_prompt, start_url, max_steps)
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_page_snapshot)
async def browser_get_page_snapshot(self) -> str:
return await super().browser_get_page_snapshot()
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
async def browser_get_som_screenshot(self):
return await super().browser_get_som_screenshot()
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_page_links)
async def browser_get_page_links(self, *, ref: List[str]) -> Dict[str, Any]:
return await super().browser_get_page_links(ref=ref)
@listen_toolkit(BaseHybridBrowserToolkit.browser_close_tab)
async def browser_close_tab(self, *, tab_id: str) -> Dict[str, Any]:
return await super().browser_close_tab(tab_id=tab_id)
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_tab_info)
async def browser_get_tab_info(self) -> Dict[str, Any]:
return await super().browser_get_tab_info()
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
browser = HybridBrowserPythonToolkit(

View file

@ -16,7 +16,7 @@ from camel.toolkits.hybrid_browser_toolkit.ws_wrapper import WebSocketBrowserWra
from app.component.command import bun, uv
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@ -45,8 +45,13 @@ class WebSocketBrowserWrapper(BaseWebSocketBrowserWrapper):
future.set_result(response)
logger.debug(f"Processed response for message {message_id}")
else:
# Log unexpected messages
logger.warning(f"Received unexpected message: {response}")
message_summary = {
"id": response.get("id"),
"success": response.get("success"),
"has_result": "result" in response,
"result_type": type(response.get("result")).__name__ if "result" in response else None
}
logger.debug(f"Received unexpected message: {message_summary}")
except asyncio.CancelledError:
disconnect_reason = "Receive loop cancelled"
@ -210,6 +215,7 @@ class WebSocketConnectionPool:
websocket_connection_pool = WebSocketConnectionPool()
@auto_listen_toolkit(BaseHybridBrowserToolkit)
class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
agent_name: str = Agents.search_agent
@ -240,7 +246,10 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
cdp_keep_current_page: bool = False,
full_visual_mode: bool = False,
) -> None:
logger.info(f"[HybridBrowserToolkit] Initializing with api_task_id: {api_task_id}")
self.api_task_id = api_task_id
logger.debug(f"[HybridBrowserToolkit] api_task_id set to: {self.api_task_id}")
logger.debug(f"[HybridBrowserToolkit] Calling super().__init__ with session_id: {session_id}")
super().__init__(
headless=headless,
user_data_dir=user_data_dir,
@ -264,16 +273,20 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
cdp_keep_current_page=cdp_keep_current_page,
full_visual_mode=full_visual_mode,
)
logger.info(f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}")
async def _ensure_ws_wrapper(self):
"""Ensure WebSocket wrapper is initialized using connection pool."""
logger.debug(f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}")
global websocket_connection_pool
# Get session ID from config or use default
session_id = self._ws_config.get("session_id", "default")
logger.debug(f"[HybridBrowserToolkit] Using session_id: {session_id}")
# Get or create connection from pool
self._ws_wrapper = await websocket_connection_pool.get_connection(session_id, self._ws_config)
logger.info(f"[HybridBrowserToolkit] WebSocket wrapper initialized for session: {session_id}")
# Additional health check
if self._ws_wrapper.websocket is None:
@ -336,74 +349,3 @@ class HybridBrowserToolkit(BaseHybridBrowserToolkit, AbstractToolkit):
if hasattr(self, "_ws_wrapper") and self._ws_wrapper:
session_id = self._ws_config.get("session_id", "default")
logger.debug(f"HybridBrowserToolkit for session {session_id} is being garbage collected")
@listen_toolkit(BaseHybridBrowserToolkit.browser_open)
async def browser_open(self) -> Dict[str, Any]:
return await super().browser_open()
@listen_toolkit(BaseHybridBrowserToolkit.browser_close)
async def browser_close(self) -> str:
return await super().browser_close()
@listen_toolkit(BaseHybridBrowserToolkit.browser_visit_page)
async def browser_visit_page(self, url: str) -> Dict[str, Any]:
logger.debug(f"browser_visit_page called with URL: {url}")
try:
result = await super().browser_visit_page(url)
logger.debug(f"browser_visit_page succeeded for URL: {url}")
return result
except Exception as e:
logger.error(f"browser_visit_page failed for URL {url}: {type(e).__name__}: {e}")
raise
@listen_toolkit(BaseHybridBrowserToolkit.browser_back)
async def browser_back(self) -> Dict[str, Any]:
return await super().browser_back()
@listen_toolkit(BaseHybridBrowserToolkit.browser_forward)
async def browser_forward(self) -> Dict[str, Any]:
return await super().browser_forward()
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_page_snapshot)
async def browser_get_page_snapshot(self) -> str:
return await super().browser_get_page_snapshot()
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_som_screenshot)
async def browser_get_som_screenshot(self, read_image: bool = False, instruction: str | None = None) -> str:
return await super().browser_get_som_screenshot(read_image, instruction)
@listen_toolkit(BaseHybridBrowserToolkit.browser_click)
async def browser_click(self, *, ref: str) -> Dict[str, Any]:
return await super().browser_click(ref=ref)
@listen_toolkit(BaseHybridBrowserToolkit.browser_type)
async def browser_type(self, *, ref: str, text: str) -> Dict[str, Any]:
return await super().browser_type(ref=ref, text=text)
@listen_toolkit(BaseHybridBrowserToolkit.browser_select)
async def browser_select(self, *, ref: str, value: str) -> Dict[str, Any]:
return await super().browser_select(ref=ref, value=value)
@listen_toolkit(BaseHybridBrowserToolkit.browser_scroll)
async def browser_scroll(self, *, direction: str, amount: int = 500) -> Dict[str, Any]:
return await super().browser_scroll(direction=direction, amount=amount)
@listen_toolkit(BaseHybridBrowserToolkit.browser_enter)
async def browser_enter(self) -> Dict[str, Any]:
return await super().browser_enter()
@listen_toolkit(BaseHybridBrowserToolkit.browser_wait_user)
async def browser_wait_user(self, timeout_sec: float | None = None) -> Dict[str, Any]:
return await super().browser_wait_user(timeout_sec)
@listen_toolkit(BaseHybridBrowserToolkit.browser_switch_tab)
async def browser_switch_tab(self, *, tab_id: str) -> Dict[str, Any]:
return await super().browser_switch_tab(tab_id=tab_id)
@listen_toolkit(BaseHybridBrowserToolkit.browser_close_tab)
async def browser_close_tab(self, *, tab_id: str) -> Dict[str, Any]:
return await super().browser_close_tab(tab_id=tab_id)
@listen_toolkit(BaseHybridBrowserToolkit.browser_get_tab_info)
async def browser_get_tab_info(self) -> Dict[str, Any]:
return await super().browser_get_tab_info()

View file

@ -2,10 +2,11 @@ from camel.models import BaseModelBackend
from camel.toolkits import ImageAnalysisToolkit as BaseImageAnalysisToolkit
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseImageAnalysisToolkit)
class ImageAnalysisToolkit(BaseImageAnalysisToolkit, AbstractToolkit):
agent_name: str = Agents.multi_modal_agent
@ -17,24 +18,3 @@ class ImageAnalysisToolkit(BaseImageAnalysisToolkit, AbstractToolkit):
):
super().__init__(model, timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseImageAnalysisToolkit.image_to_text,
lambda _,
image_path,
sys_prompt: f"transcribe image from {image_path} and ask sys_prompt: {sys_prompt}",
)
def image_to_text(self, image_path: str, sys_prompt: str | None = None) -> str:
return super().image_to_text(image_path, sys_prompt)
@listen_toolkit(
BaseImageAnalysisToolkit.ask_question_about_image,
lambda _,
image_path,
question,
sys_prompt: f"transcribe image from {image_path} and ask question: {question} with sys_prompt: {sys_prompt}",
)
def ask_question_about_image(
self, image_path: str, question: str, sys_prompt: str | None = None
) -> str:
return super().ask_question_about_image(image_path, question, sys_prompt)

View file

@ -2,10 +2,11 @@ from camel.toolkits import LinkedInToolkit as BaseLinkedInToolkit
from camel.toolkits.function_tool import FunctionTool
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseLinkedInToolkit)
class LinkedInToolkit(BaseLinkedInToolkit, AbstractToolkit):
agent_name: str = Agents.social_medium_agent
@ -13,27 +14,6 @@ class LinkedInToolkit(BaseLinkedInToolkit, AbstractToolkit):
super().__init__(timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseLinkedInToolkit.create_post,
lambda _, text: f"create a LinkedIn post with text: {text}",
)
def create_post(self, text: str) -> dict:
return super().create_post(text)
@listen_toolkit(
BaseLinkedInToolkit.delete_post,
lambda _, post_id: f"delete LinkedIn post with id: {post_id}",
)
def delete_post(self, post_id: str) -> str:
return super().delete_post(post_id)
@listen_toolkit(
BaseLinkedInToolkit.get_profile,
lambda _, include_id: f"get LinkedIn profile with include_id: {include_id}",
)
def get_profile(self, include_id: bool = False) -> dict:
return super().get_profile(include_id)
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
if env("LINKEDIN_ACCESS_TOKEN"):

View file

@ -2,17 +2,14 @@ from typing import Dict, List
from camel.toolkits import MarkItDownToolkit as BaseMarkItDownToolkit
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseMarkItDownToolkit)
class MarkItDownToolkit(BaseMarkItDownToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent
def __init__(self, api_task_id: str, timeout: float | None = None):
self.api_task_id = api_task_id
super().__init__(timeout)
@listen_toolkit(BaseMarkItDownToolkit.read_files)
def read_files(self, file_paths: List[str]) -> Dict[str, str]:
return super().read_files(file_paths)

View file

@ -5,10 +5,11 @@ from typing import Optional
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseNoteTakingToolkit)
class NoteTakingToolkit(BaseNoteTakingToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent
@ -25,19 +26,3 @@ class NoteTakingToolkit(BaseNoteTakingToolkit, AbstractToolkit):
if working_directory is None:
working_directory = env("file_save_path", os.path.expanduser("~/.eigent/notes")) + "/note.md"
super().__init__(working_directory=working_directory, timeout=timeout)
@listen_toolkit(BaseNoteTakingToolkit.append_note)
def append_note(self, note_name: str, content: str) -> str:
return super().append_note(note_name=note_name, content=content)
@listen_toolkit(BaseNoteTakingToolkit.read_note)
def read_note(self, note_name: Optional[str] = "all_notes") -> str:
return super().read_note(note_name=note_name)
@listen_toolkit(BaseNoteTakingToolkit.create_note)
def create_note(self, note_name: str, content: str, overwrite: bool = False) -> str:
return super().create_note(note_name=note_name, content=content, overwrite=overwrite)
@listen_toolkit(BaseNoteTakingToolkit.list_note)
def list_note(self) -> str:
return super().list_note()

View file

@ -6,6 +6,25 @@ from app.component.environment import env
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from camel.toolkits.mcp_toolkit import MCPToolkit
def _customize_function_parameters(schema: Dict[str, Any]) -> None:
r"""Customize function parameters for specific functions.
This method allows modifying parameter descriptions or other schema
attributes for specific functions.
"""
function_info = schema.get("function", {})
function_name = function_info.get("name", "")
parameters = function_info.get("parameters", {})
properties = parameters.get("properties", {})
required = parameters.get("required", [])
# Modify the notion-create-pages function to make parent optional
if function_name == "notion-create-pages":
required.remove("parent")
parameters["required"] = required
if "parent" in properties:
# Update the parent parameter description
properties["parent"]["description"] = "Optional. " + properties["parent"]["description"]
class NotionMCPToolkit(MCPToolkit, AbstractToolkit):
@ -33,68 +52,7 @@ class NotionMCPToolkit(MCPToolkit, AbstractToolkit):
}
}
}
super().__init__(config_dict=config_dict, timeout=timeout)
def get_tools(self) -> List[FunctionTool]:
r"""Returns a list of tools provided by the NotionMCPToolkit.
Returns:
List[FunctionTool]: List of available tools.
"""
all_tools = []
for client in self.clients:
try:
original_build_schema = client._build_tool_schema
def create_wrapper(orig_func):
def wrapper(mcp_tool):
return self._build_custom_tool_schema(
mcp_tool, orig_func
)
return wrapper
client._build_tool_schema = create_wrapper( # type: ignore[method-assign]
original_build_schema
)
client_tools = client.get_tools()
all_tools.extend(client_tools)
client._build_tool_schema = original_build_schema # type: ignore[method-assign]
except Exception as e:
logger.error(f"Failed to get tools from client: {e}")
return all_tools
def _build_custom_tool_schema(self, mcp_tool, original_build_schema):
r"""Build tool schema with custom modifications."""
schema = original_build_schema(mcp_tool)
self._customize_function_parameters(schema)
return schema
def _customize_function_parameters(self, schema: Dict[str, Any]) -> None:
r"""Customize function parameters for specific functions.
This method allows modifying parameter descriptions or other schema
attributes for specific functions.
"""
function_info = schema.get("function", {})
function_name = function_info.get("name", "")
parameters = function_info.get("parameters", {})
properties = parameters.get("properties", {})
# Modify the notion-create-pages function to make parent optional
if function_name == "notion-create-pages":
if "parent" in properties:
# Update the parent parameter description
properties["parent"]["description"] = (
"Optional. The parent under which the new pages will be created. "
"This can be a page (page_id), a database page (database_id), or "
"a data source/collection under a database (data_source_id). "
"If omitted, the new pages will be created as private pages at the workspace level. "
"Use data_source_id when you have a collection:// URL from the fetch tool."
)
super().__init__(config_dict=config_dict, timeout=timeout)
@classmethod
async def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
@ -104,6 +62,12 @@ class NotionMCPToolkit(MCPToolkit, AbstractToolkit):
await toolkit.connect()
# Use subclass implementation that inlines upstream processing
all_tools = toolkit.get_tools()
tool_schema = [
item.get_openai_tool_schema() for item in all_tools
]
#adjust tool schema
for item in tool_schema:
_customize_function_parameters(item)
for item in all_tools:
setattr(item, "_toolkit_name", cls.__name__)
tools.append(item)

View file

@ -3,10 +3,11 @@ from camel.toolkits import NotionToolkit as BaseNotionToolkit
from camel.toolkits.function_tool import FunctionTool
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseNotionToolkit)
class NotionToolkit(BaseNotionToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent
@ -19,29 +20,6 @@ class NotionToolkit(BaseNotionToolkit, AbstractToolkit):
super().__init__(notion_token, timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseNotionToolkit.list_all_pages,
lambda _: "list all pages in Notion workspace",
lambda result: f"{len(result)} pages found",
)
def list_all_pages(self) -> List[dict]:
return super().list_all_pages()
@listen_toolkit(
BaseNotionToolkit.list_all_users,
lambda _: "list all users in Notion workspace",
lambda result: f"{len(result)} users found",
)
def list_all_users(self) -> List[dict]:
return super().list_all_users()
@listen_toolkit(
BaseNotionToolkit.get_notion_block_text_content,
lambda _, page_id: f"get text content of page with id: {page_id}",
)
def get_notion_block_text_content(self, block_id: str) -> str:
return super().get_notion_block_text_content(block_id)
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> List[FunctionTool]:
if env("NOTION_TOKEN"):

View file

@ -3,11 +3,12 @@ from camel.toolkits import OpenAIImageToolkit as BaseOpenAIImageToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from typing import Literal, Optional, Union, List
@auto_listen_toolkit(BaseOpenAIImageToolkit)
class OpenAIImageToolkit(BaseOpenAIImageToolkit, AbstractToolkit):
agent_name: str = Agents.multi_modal_agent

View file

@ -4,11 +4,12 @@ from camel.toolkits import PPTXToolkit as BasePPTXToolkit
from app.component.environment import env
from app.service.task import ActionWriteFileData, Agents, get_task_lock
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
from app.service.task import process_task
@auto_listen_toolkit(BasePPTXToolkit)
class PPTXToolkit(BasePPTXToolkit, AbstractToolkit):
agent_name: str = Agents.document_agent

View file

@ -4,10 +4,11 @@ from camel.toolkits import PyAutoGUIToolkit as BasePyAutoGUIToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BasePyAutoGUIToolkit)
class PyAutoGUIToolkit(BasePyAutoGUIToolkit, AbstractToolkit):
agent_name: str = Agents.search_agent
@ -21,69 +22,3 @@ class PyAutoGUIToolkit(BasePyAutoGUIToolkit, AbstractToolkit):
screenshots_dir = env("file_save_path", os.path.expanduser("~/Downloads"))
super().__init__(timeout, screenshots_dir)
self.api_task_id = api_task_id
@listen_toolkit(BasePyAutoGUIToolkit.mouse_move, lambda _, x, y: f"mouse move to {x}, {y}")
def mouse_move(self, x: int, y: int) -> str:
return super().mouse_move(x, y)
@listen_toolkit(
BasePyAutoGUIToolkit.mouse_click,
lambda _, button="left", clicks=1, x=None, y=None: f"mouse click {button} {clicks} times at {x}, {y}",
)
def mouse_click(
self,
button: Literal["left", "middle", "right"] = "left",
clicks: int = 1,
x: int | None = None,
y: int | None = None,
) -> str:
return super().mouse_click(button, clicks, x, y)
@listen_toolkit(
BasePyAutoGUIToolkit.keyboard_type,
lambda _, text, interval=0: f"keyboard type {text}, interval {interval}",
)
def keyboard_type(self, text: str, interval: float = 0) -> str:
return super().keyboard_type(text, interval)
@listen_toolkit(BasePyAutoGUIToolkit.take_screenshot)
def take_screenshot(self) -> str:
return super().take_screenshot()
@listen_toolkit(BasePyAutoGUIToolkit.get_mouse_position)
def get_mouse_position(self) -> str:
return super().get_mouse_position()
@listen_toolkit(BasePyAutoGUIToolkit.press_key, lambda _, key: f"press key {key}")
def press_key(self, key: str | list[str]) -> str:
return super().press_key(key)
@listen_toolkit(BasePyAutoGUIToolkit.hotkey, lambda _, keys: f"hotkey {keys}")
def hotkey(self, keys: List[str]) -> str:
return super().hotkey(keys)
@listen_toolkit(
BasePyAutoGUIToolkit.mouse_drag,
lambda _,
start_x,
start_y,
end_x,
end_y,
button="left": f"mouse drag from {start_x}, {start_y} to {end_x}, {end_y} with {button} button",
)
def mouse_drag(
self,
start_x: int,
start_y: int,
end_x: int,
end_y: int,
button: Literal["left", "middle", "right"] = "left",
) -> str:
return super().mouse_drag(start_x, start_y, end_x, end_y, button)
@listen_toolkit(
BasePyAutoGUIToolkit.scroll,
lambda _, scroll_amount, x=None, y=None: f"scroll {scroll_amount} at {x}, {y}",
)
def scroll(self, scroll_amount: int, x: int | None = None, y: int | None = None) -> str:
return super().scroll(scroll_amount, x, y)

View file

@ -3,10 +3,11 @@ from camel.toolkits import RedditToolkit as BaseRedditToolkit
from camel.toolkits.function_tool import FunctionTool
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseRedditToolkit)
class RedditToolkit(BaseRedditToolkit, AbstractToolkit):
agent_name: str = Agents.social_medium_agent
@ -20,47 +21,6 @@ class RedditToolkit(BaseRedditToolkit, AbstractToolkit):
super().__init__(retries, delay, timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseRedditToolkit.collect_top_posts,
lambda _,
subreddit_name,
post_limit=5,
comment_limit=5: f"collect top posts from subreddit: {subreddit_name} with post limit: {post_limit} and comment limit: {comment_limit}",
lambda result: f"top posts collected: {result}",
)
def collect_top_posts(
self, subreddit_name: str, post_limit: int = 5, comment_limit: int = 5
) -> List[Dict[str, Any]] | str:
return super().collect_top_posts(subreddit_name, post_limit, comment_limit)
@listen_toolkit(
BaseRedditToolkit.perform_sentiment_analysis,
lambda _, data: f"perform sentiment analysis on data number: {len(data)}",
lambda result: f"perform analysis result: {result}",
)
def perform_sentiment_analysis(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
return super().perform_sentiment_analysis(data)
@listen_toolkit(
BaseRedditToolkit.track_keyword_discussions,
lambda _,
subreddits,
keywords,
post_limit=10,
comment_limit=10,
sentiment_analysis=False: f"track keyword discussions for subreddits: {subreddits}, keywords: {keywords}",
lambda result: f"track keyword discussions result: {result}",
)
def track_keyword_discussions(
self,
subreddits: List[str],
keywords: List[str],
post_limit: int = 10,
comment_limit: int = 10,
sentiment_analysis: bool = False,
) -> List[Dict[str, Any]] | str:
return super().track_keyword_discussions(subreddits, keywords, post_limit, comment_limit, sentiment_analysis)
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
if env("REDDIT_CLIENT_ID") and env("REDDIT_CLIENT_SECRET") and env("REDDIT_USER_AGENT"):

View file

@ -3,10 +3,11 @@ from camel.toolkits import ScreenshotToolkit as BaseScreenshotToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseScreenshotToolkit)
class ScreenshotToolkit(BaseScreenshotToolkit, AbstractToolkit):
agent_name: str = Agents.developer_agent
@ -15,13 +16,3 @@ class ScreenshotToolkit(BaseScreenshotToolkit, AbstractToolkit):
if working_directory is None:
working_directory = env("file_save_path", os.path.expanduser("~/Downloads"))
super().__init__(working_directory, timeout)
@listen_toolkit(BaseScreenshotToolkit.take_screenshot_and_read_image)
def take_screenshot_and_read_image(
self, filename: str, save_to_file: bool = True, read_image: bool = True, instruction: str | None = None
) -> str:
return super().take_screenshot_and_read_image(filename, save_to_file, read_image, instruction)
@listen_toolkit(BaseScreenshotToolkit.read_image)
def read_image(self, image_path: str, instruction: str = "") -> str:
return super().read_image(image_path, instruction)

View file

@ -5,10 +5,11 @@ import httpx
from loguru import logger
from app.component.environment import env, env_not_empty
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseSearchToolkit)
class SearchToolkit(BaseSearchToolkit, AbstractToolkit):
agent_name: str = Agents.search_agent
@ -50,19 +51,36 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit):
@listen_toolkit(
BaseSearchToolkit.search_google,
lambda _, query, search_type="web": f"with query '{query}' and {search_type} result pages",
lambda _, query, search_type="web", number_of_result_pages=10, start_page=1: f"with query '{query}', {search_type} type, {number_of_result_pages} result pages starting from page {start_page}",
)
def search_google(self, query: str, search_type: str = "web") -> list[dict[str, Any]]:
def search_google(
self,
query: str,
search_type: str = "web",
number_of_result_pages: int = 10,
start_page: int = 1
) -> list[dict[str, Any]]:
if env("GOOGLE_API_KEY") and env("SEARCH_ENGINE_ID"):
return super().search_google(query, search_type)
return super().search_google(query, search_type, number_of_result_pages, start_page)
else:
return self.cloud_search_google(query, search_type)
return self.cloud_search_google(query, search_type, number_of_result_pages, start_page)
def cloud_search_google(self, query: str, search_type):
def cloud_search_google(
self,
query: str,
search_type: str = "web",
number_of_result_pages: int = 10,
start_page: int = 1
):
url = env_not_empty("SERVER_URL")
res = httpx.get(
url + "/proxy/google",
params={"query": query, "search_type": search_type},
params={
"query": query,
"search_type": search_type,
"number_of_result_pages": number_of_result_pages,
"start_page": start_page
},
headers={"api-key": env_not_empty("cloud_api_key")},
)
return res.json()
@ -163,73 +181,73 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit):
# def search_bing(self, query: str) -> dict[str, Any]:
# return super().search_bing(query)
@listen_toolkit(BaseSearchToolkit.search_exa, lambda _, query, *args, **kwargs: f"{query}, {args}, {kwargs}")
def search_exa(
self,
query: str,
search_type: Literal["auto", "neural", "keyword"] = "auto",
category: None
| Literal[
"company",
"research paper",
"news",
"pdf",
"github",
"tweet",
"personal site",
"linkedin profile",
"financial report",
] = None,
include_text: List[str] | None = None,
exclude_text: List[str] | None = None,
use_autoprompt: bool = True,
text: bool = False,
) -> Dict[str, Any]:
if env("EXA_API_KEY"):
res = super().search_exa(query, search_type, category, include_text, exclude_text, use_autoprompt, text)
return res
else:
return self.cloud_search_exa(query, search_type, category, include_text, exclude_text, use_autoprompt, text)
def cloud_search_exa(
self,
query: str,
search_type: Literal["auto", "neural", "keyword"] = "auto",
category: None
| Literal[
"company",
"research paper",
"news",
"pdf",
"github",
"tweet",
"personal site",
"linkedin profile",
"financial report",
] = None,
include_text: List[str] | None = None,
exclude_text: List[str] | None = None,
use_autoprompt: bool = True,
text: bool = False,
):
url = env_not_empty("SERVER_URL")
logger.debug(f">>>>>>>>>>>>>>>>{url}<<<<")
res = httpx.post(
url + "/proxy/exa",
json={
"query": query,
"search_type": search_type,
"category": category,
"include_text": include_text,
"exclude_text": exclude_text,
"use_autoprompt": use_autoprompt,
"text": text,
},
headers={"api-key": env_not_empty("cloud_api_key")},
)
logger.debug(">>>>>>>>>>>>>>>>>")
logger.debug(res)
return res.json()
# @listen_toolkit(BaseSearchToolkit.search_exa, lambda _, query, *args, **kwargs: f"{query}, {args}, {kwargs}")
# def search_exa(
# self,
# query: str,
# search_type: Literal["auto", "neural", "keyword"] = "auto",
# category: None
# | Literal[
# "company",
# "research paper",
# "news",
# "pdf",
# "github",
# "tweet",
# "personal site",
# "linkedin profile",
# "financial report",
# ] = None,
# include_text: List[str] | None = None,
# exclude_text: List[str] | None = None,
# use_autoprompt: bool = True,
# text: bool = False,
# ) -> Dict[str, Any]:
# if env("EXA_API_KEY"):
# res = super().search_exa(query, search_type, category, include_text, exclude_text, use_autoprompt, text)
# return res
# else:
# return self.cloud_search_exa(query, search_type, category, include_text, exclude_text, use_autoprompt, text)
#
# def cloud_search_exa(
# self,
# query: str,
# search_type: Literal["auto", "neural", "keyword"] = "auto",
# category: None
# | Literal[
# "company",
# "research paper",
# "news",
# "pdf",
# "github",
# "tweet",
# "personal site",
# "linkedin profile",
# "financial report",
# ] = None,
# include_text: List[str] | None = None,
# exclude_text: List[str] | None = None,
# use_autoprompt: bool = True,
# text: bool = False,
# ):
# url = env_not_empty("SERVER_URL")
# logger.debug(f">>>>>>>>>>>>>>>>{url}<<<<")
# res = httpx.post(
# url + "/proxy/exa",
# json={
# "query": query,
# "search_type": search_type,
# "category": category,
# "include_text": include_text,
# "exclude_text": exclude_text,
# "use_autoprompt": use_autoprompt,
# "text": text,
# },
# headers={"api-key": env_not_empty("cloud_api_key")},
# )
# logger.debug(">>>>>>>>>>>>>>>>>")
# logger.debug(res)
# return res.json()
# @listen_toolkit(
# BaseSearchToolkit.search_alibaba_tongxiao,
@ -289,12 +307,12 @@ class SearchToolkit(BaseSearchToolkit, AbstractToolkit):
# if env("BOCHA_API_KEY"):
# tools.append(FunctionTool(search_toolkit.search_bocha))
if env("EXA_API_KEY") or env("cloud_api_key"):
tools.append(FunctionTool(search_toolkit.search_exa))
# if env("EXA_API_KEY") or env("cloud_api_key"):
# tools.append(FunctionTool(search_toolkit.search_exa))
# if env("TONGXIAO_API_KEY"):
# tools.append(FunctionTool(search_toolkit.search_alibaba_tongxiao))
return tools
def get_tools(self) -> List[FunctionTool]:
return [FunctionTool(self.search_exa)]
# def get_tools(self) -> List[FunctionTool]:
# return [FunctionTool(self.search_exa)]

View file

@ -3,10 +3,11 @@ from camel.toolkits.function_tool import FunctionTool
from loguru import logger
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseSlackToolkit)
class SlackToolkit(BaseSlackToolkit, AbstractToolkit):
agent_name: str = Agents.social_medium_agent
@ -14,71 +15,6 @@ class SlackToolkit(BaseSlackToolkit, AbstractToolkit):
super().__init__(timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseSlackToolkit.create_slack_channel,
lambda _, name, is_private=True: f"create a Slack channel with name: {name} and is_private: {is_private}",
)
def create_slack_channel(self, name: str, is_private: bool | None = True) -> str:
return super().create_slack_channel(name, is_private)
@listen_toolkit(
BaseSlackToolkit.join_slack_channel,
lambda _, channel_id: f"join Slack channel with id: {channel_id}",
)
def join_slack_channel(self, channel_id: str) -> str:
return super().join_slack_channel(channel_id)
@listen_toolkit(
BaseSlackToolkit.leave_slack_channel,
lambda _, channel_id: f"leave Slack channel with id: {channel_id}",
)
def leave_slack_channel(self, channel_id: str) -> str:
return super().leave_slack_channel(channel_id)
@listen_toolkit(
BaseSlackToolkit.get_slack_channel_information,
lambda _: "get Slack channel information",
)
def get_slack_channel_information(self) -> str:
return super().get_slack_channel_information()
@listen_toolkit(
BaseSlackToolkit.get_slack_channel_message,
lambda _, channel_id: f"get Slack channel message for channel id: {channel_id}",
)
def get_slack_channel_message(self, channel_id: str) -> str:
return super().get_slack_channel_message(channel_id)
@listen_toolkit(
BaseSlackToolkit.send_slack_message,
lambda _, message, channel_id, file_path=None, user=None: f"send Slack message: {message} to channel id: {channel_id}, file: {file_path}, user: {user}",
)
def send_slack_message(self, message: str, channel_id: str, file_path: str | None = None, user: str | None = None) -> str:
return super().send_slack_message(message, channel_id, file_path, user)
@listen_toolkit(
BaseSlackToolkit.delete_slack_message,
lambda _,
time_stamp,
channel_id: f"delete Slack message with timestamp: {time_stamp} in channel id: {channel_id}",
)
def delete_slack_message(self, time_stamp: str, channel_id: str) -> str:
return super().delete_slack_message(time_stamp, channel_id)
@listen_toolkit(
BaseSlackToolkit.get_slack_user_list,
lambda _: "get Slack user list",
)
def get_slack_user_list(self) -> str:
return super().get_slack_user_list()
@listen_toolkit(
BaseSlackToolkit.get_slack_user_info,
lambda _, user_id: f"get Slack user info with user id: {user_id}",
)
def get_slack_user_info(self, user_id: str) -> str:
return super().get_slack_user_info(user_id)
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
logger.debug(f"slack===={env('SLACK_BOT_TOKEN')}")

View file

@ -4,11 +4,12 @@ from camel.toolkits.terminal_toolkit import TerminalToolkit as BaseTerminalToolk
from camel.toolkits.terminal_toolkit.terminal_toolkit import _to_plain
from app.component.environment import env
from app.service.task import Action, ActionTerminalData, Agents, get_task_lock
from app.utils.listen.toolkit_listen import listen_toolkit
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
@auto_listen_toolkit(BaseTerminalToolkit)
class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
agent_name: str = Agents.developer_agent
@ -67,45 +68,3 @@ class TerminalToolkit(BaseTerminalToolkit, AbstractToolkit):
)
if hasattr(task_lock, "add_background_task"):
task_lock.add_background_task(task)
@listen_toolkit(
BaseTerminalToolkit.shell_exec,
lambda _, id, command, block=True: f"id: {id}, command: {command}, block: {block}",
)
def shell_exec(self, id: str, command: str, block: bool = True) -> str:
return super().shell_exec(id=id, command=command, block=block)
@listen_toolkit(
BaseTerminalToolkit.shell_view,
lambda _, id: f"id: {id}",
)
def shell_view(self, id: str) -> str:
return super().shell_view(id)
@listen_toolkit(
BaseTerminalToolkit.shell_wait,
lambda _, id, wait_seconds=None: f"id: {id}, wait_seconds: {wait_seconds}",
)
def shell_wait(self, id: str, wait_seconds: float = 5.0) -> str:
return super().shell_wait(id=id, wait_seconds=wait_seconds)
@listen_toolkit(
BaseTerminalToolkit.shell_write_to_process,
lambda _, id, command: f"id: {id}, command: {command}",
)
def shell_write_to_process(self, id: str, command: str) -> str:
return super().shell_write_to_process(id=id, command=command)
@listen_toolkit(
BaseTerminalToolkit.shell_kill_process,
lambda _, id: f"id: {id}",
)
def shell_kill_process(self, id: str) -> str:
return super().shell_kill_process(id=id)
@listen_toolkit(
BaseTerminalToolkit.shell_ask_user_for_help,
lambda _, id, prompt: f"id: {id}, prompt: {prompt}",
)
def shell_ask_user_for_help(self, id: str, prompt: str) -> str:
return super().shell_ask_user_for_help(id=id, prompt=prompt)

View file

@ -1,40 +1,13 @@
from camel.toolkits import ThinkingToolkit as BaseThinkingToolkit
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseThinkingToolkit)
class ThinkingToolkit(BaseThinkingToolkit, AbstractToolkit):
def __init__(self, api_task_id: str, agent_name: str, timeout: float | None = None):
super().__init__(timeout)
self.api_task_id = api_task_id
self.agent_name = agent_name
@listen_toolkit(BaseThinkingToolkit.plan)
def plan(self, plan: str) -> str:
return super().plan(plan)
@listen_toolkit(BaseThinkingToolkit.hypothesize)
def hypothesize(self, hypothesis: str) -> str:
return super().hypothesize(hypothesis)
@listen_toolkit(BaseThinkingToolkit.think)
def think(self, thought: str) -> str:
return super().think(thought)
@listen_toolkit(BaseThinkingToolkit.contemplate)
def contemplate(self, contemplation: str) -> str:
return super().contemplate(contemplation)
@listen_toolkit(BaseThinkingToolkit.critique)
def critique(self, critique: str) -> str:
return super().critique(critique)
@listen_toolkit(BaseThinkingToolkit.synthesize)
def synthesize(self, synthesis: str) -> str:
return super().synthesize(synthesis)
@listen_toolkit(BaseThinkingToolkit.reflect)
def reflect(self, reflection: str) -> str:
return super().reflect(reflection)

View file

@ -9,10 +9,11 @@ from camel.toolkits.twitter_toolkit import (
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseTwitterToolkit)
class TwitterToolkit(BaseTwitterToolkit, AbstractToolkit):
agent_name: str = Agents.social_medium_agent

View file

@ -4,10 +4,11 @@ from camel.toolkits import VideoAnalysisToolkit as BaseVideoAnalysisToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseVideoAnalysisToolkit)
class VideoAnalysisToolkit(BaseVideoAnalysisToolkit, AbstractToolkit):
agent_name: str = Agents.multi_modal_agent
@ -36,10 +37,3 @@ class VideoAnalysisToolkit(BaseVideoAnalysisToolkit, AbstractToolkit):
cookies_path,
timeout,
)
@listen_toolkit(
BaseVideoAnalysisToolkit.ask_question_about_video,
lambda _, video_path, question: f"transcribe video from {video_path} and ask question: {question}",
)
def ask_question_about_video(self, video_path: str, question: str) -> str:
return super().ask_question_about_video(video_path, question)

View file

@ -5,10 +5,11 @@ from camel.toolkits import VideoDownloaderToolkit as BaseVideoDownloaderToolkit
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseVideoDownloaderToolkit)
class VideoDownloaderToolkit(BaseVideoDownloaderToolkit, AbstractToolkit):
agent_name: str = Agents.multi_modal_agent
@ -23,23 +24,3 @@ class VideoDownloaderToolkit(BaseVideoDownloaderToolkit, AbstractToolkit):
working_directory = env("file_save_path", os.path.expanduser("~/Downloads"))
super().__init__(working_directory, cookies_path, timeout)
self.api_task_id = api_task_id
@listen_toolkit(BaseVideoDownloaderToolkit.download_video)
def download_video(self, url: str) -> str:
return super().download_video(url)
@listen_toolkit(
BaseVideoDownloaderToolkit.get_video_bytes,
lambda _, video_path: f"get video bytes from {video_path}",
lambda _: "get video bytes",
)
def get_video_bytes(self, video_path: str) -> bytes:
return super().get_video_bytes(video_path)
@listen_toolkit(
BaseVideoDownloaderToolkit.get_video_screenshots,
lambda _, video_path, amount: f"get video screenshots from {video_path}, amount: {amount}",
lambda results: f"get video screenshots {len(results)}",
)
def get_video_screenshots(self, video_path: str, amount: int) -> List[Image]:
return super().get_video_screenshots(video_path, amount)

View file

@ -3,10 +3,11 @@ from typing import Any, Dict
from camel.toolkits import WebDeployToolkit as BaseWebDeployToolkit
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit, listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseWebDeployToolkit)
class WebDeployToolkit(BaseWebDeployToolkit, AbstractToolkit):
agent_name: str = Agents.developer_agent
@ -43,11 +44,3 @@ class WebDeployToolkit(BaseWebDeployToolkit, AbstractToolkit):
) -> Dict[str, Any]:
subdirectory = str(uuid.uuid4())
return super().deploy_folder(folder_path, port, domain, subdirectory)
@listen_toolkit(BaseWebDeployToolkit.stop_server)
def stop_server(self, port: int) -> Dict[str, Any]:
return super().stop_server(port)
@listen_toolkit(BaseWebDeployToolkit.list_running_servers)
def list_running_servers(self) -> Dict[str, Any]:
return super().list_running_servers()

View file

@ -3,10 +3,11 @@ from camel.toolkits import WhatsAppToolkit as BaseWhatsAppToolkit
from camel.toolkits.function_tool import FunctionTool
from app.component.environment import env
from app.service.task import Agents
from app.utils.listen.toolkit_listen import listen_toolkit
from app.utils.listen.toolkit_listen import auto_listen_toolkit
from app.utils.toolkit.abstract_toolkit import AbstractToolkit
@auto_listen_toolkit(BaseWhatsAppToolkit)
class WhatsAppToolkit(BaseWhatsAppToolkit, AbstractToolkit):
agent_name: str = Agents.social_medium_agent
@ -14,30 +15,6 @@ class WhatsAppToolkit(BaseWhatsAppToolkit, AbstractToolkit):
super().__init__(timeout)
self.api_task_id = api_task_id
@listen_toolkit(
BaseWhatsAppToolkit.send_message,
lambda _, to, message: f"send message to {to}: {message}",
lambda result: f"message sent result: {result}",
)
def send_message(self, to: str, message: str) -> Dict[str, Any] | str:
return super().send_message(to, message)
@listen_toolkit(
BaseWhatsAppToolkit.get_message_templates,
lambda _: "get message templates",
lambda result: f"message templates: {result}",
)
def get_message_templates(self) -> List[Dict[str, Any]] | str:
return super().get_message_templates()
@listen_toolkit(
BaseWhatsAppToolkit.get_business_profile,
lambda _: "get business profile",
lambda result: f"business profile: {result}",
)
def get_business_profile(self) -> Dict[str, Any] | str:
return super().get_business_profile()
@classmethod
def get_can_use_tools(cls, api_task_id: str) -> list[FunctionTool]:
if env("WHATSAPP_ACCESS_TOKEN") and env("WHATSAPP_PHONE_NUMBER_ID"):

View file

@ -85,7 +85,6 @@ class Workforce(BaseWorkforce):
self.set_channel(TaskChannel())
self._state = WorkforceState.RUNNING
task.state = TaskState.OPEN
self._pending_tasks.append(task)
# Decompose the task into subtasks first
subtasks_result = self._decompose_task(task)
@ -133,7 +132,9 @@ class Workforce(BaseWorkforce):
# Find task content
task_obj = get_camel_task(item.task_id, tasks)
if task_obj is None:
logger.warning(f"[WF] WARN: Task {item.task_id} not found in tasks list during ASSIGN phase. This may indicate a task tree inconsistency.")
logger.warning(
f"[WF] WARN: Task {item.task_id} not found in tasks list during ASSIGN phase. This may indicate a task tree inconsistency."
)
content = ""
else:
content = task_obj.content
@ -179,7 +180,11 @@ class Workforce(BaseWorkforce):
await super()._post_task(task, assignee_id)
def add_single_agent_worker(
self, description: str, worker: ListenChatAgent, pool_max_size: int = DEFAULT_WORKER_POOL_SIZE
self,
description: str,
worker: ListenChatAgent,
pool_max_size: int = DEFAULT_WORKER_POOL_SIZE,
enable_workflow_memory: bool = False,
) -> BaseWorkforce:
if self._state == WorkforceState.RUNNING:
raise RuntimeError("Cannot add workers while workforce is running. Pause the workforce first.")
@ -195,6 +200,8 @@ class Workforce(BaseWorkforce):
worker=worker,
pool_max_size=pool_max_size,
use_structured_output_handler=self.use_structured_output_handler,
context_utility=None, # Will be set during save/load operations
enable_workflow_memory=enable_workflow_memory,
)
self._children.append(worker_node)

View file

@ -5,7 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = "==3.10.16"
dependencies = [
"camel-ai[eigent]==0.2.76a13",
"camel-ai[eigent]==0.2.78",
"fastapi>=0.115.12",
"fastapi-babel>=1.0.0",
"uvicorn[standard]>=0.34.2",

18
backend/uv.lock generated
View file

@ -122,6 +122,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" },
]
[[package]]
name = "astor"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" },
]
[[package]]
name = "async-timeout"
version = "5.0.1"
@ -240,7 +249,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "aiofiles", specifier = ">=24.1.0" },
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.76a13" },
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.78" },
{ name = "fastapi", specifier = ">=0.115.12" },
{ name = "fastapi-babel", specifier = ">=1.0.0" },
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
@ -324,9 +333,10 @@ wheels = [
[[package]]
name = "camel-ai"
version = "0.2.76a13"
version = "0.2.78"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "astor" },
{ name = "colorama" },
{ name = "docstring-parser" },
{ name = "httpx" },
@ -339,9 +349,9 @@ dependencies = [
{ name = "tiktoken" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/7c/0145edf0307e360557917de28691eb0c41b36b017a28c6b67e58a729a6da/camel_ai-0.2.76a13.tar.gz", hash = "sha256:487570c36a39a333ae8000783babd5a82350a829aaa8aa2ae712470b596cafe1", size = 950278, upload-time = "2025-10-06T06:09:46.064Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/2b/cd5181bfd0ebcf567a088ee5c1e3768b132ba4b1489ee19d5fb0bd679586/camel_ai-0.2.78.tar.gz", hash = "sha256:24745da225da7da96dcd85f72d143c6104569c17f14280c369d7e82b86851284", size = 964632, upload-time = "2025-10-15T17:20:54.181Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/46/9886106669491737631178830bce79bd7bf63391db4d2200f645089dd9df/camel_ai-0.2.76a13-py3-none-any.whl", hash = "sha256:b860412e4a5b5fc31b0cc3d4b1eeefcd02382d9a5aced252856a1eff0285a97b", size = 1400549, upload-time = "2025-10-06T06:09:43.291Z" },
{ url = "https://files.pythonhosted.org/packages/01/81/0cfb1c0d9da589665e2eb4471887967e70bba428638c37fb4f6a78baf300/camel_ai-0.2.78-py3-none-any.whl", hash = "sha256:356624da13dfe0c55ef43dc509c18ce029f67fe3997966495a4ce9be931078d5", size = 1415578, upload-time = "2025-10-15T17:20:51.727Z" },
]
[package.optional-dependencies]

View file

@ -559,6 +559,28 @@ export class FileReader {
return [];
}
}
public deleteTaskFiles(email: string, taskId: string): {
success: boolean;
path: { dirPath: string; logPath: string }
}
{
const safeEmail = email.split('@')[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(/^\.+|\.+$/g, "");
const userHome = app.getPath('home');
const dirPath = path.join(userHome, "eigent", safeEmail, `task_${taskId}`);
const logPath = path.join(userHome, ".eigent", safeEmail, `task_${taskId}`);
try {
if (fs.existsSync(dirPath)&&fs.existsSync(logPath)) {
fs.rmSync(dirPath, { recursive: true, force: true });
fs.rmSync(logPath, { recursive: true, force: true });
}
return { success: true, path: { dirPath, logPath } };
} catch (err) {
console.error("Delete task files failed:", dirPath, err);
return { success: false, path: { dirPath, logPath } };
}
}
public getLogFolder(email: string): string {
const safeEmail = email.split('@')[0].replace(/[\\/*?:"<>|\s]/g, "_").replace(/^\.+|\.+$/g, "");

View file

@ -51,6 +51,13 @@ findAvailablePort(browser_port).then(port => {
app.commandLine.appendSwitch('remote-debugging-port', port + '');
});
// Memory optimization settings
app.commandLine.appendSwitch('js-flags', '--max-old-space-size=4096');
app.commandLine.appendSwitch('force-gpu-mem-available-mb', '512');
app.commandLine.appendSwitch('max_old_space_size', '4096');
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
app.commandLine.appendSwitch('renderer-process-limit', '8');
// ==================== app config ====================
process.env.APP_ROOT = MAIN_DIST;
process.env.VITE_PUBLIC = VITE_PUBLIC;
@ -602,6 +609,13 @@ function registerIpcHandlers() {
return { success: false, error: 'File does not exist' };
}
// Check if it's a directory
const stats = await fsp.stat(filePath);
if (stats.isDirectory()) {
log.error('Path is a directory, not a file:', filePath);
return { success: false, error: 'Path is a directory, not a file' };
}
// Read file content
const fileContent = await fsp.readFile(filePath);
log.info('File read successfully:', filePath);
@ -800,6 +814,11 @@ function registerIpcHandlers() {
return manager.getFileList(email, taskId);
});
ipcMain.handle('delete-task-files', async (_, email: string, taskId: string) => {
const manager = checkManagerInstance(fileReader, 'FileReader');
return manager.deleteTaskFiles(email, taskId);
});
ipcMain.handle('get-log-folder', async (_, email: string) => {
const manager = checkManagerInstance(fileReader, 'FileReader');
return manager.getLogFolder(email);
@ -922,8 +941,8 @@ async function createWindow() {
fileReader = new FileReader(win);
webViewManager = new WebViewManager(win);
// create multiple webviews
for (let i = 1; i <= 8; i++) {
// create initial webviews (reduced from 8 to 3)
for (let i = 1; i <= 3; i++) {
webViewManager.createWebview(i === 1 ? undefined : i.toString());
}
@ -1197,14 +1216,24 @@ const cleanupPythonProcess = async () => {
const pid = python_process.pid;
log.info('Cleaning up Python process', { pid });
// Remove all listeners to prevent memory leaks
python_process.removeAllListeners();
await new Promise<void>((resolve) => {
kill(pid, 'SIGINT', (err) => {
kill(pid, 'SIGTERM', (err) => {
if (err) {
log.error('Failed to clean up process tree:', err);
log.error('Failed to clean up process tree with SIGTERM:', err);
// Try SIGKILL as fallback
kill(pid, 'SIGKILL', (killErr) => {
if (killErr) {
log.error('Failed to force kill process tree:', killErr);
}
resolve();
});
} else {
log.info('Successfully cleaned up Python process tree');
resolve();
}
resolve();
});
});
}
@ -1224,17 +1253,38 @@ const cleanupPythonProcess = async () => {
}
}
// Clean up any temporary files in userData
try {
const tempFiles = ['backend.lock', 'uv_installing.lock'];
for (const file of tempFiles) {
const filePath = path.join(userData, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
} catch (error) {
log.error('Error cleaning up temp files:', error);
}
python_process = null;
} catch (error) {
log.error('Error occurred while cleaning up process:', error);
}
};
// brefore close
// before close
const handleBeforeClose = () => {
let isQuitting = false;
app.on('before-quit', () => {
isQuitting = true;
});
win?.on("close", (event) => {
event.preventDefault();
win?.webContents.send("before-close");
if (!isQuitting) {
event.preventDefault();
win?.webContents.send("before-close");
}
})
}
@ -1289,8 +1339,15 @@ app.whenReady().then(() => {
// ==================== window close event ====================
app.on('window-all-closed', () => {
log.info('window-all-closed');
webViewManager = null;
// Clean up WebView manager
if (webViewManager) {
webViewManager.destroy();
webViewManager = null;
}
win = null;
if (process.platform !== 'darwin') {
app.quit();
}
@ -1310,12 +1367,44 @@ app.on('activate', () => {
});
// ==================== app exit event ====================
app.on('before-quit', () => {
app.on('before-quit', async (event) => {
log.info('before-quit');
log.info('quit python_process.pid: ' + python_process?.pid);
if (win) {
win.destroy();
// Prevent default quit to ensure cleanup completes
event.preventDefault();
try {
// Clean up resources
if (webViewManager) {
webViewManager.destroy();
webViewManager = null;
}
if (win && !win.isDestroyed()) {
win.destroy();
win = null;
}
// Wait for Python process cleanup
await cleanupPythonProcess();
// Clean up file reader if exists
if (fileReader) {
fileReader = null;
}
// Clear any remaining timeouts/intervals
if (global.gc) {
global.gc();
}
log.info('All cleanup completed, exiting...');
} catch (error) {
log.error('Error during cleanup:', error);
} finally {
// Force quit after cleanup
app.exit(0);
}
cleanupPythonProcess();
});

View file

@ -232,8 +232,6 @@ class InstallLogs {
displayFilteredLogs(data:String) {
if (!data) return;
const msg = data.toString().trimEnd();
//Detect if uv sync is run
detectInstallationLogs(msg);
if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) {
log.error(`BACKEND: [DEPS INSTALL] ${msg}`);
safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });

View file

@ -20,6 +20,9 @@ export class WebViewManager {
private webViews = new Map<string, WebViewInfo>()
private win: BrowserWindow | null = null
private size: Size = { x: 0, y: 0, width: 0, height: 0 }
private maxInactiveWebviews = 5
private lastCleanupTime = Date.now()
constructor(window: BrowserWindow) {
this.win = window
}
@ -61,8 +64,17 @@ export class WebViewManager {
}
const view = new WebContentsView({
webPreferences: {
// Use a separate session partition for webviews to isolate storage from main window
// This ensures clearing webview storage won't affect main window's auth data
partition: 'persist:agent-webview',
nodeIntegration: false,
contextIsolation: true,
backgroundThrottling: true,
offscreen: false,
sandbox: true,
disableBlinkFeatures: 'Accelerated2dCanvas',
enableBlinkFeatures: 'IdleDetection',
autoplayPolicy: 'document-user-activation-required',
},
})
view.webContents.on('did-finish-load', () => {
@ -119,13 +131,22 @@ export class WebViewManager {
webViewInfo.view.setBounds({ x: -1919, y: -1079, width: 1920, height: 1080 })
const activeSize = this.getActiveWebview().length
const allSize = Array.from(this.webViews.values()).length
if (allSize - activeSize <= 3) {
const inactiveSize = allSize - activeSize
// Clean up inactive webviews if too many
if (inactiveSize > this.maxInactiveWebviews && Date.now() - this.lastCleanupTime > 30000) {
this.cleanupInactiveWebviews()
this.lastCleanupTime = Date.now()
}
// Create new webviews if needed
if (inactiveSize <= 2) {
const existingKeys = Array.from(this.webViews.keys()).map(Number).filter(n => !isNaN(n))
const maxId = existingKeys.length > 0 ? Math.max(...existingKeys) : 0
const startId = maxId + 1
// Create webviews sequentially to avoid race conditions
for (let i = 0; i < 3; i++) {
// Create only 2 new webviews to reduce memory usage
for (let i = 0; i < 2; i++) {
const nextId = (startId + i).toString()
this.createWebview(nextId, 'about:blank?use=0')
}
@ -190,6 +211,10 @@ export class WebViewManager {
let newId = Number(id)
webViewInfo.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 })
webViewInfo.isShow = false
if (webViewInfo.view.webContents && !webViewInfo.view.webContents.isDestroyed()) {
webViewInfo.view.webContents.setBackgroundThrottling(true)
}
return { success: true }
}
@ -198,19 +223,36 @@ export class WebViewManager {
let newId = Number(webview.id)
webview.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 })
webview.isShow = false
if (webview.view.webContents && !webview.view.webContents.isDestroyed()) {
webview.view.webContents.setBackgroundThrottling(true)
}
})
}
public showWebview(id: string) {
const webViewInfo = this.webViews.get(id)
public async showWebview(id: string) {
let webViewInfo = this.webViews.get(id)
// If webview doesn't exist, create it
if (!webViewInfo) {
return { success: false, error: `Webview with id ${id} not found` }
console.log(`Webview ${id} not found, creating new one`)
const createResult = await this.createWebview(id, 'about:blank?use=0')
if (!createResult.success) {
return { success: false, error: `Failed to create webview ${id}` }
}
webViewInfo = this.webViews.get(id)!
}
const currentUrl = webViewInfo.view.webContents.getURL();
this.win?.webContents.send("url-updated", currentUrl);
webViewInfo.isShow = true
this.changeViewSize(id, this.size)
console.log("showWebview", id, this.size)
if (webViewInfo.view.webContents && !webViewInfo.view.webContents.isDestroyed()) {
webViewInfo.view.webContents.setBackgroundThrottling(false)
}
if (this.win && !this.win.isDestroyed()) {
this.win.webContents.send('webview-show', id)
}
@ -228,6 +270,15 @@ export class WebViewManager {
return { success: false, error: `Webview with id ${id} not found` }
}
if (!webViewInfo.view.webContents.isDestroyed()) {
webViewInfo.view.webContents.removeAllListeners()
// Now safe to clear all storage since webviews use separate partition
webViewInfo.view.webContents.session.clearCache()
webViewInfo.view.webContents.session.clearStorageData({
storages: ['cookies', 'localstorage', 'websql', 'indexdb', 'serviceworkers', 'cachestorage']
})
}
// remove webview from parent container
if (this.win?.contentView) {
this.win.contentView.removeChildView(webViewInfo.view)
@ -254,5 +305,18 @@ export class WebViewManager {
})
this.webViews.clear()
}
private cleanupInactiveWebviews() {
const inactiveWebviews = Array.from(this.webViews.entries())
.filter(([id, info]) => !info.isActive && !info.isShow && info.currentUrl === 'about:blank?use=0')
.sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
const toRemove = inactiveWebviews.slice(this.maxInactiveWebviews)
toRemove.forEach(([id, _]) => {
console.log(`Cleaning up inactive webview: ${id}`)
this.destroyWebview(id)
})
}
}

View file

@ -0,0 +1,36 @@
"""modify_chat_history_add_project_id
Revision ID: eec7242b3a9b
Revises: d74ab2a44600
Create Date: 2025-10-15 14:46:47.904254
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
# revision identifiers, used by Alembic.
revision: str = "eec7242b3a9b"
down_revision: Union[str, None] = "d74ab2a44600"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("chat_history", sa.Column("project_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True))
op.create_index(op.f("ix_chat_history_project_id"), "chat_history", ["project_id"], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_chat_history_project_id"), table_name="chat_history")
op.drop_column("chat_history", "project_id")
# ### end Alembic commands ###

View file

@ -4,7 +4,7 @@ from typing import Optional
from enum import IntEnum
from sqlalchemy_utils import ChoiceType
from app.model.abstract.model import AbstractModel, DefaultTimes
from pydantic import BaseModel
from pydantic import BaseModel, model_validator
class ChatStatus(IntEnum):
@ -16,6 +16,7 @@ class ChatHistory(AbstractModel, DefaultTimes, table=True):
id: int = Field(default=None, primary_key=True)
user_id: int = Field(index=True)
task_id: str = Field(index=True, unique=True)
project_id: str = Field(index=True, unique=False, nullable=True)
question: str
language: str
model_platform: str
@ -34,6 +35,7 @@ class ChatHistory(AbstractModel, DefaultTimes, table=True):
class ChatHistoryIn(BaseModel):
task_id: str
project_id: str | None = None
user_id: int | None = None
question: str
language: str
@ -54,6 +56,7 @@ class ChatHistoryIn(BaseModel):
class ChatHistoryOut(BaseModel):
id: int
task_id: str
project_id: str | None = None
question: str
language: str
model_platform: str
@ -68,9 +71,17 @@ class ChatHistoryOut(BaseModel):
tokens: int
status: int
@model_validator(mode="after")
def fill_project_id_from_task_id(self):
"""fill by task_id when project_id is None"""
if self.project_id is None:
self.project_id = self.task_id
return self
class ChatHistoryUpdate(BaseModel):
project_name: str | None = None
summary: str | None = None
tokens: int | None = None
status: int | None = None
project_id: str | None = None

View file

@ -294,10 +294,10 @@ export default function ChatBox(): JSX.Element {
onTyping={scrollToBottom}
/>
<div className="flex gap-2 flex-wrap">
{item.fileList?.map((file) => {
{item.fileList?.map((file, fileIndex) => {
return (
<div
key={"file-" + file.name}
key={`file-${item.id}-${fileIndex}-${file.name}`}
onClick={() => {
// set selected file
chatStore.setSelectedFile(
@ -376,10 +376,10 @@ export default function ChatBox(): JSX.Element {
onTyping={scrollToBottom}
/> */}
<div className="flex gap-2 flex-wrap">
{item.fileList?.map((file) => {
{item.fileList?.map((file, fileIndex) => {
return (
<div
key={"file-" + file.name}
key={`file-${item.id}-${fileIndex}-${file.name}`}
onClick={() => {
// set selected file
chatStore.setSelectedFile(

View file

@ -37,6 +37,7 @@ import { proxyFetchGet, proxyFetchDelete, proxyFetchPost } from "@/api/http";
import { Tag } from "../ui/tag";
import { share } from "@/lib/share";
import { useTranslation } from "react-i18next";
import {getAuthStore} from "@/store/authStore";
export default function HistorySidebar() {
const { t } = useTranslation();
@ -175,6 +176,16 @@ export default function HistorySidebar() {
try {
const res = await proxyFetchDelete(`/api/chat/history/${curHistoryId}`);
console.log(res);
// also delete local files for this task if available (via Electron IPC)
const {email} = getAuthStore()
const history = historyTasks.find((item) => item.id === curHistoryId);
if (history?.task_id && (window as any).ipcRenderer) {
try {
await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id);
} catch (error) {
console.warn("Local file cleanup failed:", error);
}
}
} catch (error) {
console.error("Failed to delete history task:", error);
}
@ -299,7 +310,8 @@ export default function HistorySidebar() {
/>
</div>
<div className="text-left text-[14px] text-text-primary font-bold leading-9 overflow-hidden text-ellipsis break-words line-clamp-3">
{task?.messages[0]?.content || t("task-hub.new-project")}
{task?.messages?.[0]?.content ||
t("task-hub.new-project")}
</div>
<div className="w-full">
<Progress
@ -398,13 +410,13 @@ export default function HistorySidebar() {
<Tooltip>
<TooltipTrigger asChild>
<span>
{task?.messages[0]?.content ||
{task?.messages?.[0]?.content ||
t("task-hub.new-project")}
</span>
</TooltipTrigger>
<TooltipContent className="w-[200px] bg-white-100% p-2 text-wrap break-words text-xs select-text pointer-events-auto !fixed ">
<p>
{task?.messages[0]?.content ||
{task?.messages?.[0]?.content ||
t("task-hub.new-project")}
</p>
</TooltipContent>
@ -449,7 +461,7 @@ export default function HistorySidebar() {
<div className="flex justify-start items-center flex-wrap gap-2 ">
{historyTasks
.filter((task) =>
task.question
task?.question
?.toLowerCase()
.includes(searchValue.toLowerCase())
)
@ -457,11 +469,14 @@ export default function HistorySidebar() {
return (
<div
onClick={() =>
handleSetActive(task.task_id, task.question)
handleSetActive(
task?.task_id,
task?.question
)
}
key={task.task_id}
className={`${
chatStore.activeTaskId === task.task_id
chatStore.activeTaskId === task?.task_id
? "!bg-white-100%"
: ""
} max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl w-[316px] h-[180px] p-6 shadow-history-item`}
@ -475,16 +490,16 @@ export default function HistorySidebar() {
alt="folder-icon"
/>
<Tag variant="primary">
# Token {task.tokens || 0}
# Token {task?.tokens || 0}
</Tag>
</div>
<div className="text-[14px] text-text-primary font-bold leading-9 overflow-hidden text-ellipsis whitespace-nowrap">
{task?.question.split("|")[0] ||
{task?.question?.split("|")?.[0] ||
t("task-hub.new-project")}
</div>
<div className="text-xs text-black leading-17 overflow-hidden text-ellipsis break-words line-clamp-2">
{task?.question.split("|")[1] ||
{task?.question?.split("|")?.[1] ||
t("task-hub.new-project")}
</div>
</div>
@ -494,98 +509,110 @@ export default function HistorySidebar() {
) : (
// List
<div className=" flex flex-col justify-start items-center gap-4 ">
{historyTasks.map((task) => {
return (
<div
onClick={() => {
handleSetActive(task.task_id, task.question);
}}
key={task.task_id}
className={`${
chatStore.activeTaskId === task.task_id
? "!bg-white-100%"
: ""
} max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-2xl flex justify-between items-center gap-md w-full p-3 h-14 shadow-history-item border border-solid border-border-disabled`}
>
<img className="w-8 h-8" src={folderIcon} alt="folder-icon" />
<div className="w-full text-[14px] text-text-primary font-bold leading-9 overflow-hidden text-ellipsis whitespace-nowrap">
<Tooltip>
<TooltipTrigger asChild>
<span>
{" "}
{task?.question.split("|")[0] || t("task-hub.new-project")}
</span>
</TooltipTrigger>
<TooltipContent
align="start"
className="w-[800px] bg-white-100% p-2 text-wrap break-words text-xs select-text pointer-events-auto"
>
<div>
{" "}
{task?.question.split("|")[0] || t("task-hub.new-project")}
</div>
</TooltipContent>
</Tooltip>
</div>
<Tag
variant="primary"
className="text-xs leading-17 font-medium text-nowrap"
{historyTasks.map((task) => {
return (
<div
onClick={() => {
handleSetActive(
task?.task_id,
task?.question
);
}}
key={task.task_id}
className={`${
chatStore.activeTaskId === task?.task_id
? "!bg-white-100%"
: ""
} max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-2xl flex justify-between items-center gap-md w-full p-3 h-14 shadow-history-item border border-solid border-border-disabled`}
>
# Token {task.tokens || 0}
</Tag>
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
onClick={(e) => e.stopPropagation()}
variant="ghost"
>
<Ellipsis size={16} className="text-text-primary" />
</Button>
</PopoverTrigger>
<PopoverContent className=" w-[98px] p-sm rounded-[12px] bg-dropdown-bg border border-solid border-dropdown-border">
<div className="space-y-1">
<PopoverClose asChild>
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={(e) => {
e.stopPropagation();
handleShare(task.task_id);
}}
>
<Share size={16} />
{t("task-hub.share")}
</Button>
</PopoverClose>
<PopoverClose asChild>
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={(e) => {
e.stopPropagation();
handleDelete(task.id);
}}
>
<Trash2
size={16}
className="text-icon-primary group-hover:text-icon-cuation"
/>
{t("task-hub.delete")}
</Button>
</PopoverClose>
</div>
</PopoverContent>
</Popover>
</div>
);
})}
</div>
<img
className="w-8 h-8"
src={folderIcon}
alt="folder-icon"
/>
<div className="w-full text-[14px] text-text-primary font-bold leading-9 overflow-hidden text-ellipsis whitespace-nowrap">
<Tooltip>
<TooltipTrigger asChild>
<span>
{" "}
{task?.question?.split("|")?.[0] ||
t("task-hub.new-project")}
</span>
</TooltipTrigger>
<TooltipContent
align="start"
className="w-[800px] bg-white-100% p-2 text-wrap break-words text-xs select-text pointer-events-auto"
>
<div>
{" "}
{task?.question?.split("|")?.[0] ||
t("task-hub.new-project")}
</div>
</TooltipContent>
</Tooltip>
</div>
<Tag
variant="primary"
className="text-xs leading-17 font-medium text-nowrap"
>
# Token {task?.tokens || 0}
</Tag>
<Popover>
<PopoverTrigger asChild>
<Button
size="icon"
onClick={(e) => e.stopPropagation()}
variant="ghost"
>
<Ellipsis
size={16}
className="text-text-primary"
/>
</Button>
</PopoverTrigger>
<PopoverContent className=" w-[98px] p-sm rounded-[12px] bg-dropdown-bg border border-solid border-dropdown-border">
<div className="space-y-1">
<PopoverClose asChild>
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={(e) => {
e.stopPropagation();
handleShare(task?.task_id);
}}
>
<Share size={16} />
{t("task-hub.share")}
</Button>
</PopoverClose>
<PopoverClose asChild>
<Button
variant="ghost"
size="sm"
className="w-full"
onClick={(e) => {
e.stopPropagation();
handleDelete(task?.id);
}}
>
<Trash2
size={16}
className="text-icon-primary group-hover:text-icon-cuation"
/>
{t("task-hub.delete")}
</Button>
</PopoverClose>
</div>
</PopoverContent>
</Popover>
</div>
);
})}
</div>
)}
</motion.div>
)}

View file

@ -31,12 +31,14 @@ const nodeTypes: NodeTypes = {
node: (props: any) => <CustomNodeComponent {...props} />,
};
const VIEWPORT_ANIMATION_DURATION = 500;
export default function Workflow({
taskAssigning,
}: {
taskAssigning: Agent[];
}) {
const {t} = useTranslation();
const { t } = useTranslation();
const chatStore = useChatStore();
const [isEditMode, setIsEditMode] = useState(false);
const [lastViewport, setLastViewport] = useState({ x: 0, y: 0, zoom: 1 });
@ -245,7 +247,7 @@ export default function Workflow({
},
position: isEditMode
? node.position
: { x: index * (342+20) + 8, y: 16 },
: { x: index * (342 + 20) + 8, y: 16 },
};
} else {
return {
@ -259,7 +261,7 @@ export default function Workflow({
isEditMode: isEditMode,
workerInfo: agent?.workerInfo,
},
position: { x: index * (342+20) + 8, y: 16 },
position: { x: index * (342 + 20) + 8, y: 16 },
type: "node",
};
}
@ -293,6 +295,24 @@ export default function Workflow({
};
}, [getViewport, setViewport, isEditMode]);
const [isAnimating, setIsAnimating] = useState(false);
const moveViewport = (dx: number) => {
if (isAnimating) return;
const viewport = getViewport();
setIsAnimating(true);
// Prevent scrolling past x=0 (too far right) when moving left
const newX = dx > 0 ? Math.min(0, viewport.x + dx) : viewport.x + dx;
setViewport(
{ x: newX, y: viewport.y, zoom: viewport.zoom },
{
duration: VIEWPORT_ANIMATION_DURATION,
}
);
setTimeout(() => {
setIsAnimating(false);
}, VIEWPORT_ANIMATION_DURATION);
};
const handleShare = async (taskId: string) => {
share(taskId);
};
@ -343,12 +363,7 @@ export default function Workflow({
variant="ghost"
size="icon"
onClick={() => {
const viewport = getViewport();
const newX = Math.min(0, viewport.x + 200);
setViewport(
{ x: newX, y: viewport.y, zoom: viewport.zoom },
{ duration: 500 }
);
moveViewport(200);
}}
>
<ChevronLeft className="w-4 h-4 text-icon-primary" />
@ -356,14 +371,7 @@ export default function Workflow({
<Button
variant="ghost"
size="icon"
onClick={() => {
const viewport = getViewport();
const newX = viewport.x - 200;
setViewport(
{ x: newX, y: viewport.y, zoom: viewport.zoom },
{ duration: 500 }
);
}}
onClick={() => moveViewport(-200)}
>
<ChevronRight className="w-4 h-4 text-icon-primary" />
</Button>

View file

@ -34,20 +34,17 @@ import {
PopoverTrigger,
PopoverClose,
} from "@/components/ui/popover";
import {
fetchPut,
proxyFetchDelete,
proxyFetchGet,
} from "@/api/http";
import { fetchPut, proxyFetchDelete, proxyFetchGet } from "@/api/http";
import AlertDialog from "@/components/ui/alertDialog";
import { generateUniqueId } from "@/lib";
import { SearchHistoryDialog } from "@/components/SearchHistoryDialog";
import { Tag } from "@/components/ui/tag";
import { share } from "@/lib/share";
import { useTranslation } from "react-i18next";
import {getAuthStore} from "@/store/authStore";
export default function Home() {
const {t} = useTranslation()
const { t } = useTranslation();
const navigate = useNavigate();
const chatStore = useChatStore();
const { history_type, setHistoryType } = useGlobalStore();
@ -160,6 +157,20 @@ export default function Home() {
try {
const res = await proxyFetchDelete(`/api/chat/history/${curHistoryId}`);
console.log(res);
// also delete local files for this task if available (via Electron IPC)
const { email } = getAuthStore();
const history = historyTasks.find((item) => item.id === curHistoryId);
if (history?.task_id && (window as any).ipcRenderer) {
try {
await (window as any).ipcRenderer.invoke(
"delete-task-files",
email,
history.task_id
);
} catch (error) {
console.warn("Local file cleanup failed:", error);
}
}
} catch (error) {
console.error("Failed to delete history task:", error);
}
@ -249,7 +260,9 @@ export default function Home() {
/>
<div>
<div className="px-6 py-4 flex justify-between items-center">
<div className="text-2xl font-bold leading-4">{t("task-hub.ongoing-tasks")}</div>
<div className="text-2xl font-bold leading-4">
{t("task-hub.ongoing-tasks")}
</div>
<div className="flex items-center gap-md">
<SearchHistoryDialog />
@ -396,7 +409,10 @@ export default function Home() {
<div className=" flex-1 text-[14px] text-text-primary font-bold leading-9 overflow-hidden text-ellipsis whitespace-nowrap">
<Tooltip>
<TooltipTrigger asChild>
<span> {task.summaryTask || t("task-hub.new-project")}</span>
<span>
{" "}
{task.summaryTask || t("task-hub.new-project")}
</span>
</TooltipTrigger>
<TooltipContent>
<p> {task.summaryTask || t("task-hub.new-project")}</p>
@ -549,10 +565,10 @@ export default function Home() {
{historyTasks.map((task) => {
return (
<div
onClick={() => handleSetActive(task.task_id, task.question)}
onClick={() => handleSetActive(task?.task_id, task?.question)}
key={task.task_id}
className={`${
chatStore.activeTaskId === task.task_id
chatStore.activeTaskId === task?.task_id
? "!bg-white-100%"
: ""
} relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center flex-wrap gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] p-6 shadow-history-item border border-solid border-border-disabled`}
@ -571,7 +587,7 @@ export default function Home() {
variant="primary"
className="text-xs leading-17 font-medium text-nowrap"
>
# Token {task.tokens || 0}
# Token {task?.tokens || 0}
</Tag>
</div>
</div>
@ -591,11 +607,11 @@ export default function Home() {
return (
<div
onClick={() => {
handleSetActive(task.task_id, task.question);
handleSetActive(task?.task_id, task?.question);
}}
key={task.task_id}
className={`${
chatStore.activeTaskId === task.task_id
chatStore.activeTaskId === task?.task_id
? "!bg-white-100%"
: ""
} max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-2xl flex justify-between items-center gap-md w-full p-3 h-14 shadow-history-item border border-solid border-border-disabled`}
@ -609,7 +625,8 @@ export default function Home() {
<TooltipTrigger asChild>
<span>
{" "}
{task?.question.split("|")[0] || t("task-hub.new-project")}
{task?.question?.split("|")?.[0] ||
t("task-hub.new-project")}
</span>
</TooltipTrigger>
<TooltipContent
@ -618,7 +635,8 @@ export default function Home() {
>
<div>
{" "}
{task?.question.split("|")[0] || t("task-hub.new-project")}
{task?.question?.split("|")?.[0] ||
t("task-hub.new-project")}
</div>
</TooltipContent>
</Tooltip>
@ -627,7 +645,7 @@ export default function Home() {
variant="primary"
className="text-xs leading-17 font-medium text-nowrap"
>
# Token {task.tokens || 0}
# Token {task?.tokens || 0}
</Tag>
<Popover>
@ -649,7 +667,7 @@ export default function Home() {
className="w-full"
onClick={(e) => {
e.stopPropagation();
handleShare(task.task_id);
handleShare(task?.task_id);
}}
>
<Share size={16} />
@ -664,7 +682,7 @@ export default function Home() {
className="w-full"
onClick={(e) => {
e.stopPropagation();
handleDelete(task.id);
handleDelete(task?.id);
}}
>
<Trash2

View file

@ -622,6 +622,8 @@ export default function SettingModels() {
? t("setting.gpt-4.1")
: cloud_model_type === "claude-sonnet-4-5"
? t("setting.claude-sonnet-4-5")
: cloud_model_type === "claude-sonnet-4-20250514"
? t("setting.claude-sonnet-4")
: cloud_model_type === "claude-3-5-haiku-20241022"
? t("setting.claude-3.5-haiku")
: cloud_model_type === "gpt-5"
@ -658,6 +660,9 @@ export default function SettingModels() {
<SelectItem value="claude-sonnet-4-5">
Claude Sonnet 4-5
</SelectItem>
<SelectItem value="claude-sonnet-4-20250514">
Claude Sonnet 4
</SelectItem>
<SelectItem value="claude-3-5-haiku-20241022">
Claude 3.5 Haiku
</SelectItem>

View file

@ -4,7 +4,7 @@ import { persist } from 'zustand/middleware';
// type definition
type InitState = 'permissions' | 'carousel' | 'done';
type ModelType = 'cloud' | 'local' | 'custom';
type CloudModelType = 'gemini/gemini-2.5-pro' | 'gemini-2.5-flash' | 'gpt-4.1-mini' | 'gpt-4.1' | 'claude-sonnet-4-5' | 'claude-3-5-haiku-20241022' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano';
type CloudModelType = 'gemini/gemini-2.5-pro' | 'gemini-2.5-flash' | 'gpt-4.1-mini' | 'gpt-4.1' | 'claude-sonnet-4-5' | 'claude-sonnet-4-20250514' | 'claude-3-5-haiku-20241022' | 'gpt-5' | 'gpt-5-mini' | 'gpt-5-nano';
// auth info interface
interface AuthInfo {

View file

@ -235,7 +235,9 @@ const chatStore = create<ChatStore>()(
apiModel = {
api_key: res.value,
model_type: cloud_model_type,
model_platform: cloud_model_type.includes('gpt') ? 'openai' : 'gemini',
model_platform: cloud_model_type.includes('gpt') ? 'openai' :
cloud_model_type.includes('claude') ? 'anthropic' :
cloud_model_type.includes('gemini') ? 'gemini' : 'openai-compatible-model',
api_url: res.api_url,
extra_params: {}
}
@ -725,7 +727,7 @@ const chatStore = create<ChatStore>()(
if (taskIndex !== -1) {
const { toolkit_name, method_name } = agentMessages.data;
if (toolkit_name && method_name) {
if (toolkit_name && method_name && assigneeAgentIndex !== -1) {
const task = taskAssigning[assigneeAgentIndex].tasks.find((task: TaskInfo) => task.id === agentMessages.data.process_task_id);
const message = filterMessage(agentMessages)
@ -737,7 +739,7 @@ const chatStore = create<ChatStore>()(
message: message.data.message as string,
toolkitStatus: "running" as AgentStatus,
}
if (assigneeAgentIndex !== -1 && task) {
if (task) {
task.toolkits ??= []
task.toolkits.push({ ...toolkit });
task.status = "running";
@ -887,7 +889,7 @@ const chatStore = create<ChatStore>()(
if (!type && import.meta.env.VITE_USE_LOCAL_PROXY !== 'true' && res.length > 0) {
// Upload files sequentially to avoid overwhelming the server
const uploadResults = await Promise.allSettled(
res.map(async (file: any) => {
res.filter((file: any) => !file.isFolder).map(async (file: any) => {
try {
// Read file content using Electron API
const result = await window.ipcRenderer.invoke('read-file', file.path);

View file

@ -67,22 +67,25 @@ export default defineConfig(({ command, mode }) => {
renderer: {},
}),
],
server: process.env.VSCODE_DEBUG && (() => {
const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
return {
host: url.hostname,
port: +url.port,
proxy: {
'/api': {
target: env.VITE_PROXY_URL,
changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
server: {
open: false,
...(process.env.VSCODE_DEBUG && (() => {
const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL)
return {
host: url.hostname,
port: +url.port,
proxy: {
'/api': {
target: env.VITE_PROXY_URL,
changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
},
},
},
}
})(),
clearScreen: false,
}
})()),
clearScreen: false,
}
}
})