mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-20 09:25:34 +00:00
Merge branch 'main' into fix-#471
This commit is contained in:
commit
c6e78a9fff
51 changed files with 1000 additions and 1145 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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')}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
18
backend/uv.lock
generated
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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, "");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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() });
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ###
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue