mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-19 16:31:36 +00:00
Merge branch 'main' into fix/task-hub-freeze-on-click
This commit is contained in:
commit
910a5ede74
20 changed files with 373 additions and 224 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:
|
||||
|
|
|
|||
|
|
@ -449,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,
|
||||
|
|
@ -465,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,
|
||||
|
|
@ -751,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])
|
||||
|
|
@ -815,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
|
||||
|
|
@ -861,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.
|
||||
|
|
@ -899,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],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -930,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
|
||||
|
|
@ -1162,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>
|
||||
|
|
@ -1294,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(
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ def _safe_put_queue(task_lock, data):
|
|||
asyncio.set_event_loop(new_loop)
|
||||
try:
|
||||
new_loop.run_until_complete(task_lock.put_queue(data))
|
||||
logger.debug(f"[listen_toolkit] Successfully sent data to queue using new event loop")
|
||||
finally:
|
||||
new_loop.close()
|
||||
except Exception as e:
|
||||
|
|
@ -96,7 +95,6 @@ def listen_toolkit(
|
|||
"message": args_str,
|
||||
},
|
||||
)
|
||||
logger.debug(f"[listen_toolkit] Sending activate data: {activate_data.model_dump()}")
|
||||
await task_lock.put_queue(activate_data)
|
||||
error = None
|
||||
res = None
|
||||
|
|
@ -134,7 +132,6 @@ def listen_toolkit(
|
|||
"message": res_msg,
|
||||
},
|
||||
)
|
||||
logger.debug(f"[listen_toolkit] Sending deactivate data: {deactivate_data.model_dump()}")
|
||||
await task_lock.put_queue(deactivate_data)
|
||||
if error is not None:
|
||||
raise error
|
||||
|
|
@ -181,12 +178,10 @@ def listen_toolkit(
|
|||
"message": args_str,
|
||||
},
|
||||
)
|
||||
logger.debug(f"[listen_toolkit sync] Sending activate data: {activate_data.model_dump()}")
|
||||
_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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -51,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()
|
||||
|
|
@ -164,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,
|
||||
|
|
@ -290,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)]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -613,7 +613,7 @@ function registerIpcHandlers() {
|
|||
const stats = await fsp.stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
log.error('Path is a directory, not a file:', filePath);
|
||||
return { success: false, error: 'EISDIR: illegal operation on a directory, read' };
|
||||
return { success: false, error: 'Path is a directory, not a file' };
|
||||
}
|
||||
|
||||
// Read file content
|
||||
|
|
|
|||
|
|
@ -64,6 +64,9 @@ 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,
|
||||
|
|
@ -269,6 +272,7 @@ export class WebViewManager {
|
|||
|
||||
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']
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
}
|
||||
|
|
@ -885,12 +887,9 @@ const chatStore = create<ChatStore>()(
|
|||
taskId as string
|
||||
);
|
||||
if (!type && import.meta.env.VITE_USE_LOCAL_PROXY !== 'true' && res.length > 0) {
|
||||
// Filter out directories, only upload actual files
|
||||
const files = res.filter((file: any) => !file.isFolder);
|
||||
|
||||
// Upload files sequentially to avoid overwhelming the server
|
||||
const uploadResults = await Promise.allSettled(
|
||||
files.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