debug computer use tool
This commit is contained in:
@@ -14,6 +14,8 @@ from app.tool.mcp import MCPClients, MCPClientTool
|
||||
from app.tool.python_execute import PythonExecute
|
||||
from app.tool.str_replace_editor import StrReplaceEditor
|
||||
|
||||
from app.tool.computer_use_tool import ComputerUseTool
|
||||
from app.daytona.sandbox import create_sandbox
|
||||
|
||||
class Manus(ToolCallAgent):
|
||||
"""A versatile general-purpose agent with support for both local and MCP tools."""
|
||||
@@ -61,9 +63,16 @@ class Manus(ToolCallAgent):
|
||||
"""Factory method to create and properly initialize a Manus instance."""
|
||||
instance = cls(**kwargs)
|
||||
await instance.initialize_mcp_servers()
|
||||
instance.initialize_sandbox_tools()
|
||||
instance._initialized = True
|
||||
return instance
|
||||
|
||||
def initialize_sandbox_tools(self,password="123456") -> None:
|
||||
sandbox = create_sandbox(password=password)
|
||||
computer_tool = ComputerUseTool.create_with_sandbox(sandbox)
|
||||
sandbox_tools=[computer_tool]
|
||||
self.available_tools.add_tools(*sandbox_tools)
|
||||
|
||||
async def initialize_mcp_servers(self) -> None:
|
||||
"""Initialize connections to configured MCP servers."""
|
||||
for server_id, server_config in config.mcp_config.servers.items():
|
||||
|
||||
@@ -9,9 +9,9 @@ import json
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from litellm import token_counter, completion_cost
|
||||
from services.supabase import DBConnection
|
||||
from services.llm import make_llm_api_call
|
||||
from utils.logger import logger
|
||||
from app.services.supabase import DBConnection
|
||||
from app.services.llm import make_llm_api_call
|
||||
from app.utils.logger import logger
|
||||
|
||||
# Constants for token management
|
||||
DEFAULT_TOKEN_THRESHOLD = 120000 # 80k tokens threshold for summarization
|
||||
@@ -20,62 +20,62 @@ RESERVE_TOKENS = 5000 # Reserve tokens for new messages
|
||||
|
||||
class ContextManager:
|
||||
"""Manages thread context including token counting and summarization."""
|
||||
|
||||
|
||||
def __init__(self, token_threshold: int = DEFAULT_TOKEN_THRESHOLD):
|
||||
"""Initialize the ContextManager.
|
||||
|
||||
|
||||
Args:
|
||||
token_threshold: Token count threshold to trigger summarization
|
||||
"""
|
||||
self.db = DBConnection()
|
||||
self.token_threshold = token_threshold
|
||||
|
||||
|
||||
async def get_thread_token_count(self, thread_id: str) -> int:
|
||||
"""Get the current token count for a thread using LiteLLM.
|
||||
|
||||
|
||||
Args:
|
||||
thread_id: ID of the thread to analyze
|
||||
|
||||
|
||||
Returns:
|
||||
The total token count for relevant messages in the thread
|
||||
"""
|
||||
logger.debug(f"Getting token count for thread {thread_id}")
|
||||
|
||||
|
||||
try:
|
||||
# Get messages for the thread
|
||||
messages = await self.get_messages_for_summarization(thread_id)
|
||||
|
||||
|
||||
if not messages:
|
||||
logger.debug(f"No messages found for thread {thread_id}")
|
||||
return 0
|
||||
|
||||
|
||||
# Use litellm's token_counter for accurate model-specific counting
|
||||
# This is much more accurate than the SQL-based estimation
|
||||
token_count = token_counter(model="gpt-4", messages=messages)
|
||||
|
||||
|
||||
logger.info(f"Thread {thread_id} has {token_count} tokens (calculated with litellm)")
|
||||
return token_count
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting token count: {str(e)}")
|
||||
return 0
|
||||
|
||||
|
||||
async def get_messages_for_summarization(self, thread_id: str) -> List[Dict[str, Any]]:
|
||||
"""Get all LLM messages from the thread that need to be summarized.
|
||||
|
||||
|
||||
This gets messages after the most recent summary or all messages if
|
||||
no summary exists. Unlike get_llm_messages, this includes ALL messages
|
||||
no summary exists. Unlike get_llm_messages, this includes ALL messages
|
||||
since the last summary, even if we're generating a new summary.
|
||||
|
||||
|
||||
Args:
|
||||
thread_id: ID of the thread to get messages from
|
||||
|
||||
|
||||
Returns:
|
||||
List of message objects to summarize
|
||||
"""
|
||||
logger.debug(f"Getting messages for summarization for thread {thread_id}")
|
||||
client = await self.db.client
|
||||
|
||||
|
||||
try:
|
||||
# Find the most recent summary message
|
||||
summary_result = await client.table('messages').select('created_at') \
|
||||
@@ -85,12 +85,12 @@ class ContextManager:
|
||||
.order('created_at', desc=True) \
|
||||
.limit(1) \
|
||||
.execute()
|
||||
|
||||
|
||||
# Get messages after the most recent summary or all messages if no summary
|
||||
if summary_result.data and len(summary_result.data) > 0:
|
||||
last_summary_time = summary_result.data[0]['created_at']
|
||||
logger.debug(f"Found last summary at {last_summary_time}")
|
||||
|
||||
|
||||
# Get all messages after the summary, but NOT including the summary itself
|
||||
messages_result = await client.table('messages').select('*') \
|
||||
.eq('thread_id', thread_id) \
|
||||
@@ -106,7 +106,7 @@ class ContextManager:
|
||||
.eq('is_llm_message', True) \
|
||||
.order('created_at') \
|
||||
.execute()
|
||||
|
||||
|
||||
# Parse the message content if needed
|
||||
messages = []
|
||||
for msg in messages_result.data:
|
||||
@@ -114,7 +114,7 @@ class ContextManager:
|
||||
if msg.get('type') == 'summary':
|
||||
logger.debug(f"Skipping summary message from {msg.get('created_at')}")
|
||||
continue
|
||||
|
||||
|
||||
# Parse content if it's a string
|
||||
content = msg['content']
|
||||
if isinstance(content, str):
|
||||
@@ -122,45 +122,45 @@ class ContextManager:
|
||||
content = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
pass # Keep as string if not valid JSON
|
||||
|
||||
|
||||
# Ensure we have the proper format for the LLM
|
||||
if 'role' not in content and 'type' in msg:
|
||||
# Convert message type to role if needed
|
||||
role = msg['type']
|
||||
if role == 'assistant' or role == 'user' or role == 'system' or role == 'tool':
|
||||
content = {'role': role, 'content': content}
|
||||
|
||||
|
||||
messages.append(content)
|
||||
|
||||
|
||||
logger.info(f"Got {len(messages)} messages to summarize for thread {thread_id}")
|
||||
return messages
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting messages for summarization: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def create_summary(
|
||||
self,
|
||||
thread_id: str,
|
||||
messages: List[Dict[str, Any]],
|
||||
self,
|
||||
thread_id: str,
|
||||
messages: List[Dict[str, Any]],
|
||||
model: str = "gpt-4o-mini"
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Generate a summary of conversation messages.
|
||||
|
||||
|
||||
Args:
|
||||
thread_id: ID of the thread to summarize
|
||||
messages: Messages to summarize
|
||||
model: LLM model to use for summarization
|
||||
|
||||
|
||||
Returns:
|
||||
Summary message object or None if summarization failed
|
||||
"""
|
||||
if not messages:
|
||||
logger.warning("No messages to summarize")
|
||||
return None
|
||||
|
||||
|
||||
logger.info(f"Creating summary for thread {thread_id} with {len(messages)} messages")
|
||||
|
||||
|
||||
# Create system message with summarization instructions
|
||||
system_message = {
|
||||
"role": "system",
|
||||
@@ -185,7 +185,7 @@ THE CONVERSATION HISTORY TO SUMMARIZE IS AS FOLLOWS:
|
||||
===============================================================
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
# Call LLM to generate summary
|
||||
response = await make_llm_api_call(
|
||||
@@ -195,10 +195,10 @@ THE CONVERSATION HISTORY TO SUMMARIZE IS AS FOLLOWS:
|
||||
max_tokens=SUMMARY_TARGET_TOKENS,
|
||||
stream=False
|
||||
)
|
||||
|
||||
|
||||
if response and hasattr(response, 'choices') and response.choices:
|
||||
summary_content = response.choices[0].message.content
|
||||
|
||||
|
||||
# Track token usage
|
||||
try:
|
||||
token_count = token_counter(model=model, messages=[{"role": "user", "content": summary_content}])
|
||||
@@ -206,7 +206,7 @@ THE CONVERSATION HISTORY TO SUMMARIZE IS AS FOLLOWS:
|
||||
logger.info(f"Summary generated with {token_count} tokens at cost ${cost:.6f}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating token usage: {str(e)}")
|
||||
|
||||
|
||||
# Format the summary message with clear beginning and end markers
|
||||
formatted_summary = f"""
|
||||
======== CONVERSATION HISTORY SUMMARY ========
|
||||
@@ -217,66 +217,66 @@ THE CONVERSATION HISTORY TO SUMMARIZE IS AS FOLLOWS:
|
||||
|
||||
The above is a summary of the conversation history. The conversation continues below.
|
||||
"""
|
||||
|
||||
|
||||
# Format the summary message
|
||||
summary_message = {
|
||||
"role": "user",
|
||||
"content": formatted_summary
|
||||
}
|
||||
|
||||
|
||||
return summary_message
|
||||
else:
|
||||
logger.error("Failed to generate summary: Invalid response")
|
||||
return None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating summary: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def check_and_summarize_if_needed(
|
||||
self,
|
||||
thread_id: str,
|
||||
add_message_callback,
|
||||
self,
|
||||
thread_id: str,
|
||||
add_message_callback,
|
||||
model: str = "gpt-4o-mini",
|
||||
force: bool = False
|
||||
) -> bool:
|
||||
"""Check if thread needs summarization and summarize if so.
|
||||
|
||||
|
||||
Args:
|
||||
thread_id: ID of the thread to check
|
||||
add_message_callback: Callback to add the summary message to the thread
|
||||
model: LLM model to use for summarization
|
||||
force: Whether to force summarization regardless of token count
|
||||
|
||||
|
||||
Returns:
|
||||
True if summarization was performed, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Get token count using LiteLLM (accurate model-specific counting)
|
||||
token_count = await self.get_thread_token_count(thread_id)
|
||||
|
||||
|
||||
# If token count is below threshold and not forcing, no summarization needed
|
||||
if token_count < self.token_threshold and not force:
|
||||
logger.debug(f"Thread {thread_id} has {token_count} tokens, below threshold {self.token_threshold}")
|
||||
return False
|
||||
|
||||
|
||||
# Log reason for summarization
|
||||
if force:
|
||||
logger.info(f"Forced summarization of thread {thread_id} with {token_count} tokens")
|
||||
else:
|
||||
logger.info(f"Thread {thread_id} exceeds token threshold ({token_count} >= {self.token_threshold}), summarizing...")
|
||||
|
||||
|
||||
# Get messages to summarize
|
||||
messages = await self.get_messages_for_summarization(thread_id)
|
||||
|
||||
|
||||
# If there are too few messages, don't summarize
|
||||
if len(messages) < 3:
|
||||
logger.info(f"Thread {thread_id} has too few messages ({len(messages)}) to summarize")
|
||||
return False
|
||||
|
||||
|
||||
# Create summary
|
||||
summary = await self.create_summary(thread_id, messages, model)
|
||||
|
||||
|
||||
if summary:
|
||||
# Add summary message to thread
|
||||
await add_message_callback(
|
||||
@@ -286,13 +286,13 @@ The above is a summary of the conversation history. The conversation continues b
|
||||
is_llm_message=True,
|
||||
metadata={"token_count": token_count}
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"Successfully added summary to thread {thread_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to create summary for thread {thread_id}")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in check_and_summarize_if_needed: {str(e)}", exc_info=True)
|
||||
return False
|
||||
return False
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,18 +12,18 @@ This module provides comprehensive conversation management, including:
|
||||
|
||||
import json
|
||||
from typing import List, Dict, Any, Optional, Type, Union, AsyncGenerator, Literal
|
||||
from services.llm import make_llm_api_call
|
||||
from agentpress.tool import Tool
|
||||
from agentpress.tool_registry import ToolRegistry
|
||||
from agentpress.context_manager import ContextManager
|
||||
from agentpress.response_processor import (
|
||||
from app.services.llm import make_llm_api_call
|
||||
from app.agentpress.tool import Tool
|
||||
from app.agentpress.tool_registry import ToolRegistry
|
||||
from app.agentpress.context_manager import ContextManager
|
||||
from app.agentpress.response_processor import (
|
||||
ResponseProcessor,
|
||||
ProcessorConfig
|
||||
)
|
||||
from services.supabase import DBConnection
|
||||
from utils.logger import logger
|
||||
from app.services.supabase import DBConnection
|
||||
from app.utils.logger import logger
|
||||
from langfuse.client import StatefulGenerationClient, StatefulTraceClient
|
||||
from services.langfuse import langfuse
|
||||
from app.services.langfuse import langfuse
|
||||
import datetime
|
||||
from litellm import token_counter
|
||||
|
||||
@@ -80,7 +80,7 @@ class ThreadManager:
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def _compress_message(self, msg_content: Union[str, dict], message_id: Optional[str] = None, max_length: int = 3000) -> Union[str, dict]:
|
||||
"""Compress the message content."""
|
||||
# print("max_length", max_length)
|
||||
@@ -94,7 +94,7 @@ class ThreadManager:
|
||||
return json.dumps(msg_content)[:max_length] + "... (truncated)" + f"\n\nmessage_id \"{message_id}\"\nUse expand-message tool to see contents"
|
||||
else:
|
||||
return msg_content
|
||||
|
||||
|
||||
def _safe_truncate(self, msg_content: Union[str, dict], max_length: int = 100000) -> Union[str, dict]:
|
||||
"""Truncate the message content safely by removing the middle portion."""
|
||||
max_length = min(max_length, 100000)
|
||||
@@ -104,10 +104,10 @@ class ThreadManager:
|
||||
keep_length = max_length - 150 # Reserve space for truncation message
|
||||
start_length = keep_length // 2
|
||||
end_length = keep_length - start_length
|
||||
|
||||
|
||||
start_part = msg_content[:start_length]
|
||||
end_part = msg_content[-end_length:] if end_length > 0 else ""
|
||||
|
||||
|
||||
return start_part + f"\n\n... (middle truncated) ...\n\n" + end_part + f"\n\nThis message is too long, repeat relevant information in your response to remember it"
|
||||
else:
|
||||
return msg_content
|
||||
@@ -118,14 +118,14 @@ class ThreadManager:
|
||||
keep_length = max_length - 150 # Reserve space for truncation message
|
||||
start_length = keep_length // 2
|
||||
end_length = keep_length - start_length
|
||||
|
||||
|
||||
start_part = json_str[:start_length]
|
||||
end_part = json_str[-end_length:] if end_length > 0 else ""
|
||||
|
||||
|
||||
return start_part + f"\n\n... (middle truncated) ...\n\n" + end_part + f"\n\nThis message is too long, repeat relevant information in your response to remember it"
|
||||
else:
|
||||
return msg_content
|
||||
|
||||
|
||||
def _compress_tool_result_messages(self, messages: List[Dict[str, Any]], llm_model: str, max_tokens: Optional[int], token_threshold: Optional[int] = 1000) -> List[Dict[str, Any]]:
|
||||
"""Compress the tool result messages except the most recent one."""
|
||||
uncompressed_total_token_count = token_counter(model=llm_model, messages=messages)
|
||||
@@ -186,7 +186,7 @@ class ThreadManager:
|
||||
logger.warning(f"UNEXPECTED: Message has no message_id {str(msg)[:100]}")
|
||||
else:
|
||||
msg["content"] = self._safe_truncate(msg["content"], int(max_tokens * 2))
|
||||
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
@@ -254,17 +254,17 @@ class ThreadManager:
|
||||
result = self._compress_messages(messages, llm_model, max_tokens, int(token_threshold / 2), max_iterations - 1)
|
||||
|
||||
return self._middle_out_messages(result)
|
||||
|
||||
|
||||
def _compress_messages_by_omitting_messages(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
llm_model: str,
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
llm_model: str,
|
||||
max_tokens: Optional[int] = 41000,
|
||||
removal_batch_size: int = 10,
|
||||
min_messages_to_keep: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Compress the messages by omitting messages from the middle.
|
||||
|
||||
|
||||
Args:
|
||||
messages: List of messages to compress
|
||||
llm_model: Model name for token counting
|
||||
@@ -274,27 +274,27 @@ class ThreadManager:
|
||||
"""
|
||||
if not messages:
|
||||
return messages
|
||||
|
||||
|
||||
result = messages
|
||||
result = self._remove_meta_messages(result)
|
||||
|
||||
# Early exit if no compression needed
|
||||
initial_token_count = token_counter(model=llm_model, messages=result)
|
||||
max_allowed_tokens = max_tokens or (100 * 1000)
|
||||
|
||||
|
||||
if initial_token_count <= max_allowed_tokens:
|
||||
return result
|
||||
|
||||
# Separate system message (assumed to be first) from conversation messages
|
||||
system_message = messages[0] if messages and messages[0].get('role') == 'system' else None
|
||||
conversation_messages = result[1:] if system_message else result
|
||||
|
||||
|
||||
safety_limit = 500
|
||||
current_token_count = initial_token_count
|
||||
|
||||
|
||||
while current_token_count > max_allowed_tokens and safety_limit > 0:
|
||||
safety_limit -= 1
|
||||
|
||||
|
||||
if len(conversation_messages) <= min_messages_to_keep:
|
||||
logger.warning(f"Cannot compress further: only {len(conversation_messages)} messages remain (min: {min_messages_to_keep})")
|
||||
break
|
||||
@@ -321,20 +321,20 @@ class ThreadManager:
|
||||
# Prepare final result
|
||||
final_messages = ([system_message] + conversation_messages) if system_message else conversation_messages
|
||||
final_token_count = token_counter(model=llm_model, messages=final_messages)
|
||||
|
||||
|
||||
logger.info(f"_compress_messages_by_omitting_messages: {initial_token_count} -> {final_token_count} tokens ({len(messages)} -> {len(final_messages)} messages)")
|
||||
|
||||
|
||||
return final_messages
|
||||
|
||||
|
||||
def _middle_out_messages(self, messages: List[Dict[str, Any]], max_messages: int = 320) -> List[Dict[str, Any]]:
|
||||
"""Remove messages from the middle of the list, keeping max_messages total."""
|
||||
if len(messages) <= max_messages:
|
||||
return messages
|
||||
|
||||
|
||||
# Keep half from the beginning and half from the end
|
||||
keep_start = max_messages // 2
|
||||
keep_end = max_messages - keep_start
|
||||
|
||||
|
||||
return messages[:keep_start] + messages[-keep_end:]
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ class ThreadManager:
|
||||
'is_llm_message': is_llm_message,
|
||||
'metadata': metadata or {},
|
||||
}
|
||||
|
||||
|
||||
# Add agent information if provided
|
||||
if agent_id:
|
||||
data_to_insert['agent_id'] = agent_id
|
||||
@@ -415,26 +415,26 @@ class ThreadManager:
|
||||
|
||||
try:
|
||||
# result = await client.rpc('get_llm_formatted_messages', {'p_thread_id': thread_id}).execute()
|
||||
|
||||
|
||||
# Fetch messages in batches of 1000 to avoid overloading the database
|
||||
all_messages = []
|
||||
batch_size = 1000
|
||||
offset = 0
|
||||
|
||||
|
||||
while True:
|
||||
result = await client.table('messages').select('message_id, content').eq('thread_id', thread_id).eq('is_llm_message', True).order('created_at').range(offset, offset + batch_size - 1).execute()
|
||||
|
||||
|
||||
if not result.data or len(result.data) == 0:
|
||||
break
|
||||
|
||||
|
||||
all_messages.extend(result.data)
|
||||
|
||||
|
||||
# If we got fewer than batch_size records, we've reached the end
|
||||
if len(result.data) < batch_size:
|
||||
break
|
||||
|
||||
|
||||
offset += batch_size
|
||||
|
||||
|
||||
# Use all_messages instead of result.data in the rest of the method
|
||||
result_data = all_messages
|
||||
|
||||
|
||||
+23
-23
@@ -13,7 +13,7 @@ from abc import ABC
|
||||
import json
|
||||
import inspect
|
||||
from enum import Enum
|
||||
from utils.logger import logger
|
||||
from app.utils.logger import logger
|
||||
|
||||
class SchemaType(Enum):
|
||||
"""Enumeration of supported schema types for tool definitions."""
|
||||
@@ -24,7 +24,7 @@ class SchemaType(Enum):
|
||||
@dataclass
|
||||
class XMLNodeMapping:
|
||||
"""Maps an XML node to a function parameter.
|
||||
|
||||
|
||||
Attributes:
|
||||
param_name (str): Name of the function parameter
|
||||
node_type (str): Type of node ("element", "attribute", or "content")
|
||||
@@ -39,22 +39,22 @@ class XMLNodeMapping:
|
||||
@dataclass
|
||||
class XMLTagSchema:
|
||||
"""Schema definition for XML tool tags.
|
||||
|
||||
|
||||
Attributes:
|
||||
tag_name (str): Root tag name for the tool
|
||||
mappings (List[XMLNodeMapping]): Parameter mappings for the tag
|
||||
example (str, optional): Example showing tag usage
|
||||
|
||||
|
||||
Methods:
|
||||
add_mapping: Add a new parameter mapping to the schema
|
||||
"""
|
||||
tag_name: str
|
||||
mappings: List[XMLNodeMapping] = field(default_factory=list)
|
||||
example: Optional[str] = None
|
||||
|
||||
|
||||
def add_mapping(self, param_name: str, node_type: str = "element", path: str = ".", required: bool = True) -> None:
|
||||
"""Add a new node mapping to the schema.
|
||||
|
||||
|
||||
Args:
|
||||
param_name: Name of the function parameter
|
||||
node_type: Type of node ("element", "attribute", or "content")
|
||||
@@ -63,7 +63,7 @@ class XMLTagSchema:
|
||||
"""
|
||||
self.mappings.append(XMLNodeMapping(
|
||||
param_name=param_name,
|
||||
node_type=node_type,
|
||||
node_type=node_type,
|
||||
path=path,
|
||||
required=required
|
||||
))
|
||||
@@ -72,7 +72,7 @@ class XMLTagSchema:
|
||||
@dataclass
|
||||
class ToolSchema:
|
||||
"""Container for tool schemas with type information.
|
||||
|
||||
|
||||
Attributes:
|
||||
schema_type (SchemaType): Type of schema (OpenAPI, XML, or Custom)
|
||||
schema (Dict[str, Any]): The actual schema definition
|
||||
@@ -85,7 +85,7 @@ class ToolSchema:
|
||||
@dataclass
|
||||
class ToolResult:
|
||||
"""Container for tool execution results.
|
||||
|
||||
|
||||
Attributes:
|
||||
success (bool): Whether the tool execution succeeded
|
||||
output (str): Output message or error description
|
||||
@@ -95,19 +95,19 @@ class ToolResult:
|
||||
|
||||
class Tool(ABC):
|
||||
"""Abstract base class for all tools.
|
||||
|
||||
|
||||
Provides the foundation for implementing tools with schema registration
|
||||
and result handling capabilities.
|
||||
|
||||
|
||||
Attributes:
|
||||
_schemas (Dict[str, List[ToolSchema]]): Registered schemas for tool methods
|
||||
|
||||
|
||||
Methods:
|
||||
get_schemas: Get all registered tool schemas
|
||||
success_response: Create a successful result
|
||||
fail_response: Create a failed result
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize tool with empty schema registry."""
|
||||
self._schemas: Dict[str, List[ToolSchema]] = {}
|
||||
@@ -123,7 +123,7 @@ class Tool(ABC):
|
||||
|
||||
def get_schemas(self) -> Dict[str, List[ToolSchema]]:
|
||||
"""Get all registered tool schemas.
|
||||
|
||||
|
||||
Returns:
|
||||
Dict mapping method names to their schema definitions
|
||||
"""
|
||||
@@ -131,10 +131,10 @@ class Tool(ABC):
|
||||
|
||||
def success_response(self, data: Union[Dict[str, Any], str]) -> ToolResult:
|
||||
"""Create a successful tool result.
|
||||
|
||||
|
||||
Args:
|
||||
data: Result data (dictionary or string)
|
||||
|
||||
|
||||
Returns:
|
||||
ToolResult with success=True and formatted output
|
||||
"""
|
||||
@@ -147,10 +147,10 @@ class Tool(ABC):
|
||||
|
||||
def fail_response(self, msg: str) -> ToolResult:
|
||||
"""Create a failed tool result.
|
||||
|
||||
|
||||
Args:
|
||||
msg: Error message describing the failure
|
||||
|
||||
|
||||
Returns:
|
||||
ToolResult with success=False and error message
|
||||
"""
|
||||
@@ -182,16 +182,16 @@ def xml_schema(
|
||||
):
|
||||
"""
|
||||
Decorator for XML schema tools with improved node mapping.
|
||||
|
||||
|
||||
Args:
|
||||
tag_name: Name of the root XML tag
|
||||
mappings: List of mapping definitions, each containing:
|
||||
- param_name: Name of the function parameter
|
||||
- node_type: "element", "attribute", or "content"
|
||||
- node_type: "element", "attribute", or "content"
|
||||
- path: Path to the node (default "." for root)
|
||||
- required: Whether the parameter is required (default True)
|
||||
example: Optional example showing how to use the XML tag
|
||||
|
||||
|
||||
Example:
|
||||
@xml_schema(
|
||||
tag_name="str-replace",
|
||||
@@ -211,7 +211,7 @@ def xml_schema(
|
||||
def decorator(func):
|
||||
logger.debug(f"Applying XML schema with tag '{tag_name}' to function {func.__name__}")
|
||||
xml_schema = XMLTagSchema(tag_name=tag_name, example=example)
|
||||
|
||||
|
||||
# Add mappings
|
||||
if mappings:
|
||||
for mapping in mappings:
|
||||
@@ -221,7 +221,7 @@ def xml_schema(
|
||||
path=mapping.get("path", "."),
|
||||
required=mapping.get("required", True)
|
||||
)
|
||||
|
||||
|
||||
return _add_schema(func, ToolSchema(
|
||||
schema_type=SchemaType.XML,
|
||||
schema={}, # OpenAPI schema could be added here if needed
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from typing import Dict, Type, Any, List, Optional, Callable
|
||||
from agentpress.tool import Tool, SchemaType
|
||||
from utils.logger import logger
|
||||
from app.agentpress.tool import Tool, SchemaType
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Registry for managing and accessing tools.
|
||||
|
||||
|
||||
Maintains a collection of tool instances and their schemas, allowing for
|
||||
selective registration of tool functions and easy access to tool capabilities.
|
||||
|
||||
|
||||
Attributes:
|
||||
tools (Dict[str, Dict[str, Any]]): OpenAPI-style tools and schemas
|
||||
xml_tools (Dict[str, Dict[str, Any]]): XML-style tools and schemas
|
||||
|
||||
|
||||
Methods:
|
||||
register_tool: Register a tool with optional function filtering
|
||||
get_tool: Get a specific tool by name
|
||||
@@ -20,21 +20,21 @@ class ToolRegistry:
|
||||
get_openapi_schemas: Get OpenAPI schemas for function calling
|
||||
get_xml_examples: Get examples of XML tool usage
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a new ToolRegistry instance."""
|
||||
self.tools = {}
|
||||
self.xml_tools = {}
|
||||
logger.debug("Initialized new ToolRegistry instance")
|
||||
|
||||
|
||||
def register_tool(self, tool_class: Type[Tool], function_names: Optional[List[str]] = None, **kwargs):
|
||||
"""Register a tool with optional function filtering.
|
||||
|
||||
|
||||
Args:
|
||||
tool_class: The tool class to register
|
||||
function_names: Optional list of specific functions to register
|
||||
**kwargs: Additional arguments passed to tool initialization
|
||||
|
||||
|
||||
Notes:
|
||||
- If function_names is None, all functions are registered
|
||||
- Handles both OpenAPI and XML schema registration
|
||||
@@ -42,12 +42,12 @@ class ToolRegistry:
|
||||
logger.debug(f"Registering tool class: {tool_class.__name__}")
|
||||
tool_instance = tool_class(**kwargs)
|
||||
schemas = tool_instance.get_schemas()
|
||||
|
||||
|
||||
logger.debug(f"Available schemas for {tool_class.__name__}: {list(schemas.keys())}")
|
||||
|
||||
|
||||
registered_openapi = 0
|
||||
registered_xml = 0
|
||||
|
||||
|
||||
for func_name, schema_list in schemas.items():
|
||||
if function_names is None or func_name in function_names:
|
||||
for schema in schema_list:
|
||||
@@ -58,7 +58,7 @@ class ToolRegistry:
|
||||
}
|
||||
registered_openapi += 1
|
||||
logger.debug(f"Registered OpenAPI function {func_name} from {tool_class.__name__}")
|
||||
|
||||
|
||||
if schema.schema_type == SchemaType.XML and schema.xml_schema:
|
||||
self.xml_tools[schema.xml_schema.tag_name] = {
|
||||
"instance": tool_instance,
|
||||
@@ -67,40 +67,40 @@ class ToolRegistry:
|
||||
}
|
||||
registered_xml += 1
|
||||
logger.debug(f"Registered XML tag {schema.xml_schema.tag_name} -> {func_name} from {tool_class.__name__}")
|
||||
|
||||
|
||||
logger.debug(f"Tool registration complete for {tool_class.__name__}: {registered_openapi} OpenAPI functions, {registered_xml} XML tags")
|
||||
|
||||
def get_available_functions(self) -> Dict[str, Callable]:
|
||||
"""Get all available tool functions.
|
||||
|
||||
|
||||
Returns:
|
||||
Dict mapping function names to their implementations
|
||||
"""
|
||||
available_functions = {}
|
||||
|
||||
|
||||
# Get OpenAPI tool functions
|
||||
for tool_name, tool_info in self.tools.items():
|
||||
tool_instance = tool_info['instance']
|
||||
function_name = tool_name
|
||||
function = getattr(tool_instance, function_name)
|
||||
available_functions[function_name] = function
|
||||
|
||||
|
||||
# Get XML tool functions
|
||||
for tag_name, tool_info in self.xml_tools.items():
|
||||
tool_instance = tool_info['instance']
|
||||
method_name = tool_info['method']
|
||||
function = getattr(tool_instance, method_name)
|
||||
available_functions[method_name] = function
|
||||
|
||||
|
||||
logger.debug(f"Retrieved {len(available_functions)} available functions")
|
||||
return available_functions
|
||||
|
||||
def get_tool(self, tool_name: str) -> Dict[str, Any]:
|
||||
"""Get a specific tool by name.
|
||||
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool function
|
||||
|
||||
|
||||
Returns:
|
||||
Dict containing tool instance and schema, or empty dict if not found
|
||||
"""
|
||||
@@ -111,10 +111,10 @@ class ToolRegistry:
|
||||
|
||||
def get_xml_tool(self, tag_name: str) -> Dict[str, Any]:
|
||||
"""Get tool info by XML tag name.
|
||||
|
||||
|
||||
Args:
|
||||
tag_name: XML tag name for the tool
|
||||
|
||||
|
||||
Returns:
|
||||
Dict containing tool instance, method name, and schema
|
||||
"""
|
||||
@@ -125,12 +125,12 @@ class ToolRegistry:
|
||||
|
||||
def get_openapi_schemas(self) -> List[Dict[str, Any]]:
|
||||
"""Get OpenAPI schemas for function calling.
|
||||
|
||||
|
||||
Returns:
|
||||
List of OpenAPI-compatible schema definitions
|
||||
"""
|
||||
schemas = [
|
||||
tool_info['schema'].schema
|
||||
tool_info['schema'].schema
|
||||
for tool_info in self.tools.values()
|
||||
if tool_info['schema'].schema_type == SchemaType.OPENAPI
|
||||
]
|
||||
@@ -139,7 +139,7 @@ class ToolRegistry:
|
||||
|
||||
def get_xml_examples(self) -> Dict[str, str]:
|
||||
"""Get all XML tag examples.
|
||||
|
||||
|
||||
Returns:
|
||||
Dict mapping tag names to their example usage
|
||||
"""
|
||||
|
||||
@@ -105,6 +105,13 @@ class SandboxSettings(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class DaytonaSettings(BaseModel):
|
||||
daytona_api_key: str
|
||||
daytona_server_url: Optional[str] = Field("https://app.daytona.io/api", description="")
|
||||
daytona_target: Optional[str] = Field("asia", description="enum ['asia', 'eu', 'us']")
|
||||
sandbox_image_name: Optional[str]= Field("kortix/suna:0.1.3", description="")
|
||||
sandbox_entrypoint: Optional[str]= Field("/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf", description="")
|
||||
|
||||
class MCPServerConfig(BaseModel):
|
||||
"""Configuration for a single MCP server"""
|
||||
|
||||
@@ -167,6 +174,9 @@ class AppConfig(BaseModel):
|
||||
run_flow_config: Optional[RunflowSettings] = Field(
|
||||
None, description="Run flow configuration"
|
||||
)
|
||||
daytona_config: Optional[DaytonaSettings] = Field(
|
||||
None, description="Daytona configuration"
|
||||
)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
@@ -268,6 +278,11 @@ class Config:
|
||||
sandbox_settings = SandboxSettings(**sandbox_config)
|
||||
else:
|
||||
sandbox_settings = SandboxSettings()
|
||||
daytona_config = raw_config.get("daytona", {})
|
||||
if daytona_config:
|
||||
daytona_settings = DaytonaSettings(**daytona_config)
|
||||
else:
|
||||
daytona_settings = DaytonaSettings()
|
||||
|
||||
mcp_config = raw_config.get("mcp", {})
|
||||
mcp_settings = None
|
||||
@@ -296,6 +311,7 @@ class Config:
|
||||
"search_config": search_settings,
|
||||
"mcp_config": mcp_settings,
|
||||
"run_flow_config": run_flow_settings,
|
||||
"daytona_config": daytona_settings,
|
||||
}
|
||||
|
||||
self._config = AppConfig(**config_dict)
|
||||
@@ -307,6 +323,9 @@ class Config:
|
||||
@property
|
||||
def sandbox(self) -> SandboxSettings:
|
||||
return self._config.sandbox
|
||||
@property
|
||||
def daytona(self) -> DaytonaSettings:
|
||||
return self._config.daytona_config
|
||||
|
||||
@property
|
||||
def browser_config(self) -> Optional[BrowserSettings]:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
structlog
|
||||
daytona
|
||||
litellm
|
||||
+29
-28
@@ -1,16 +1,17 @@
|
||||
from daytona_sdk import Daytona, DaytonaConfig, CreateSandboxFromImageParams, Sandbox, SessionExecuteRequest, Resources, SandboxState
|
||||
from daytona import Daytona, DaytonaConfig, CreateSandboxFromImageParams, Sandbox, SessionExecuteRequest, Resources, SandboxState
|
||||
from dotenv import load_dotenv
|
||||
from utils.logger import logger
|
||||
from utils.config import config
|
||||
from utils.config import Configuration
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from app.utils.logger import logger
|
||||
# from app.utils.config import config
|
||||
# from utils.config import config
|
||||
# from app.utils.config import Configuration
|
||||
from app.config import config
|
||||
# load_dotenv()
|
||||
daytona_settings=config.daytona
|
||||
logger.debug("Initializing Daytona sandbox configuration")
|
||||
daytona_config = DaytonaConfig(
|
||||
api_key=config.DAYTONA_API_KEY,
|
||||
server_url=config.DAYTONA_SERVER_URL,
|
||||
target=config.DAYTONA_TARGET
|
||||
api_key=daytona_settings.daytona_api_key,
|
||||
server_url=daytona_settings.daytona_server_url,
|
||||
target=daytona_settings.daytona_target
|
||||
)
|
||||
|
||||
if daytona_config.api_key:
|
||||
@@ -33,12 +34,12 @@ logger.debug("Daytona client initialized")
|
||||
|
||||
async def get_or_start_sandbox(sandbox_id: str):
|
||||
"""Retrieve a sandbox by ID, check its state, and start it if needed."""
|
||||
|
||||
|
||||
logger.info(f"Getting or starting sandbox with ID: {sandbox_id}")
|
||||
|
||||
|
||||
try:
|
||||
sandbox = daytona.get(sandbox_id)
|
||||
|
||||
|
||||
# Check if sandbox needs to be started
|
||||
if sandbox.state == SandboxState.ARCHIVED or sandbox.state == SandboxState.STOPPED:
|
||||
logger.info(f"Sandbox is in {sandbox.state} state. Starting...")
|
||||
@@ -48,16 +49,16 @@ async def get_or_start_sandbox(sandbox_id: str):
|
||||
# sleep(5)
|
||||
# Refresh sandbox state after starting
|
||||
sandbox = daytona.get(sandbox_id)
|
||||
|
||||
|
||||
# Start supervisord in a session when restarting
|
||||
start_supervisord_session(sandbox)
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting sandbox: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
logger.info(f"Sandbox {sandbox_id} is ready")
|
||||
return sandbox
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving or starting sandbox: {str(e)}")
|
||||
raise e
|
||||
@@ -68,7 +69,7 @@ def start_supervisord_session(sandbox: Sandbox):
|
||||
try:
|
||||
logger.info(f"Creating session {session_id} for supervisord")
|
||||
sandbox.process.create_session(session_id)
|
||||
|
||||
|
||||
# Execute supervisord command
|
||||
sandbox.process.execute_session_command(session_id, SessionExecuteRequest(
|
||||
command="exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
|
||||
@@ -81,17 +82,17 @@ def start_supervisord_session(sandbox: Sandbox):
|
||||
|
||||
def create_sandbox(password: str, project_id: str = None):
|
||||
"""Create a new sandbox with all required services configured and running."""
|
||||
|
||||
|
||||
logger.debug("Creating new Daytona sandbox environment")
|
||||
logger.debug("Configuring sandbox with browser-use image and environment variables")
|
||||
|
||||
|
||||
labels = None
|
||||
if project_id:
|
||||
logger.debug(f"Using sandbox_id as label: {project_id}")
|
||||
labels = {'id': project_id}
|
||||
|
||||
|
||||
params = CreateSandboxFromImageParams(
|
||||
image=Configuration.SANDBOX_IMAGE_NAME,
|
||||
image=daytona_settings.sandbox_image_name,
|
||||
public=True,
|
||||
labels=labels,
|
||||
env_vars={
|
||||
@@ -115,30 +116,30 @@ def create_sandbox(password: str, project_id: str = None):
|
||||
auto_stop_interval=15,
|
||||
auto_archive_interval=24 * 60,
|
||||
)
|
||||
|
||||
|
||||
# Create the sandbox
|
||||
sandbox = daytona.create(params)
|
||||
logger.debug(f"Sandbox created with ID: {sandbox.id}")
|
||||
|
||||
|
||||
# Start supervisord in a session for new sandbox
|
||||
start_supervisord_session(sandbox)
|
||||
|
||||
|
||||
logger.debug(f"Sandbox environment successfully initialized")
|
||||
return sandbox
|
||||
|
||||
async def delete_sandbox(sandbox_id: str):
|
||||
"""Delete a sandbox by its ID."""
|
||||
logger.info(f"Deleting sandbox with ID: {sandbox_id}")
|
||||
|
||||
|
||||
try:
|
||||
# Get the sandbox
|
||||
sandbox = daytona.get(sandbox_id)
|
||||
|
||||
|
||||
# Delete the sandbox
|
||||
daytona.delete(sandbox)
|
||||
|
||||
|
||||
logger.info(f"Successfully deleted sandbox {sandbox_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
|
||||
raise e
|
||||
raise e
|
||||
|
||||
+55
-50
@@ -1,72 +1,77 @@
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from agentpress.thread_manager import ThreadManager
|
||||
from typing import Optional,ClassVar
|
||||
from pydantic import Field
|
||||
# from app.agentpress.thread_manager import ThreadManager
|
||||
from app.tool.base import BaseTool, ToolResult
|
||||
from daytona_sdk import Sandbox
|
||||
from daytona.sandbox import get_or_start_sandbox
|
||||
from utils.logger import logger
|
||||
from utils.files_utils import clean_path
|
||||
from daytona import Sandbox
|
||||
from app.daytona.sandbox import get_or_start_sandbox
|
||||
from app.utils.logger import logger
|
||||
from app.utils.files_utils import clean_path
|
||||
|
||||
class SandboxToolsBase(BaseTool):
|
||||
"""Base class for all sandbox tools that provides project-based sandbox access."""
|
||||
|
||||
# Class variable to track if sandbox URLs have been printed
|
||||
_urls_printed = False
|
||||
_urls_printed: ClassVar[bool] = False
|
||||
|
||||
def __init__(self, project_id: str, thread_manager: Optional[ThreadManager] = None):
|
||||
super().__init__()
|
||||
self.project_id = project_id
|
||||
self.thread_manager = thread_manager
|
||||
self.workspace_path = "/workspace"
|
||||
self._sandbox = None
|
||||
self._sandbox_id = None
|
||||
self._sandbox_pass = None
|
||||
# Required fields
|
||||
project_id: Optional[str] = None
|
||||
# thread_manager: Optional[ThreadManager] = None
|
||||
|
||||
async def _ensure_sandbox(self) -> Sandbox:
|
||||
"""Ensure we have a valid sandbox instance, retrieving it from the project if needed."""
|
||||
if self._sandbox is None:
|
||||
try:
|
||||
# Get database client
|
||||
client = await self.thread_manager.db.client
|
||||
# Private fields (not part of the model schema)
|
||||
_sandbox: Optional[Sandbox] = None
|
||||
_sandbox_id: Optional[str] = None
|
||||
_sandbox_pass: Optional[str] = None
|
||||
workspace_path: str = Field(default="/workspace", exclude=True)
|
||||
|
||||
# Get project data
|
||||
project = await client.table('projects').select('*').eq('project_id', self.project_id).execute()
|
||||
if not project.data or len(project.data) == 0:
|
||||
raise ValueError(f"Project {self.project_id} not found")
|
||||
class Config:
|
||||
arbitrary_types_allowed = True # Allow non-pydantic types like ThreadManager
|
||||
underscore_attrs_are_private = True
|
||||
|
||||
project_data = project.data[0]
|
||||
sandbox_info = project_data.get('sandbox', {})
|
||||
# async def _ensure_sandbox(self) -> Sandbox:
|
||||
# """Ensure we have a valid sandbox instance, retrieving it from the project if needed."""
|
||||
# if self._sandbox is None:
|
||||
# try:
|
||||
# # Get database client
|
||||
# client = await self.thread_manager.db.client
|
||||
|
||||
if not sandbox_info.get('id'):
|
||||
raise ValueError(f"No sandbox found for project {self.project_id}")
|
||||
# # Get project data
|
||||
# project = await client.table('projects').select('*').eq('project_id', self.project_id).execute()
|
||||
# if not project.data or len(project.data) == 0:
|
||||
# raise ValueError(f"Project {self.project_id} not found")
|
||||
|
||||
# Store sandbox info
|
||||
self._sandbox_id = sandbox_info['id']
|
||||
self._sandbox_pass = sandbox_info.get('pass')
|
||||
# project_data = project.data[0]
|
||||
# sandbox_info = project_data.get('sandbox', {})
|
||||
|
||||
# Get or start the sandbox
|
||||
self._sandbox = await get_or_start_sandbox(self._sandbox_id)
|
||||
# if not sandbox_info.get('id'):
|
||||
# raise ValueError(f"No sandbox found for project {self.project_id}")
|
||||
|
||||
# # Log URLs if not already printed
|
||||
# if not SandboxToolsBase._urls_printed:
|
||||
# vnc_link = self._sandbox.get_preview_link(6080)
|
||||
# website_link = self._sandbox.get_preview_link(8080)
|
||||
# # Store sandbox info
|
||||
# self._sandbox_id = sandbox_info['id']
|
||||
# self._sandbox_pass = sandbox_info.get('pass')
|
||||
|
||||
# vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link)
|
||||
# website_url = website_link.url if hasattr(website_link, 'url') else str(website_link)
|
||||
# # Get or start the sandbox
|
||||
# self._sandbox = await get_or_start_sandbox(self._sandbox_id)
|
||||
|
||||
# print("\033[95m***")
|
||||
# print(f"VNC URL: {vnc_url}")
|
||||
# print(f"Website URL: {website_url}")
|
||||
# print("***\033[0m")
|
||||
# SandboxToolsBase._urls_printed = True
|
||||
# # # Log URLs if not already printed
|
||||
# # if not SandboxToolsBase._urls_printed:
|
||||
# # vnc_link = self._sandbox.get_preview_link(6080)
|
||||
# # website_link = self._sandbox.get_preview_link(8080)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving sandbox for project {self.project_id}: {str(e)}", exc_info=True)
|
||||
raise e
|
||||
# # vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link)
|
||||
# # website_url = website_link.url if hasattr(website_link, 'url') else str(website_link)
|
||||
|
||||
return self._sandbox
|
||||
# # print("\033[95m***")
|
||||
# # print(f"VNC URL: {vnc_url}")
|
||||
# # print(f"Website URL: {website_url}")
|
||||
# # print("***\033[0m")
|
||||
# # SandboxToolsBase._urls_printed = True
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error retrieving sandbox for project {self.project_id}: {str(e)}", exc_info=True)
|
||||
# raise e
|
||||
|
||||
# return self._sandbox
|
||||
|
||||
@property
|
||||
def sandbox(self) -> Sandbox:
|
||||
|
||||
+41
-43
@@ -3,7 +3,7 @@ from pydantic import BaseModel, Field
|
||||
from typing import Any, Dict, Optional, Union
|
||||
import inspect
|
||||
import json
|
||||
from utils.logger import logger
|
||||
from app.utils.logger import logger
|
||||
|
||||
# class BaseTool(ABC, BaseModel):
|
||||
# name: str
|
||||
@@ -33,6 +33,46 @@ from utils.logger import logger
|
||||
# }
|
||||
|
||||
|
||||
|
||||
class ToolResult(BaseModel):
|
||||
"""Represents the result of a tool execution."""
|
||||
|
||||
output: Any = Field(default=None)
|
||||
error: Optional[str] = Field(default=None)
|
||||
base64_image: Optional[str] = Field(default=None)
|
||||
system: Optional[str] = Field(default=None)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __bool__(self):
|
||||
return any(getattr(self, field) for field in self.__fields__)
|
||||
|
||||
def __add__(self, other: "ToolResult"):
|
||||
def combine_fields(
|
||||
field: Optional[str], other_field: Optional[str], concatenate: bool = True
|
||||
):
|
||||
if field and other_field:
|
||||
if concatenate:
|
||||
return field + other_field
|
||||
raise ValueError("Cannot combine tool results")
|
||||
return field or other_field
|
||||
|
||||
return ToolResult(
|
||||
output=combine_fields(self.output, other.output),
|
||||
error=combine_fields(self.error, other.error),
|
||||
base64_image=combine_fields(self.base64_image, other.base64_image, False),
|
||||
system=combine_fields(self.system, other.system),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Error: {self.error}" if self.error else self.output
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""Returns a new ToolResult with the given fields replaced."""
|
||||
# return self.copy(update=kwargs)
|
||||
return type(self)(**{**self.dict(), **kwargs})
|
||||
|
||||
class BaseTool(ABC, BaseModel):
|
||||
"""Consolidated base class for all tools combining BaseModel and Tool functionality.
|
||||
|
||||
@@ -129,48 +169,6 @@ class BaseTool(ABC, BaseModel):
|
||||
"""
|
||||
logger.debug(f"Tool {self.__class__.__name__} returned failed result: {msg}")
|
||||
return ToolResult(error=msg)
|
||||
|
||||
|
||||
class ToolResult(BaseModel):
|
||||
"""Represents the result of a tool execution."""
|
||||
|
||||
output: Any = Field(default=None)
|
||||
error: Optional[str] = Field(default=None)
|
||||
base64_image: Optional[str] = Field(default=None)
|
||||
system: Optional[str] = Field(default=None)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def __bool__(self):
|
||||
return any(getattr(self, field) for field in self.__fields__)
|
||||
|
||||
def __add__(self, other: "ToolResult"):
|
||||
def combine_fields(
|
||||
field: Optional[str], other_field: Optional[str], concatenate: bool = True
|
||||
):
|
||||
if field and other_field:
|
||||
if concatenate:
|
||||
return field + other_field
|
||||
raise ValueError("Cannot combine tool results")
|
||||
return field or other_field
|
||||
|
||||
return ToolResult(
|
||||
output=combine_fields(self.output, other.output),
|
||||
error=combine_fields(self.error, other.error),
|
||||
base64_image=combine_fields(self.base64_image, other.base64_image, False),
|
||||
system=combine_fields(self.system, other.system),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Error: {self.error}" if self.error else self.output
|
||||
|
||||
def replace(self, **kwargs):
|
||||
"""Returns a new ToolResult with the given fields replaced."""
|
||||
# return self.copy(update=kwargs)
|
||||
return type(self)(**{**self.dict(), **kwargs})
|
||||
|
||||
|
||||
class CLIResult(ToolResult):
|
||||
"""A ToolResult that can be rendered as a CLI output."""
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import base64
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict,Literal
|
||||
import os
|
||||
|
||||
from pydantic import Field
|
||||
# from agentpress.tool import Tool, ToolResult, openapi_schema, xml_schema
|
||||
from app.daytona.tool_base import SandboxToolsBase, Sandbox
|
||||
from app.tool.base import ToolResult
|
||||
@@ -775,7 +775,19 @@ class ComputerUseTool(SandboxToolsBase):
|
||||
mouse_x: int = Field(default=0, exclude=True)
|
||||
mouse_y: int = Field(default=0, exclude=True)
|
||||
api_base_url: Optional[str] = Field(default=None, exclude=True)
|
||||
sandbox: Optional[Sandbox] = Field(default=None, exclude=True)
|
||||
def __init__(self, sandbox: Optional[Sandbox] = None, **data):
|
||||
"""Initialize with optional sandbox."""
|
||||
super().__init__(**data)
|
||||
if sandbox is not None:
|
||||
self._sandbox = sandbox # 直接操作基类的私有属性
|
||||
self.api_base_url = sandbox.get_preview_link(8000)
|
||||
logging.info(f"Initialized ComputerUseTool with API URL: {self.api_base_url}")
|
||||
|
||||
@classmethod
|
||||
def create_with_sandbox(cls, sandbox: Sandbox) -> "ComputerUseTool":
|
||||
"""Factory method to create a tool with sandbox."""
|
||||
return cls(sandbox=sandbox) # 通过构造函数初始化
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""Get or create aiohttp session for API requests."""
|
||||
if self.session is None or self.session.closed:
|
||||
@@ -1008,21 +1020,31 @@ class ComputerUseTool(SandboxToolsBase):
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
||||
self.session = None
|
||||
|
||||
def __del__(self):
|
||||
"""Ensure cleanup when object is destroyed."""
|
||||
if self.session is not None:
|
||||
"""Ensure cleanup on destruction."""
|
||||
if hasattr(self, 'session') and self.session is not None:
|
||||
try:
|
||||
asyncio.run(self.cleanup())
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.cleanup())
|
||||
loop.close()
|
||||
@classmethod
|
||||
def create_with_sandbox(cls, sandbox: Sandbox) -> "ComputerUseTool":
|
||||
"""Factory method to create a ComputerUseTool with a sandbox connection."""
|
||||
tool = cls()
|
||||
tool.sandbox = sandbox
|
||||
tool.api_base_url = sandbox.get_preview_link(8000)
|
||||
logging.info(f"Initialized Computer Use Tool with API URL: {tool.api_base_url}")
|
||||
return tool
|
||||
# def __del__(self):
|
||||
# """Ensure cleanup when object is destroyed."""
|
||||
# if self.session is not None:
|
||||
# try:
|
||||
# asyncio.run(self.cleanup())
|
||||
# except RuntimeError:
|
||||
# loop = asyncio.new_event_loop()
|
||||
# loop.run_until_complete(self.cleanup())
|
||||
# loop.close()
|
||||
# @classmethod
|
||||
# def create_with_sandbox(cls, sandbox: Sandbox) -> "ComputerUseTool":
|
||||
# """Factory method to create a ComputerUseTool with a sandbox connection."""
|
||||
# tool = cls()
|
||||
# tool.sandbox = sandbox
|
||||
# tool.api_base_url = sandbox.get_preview_link(8000)
|
||||
# logging.info(f"Initialized Computer Use Tool with API URL: {tool.api_base_url}")
|
||||
# return tool
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from agent.tools.data_providers.RapidDataProviderBase import RapidDataProviderBase, EndpointSchema
|
||||
|
||||
|
||||
class ActiveJobsProvider(RapidDataProviderBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"active_jobs": {
|
||||
"route": "/active-ats-7d",
|
||||
"method": "GET",
|
||||
"name": "Active Jobs Search",
|
||||
"description": "Get active job listings with various filter options.",
|
||||
"payload": {
|
||||
"limit": "Optional. Number of jobs per API call (10-100). Default is 100.",
|
||||
"offset": "Optional. Offset for pagination. Default is 0.",
|
||||
"title_filter": "Optional. Search terms for job title.",
|
||||
"advanced_title_filter": "Optional. Advanced title filter with operators (can't be used with title_filter).",
|
||||
"location_filter": "Optional. Filter by location(s). Use full names like 'United States' not 'US'.",
|
||||
"description_filter": "Optional. Filter on job description content.",
|
||||
"organization_filter": "Optional. Filter by company name(s).",
|
||||
"description_type": "Optional. Return format for description: 'text' or 'html'. Leave empty to exclude descriptions.",
|
||||
"source": "Optional. Filter by ATS source.",
|
||||
"date_filter": "Optional. Filter by posting date (greater than).",
|
||||
"ai_employment_type_filter": "Optional. Filter by employment type (FULL_TIME, PART_TIME, etc).",
|
||||
"ai_work_arrangement_filter": "Optional. Filter by work arrangement (On-site, Hybrid, Remote OK, Remote Solely).",
|
||||
"ai_experience_level_filter": "Optional. Filter by experience level (0-2, 2-5, 5-10, 10+).",
|
||||
"li_organization_slug_filter": "Optional. Filter by LinkedIn company slug.",
|
||||
"li_organization_slug_exclusion_filter": "Optional. Exclude LinkedIn company slugs.",
|
||||
"li_industry_filter": "Optional. Filter by LinkedIn industry.",
|
||||
"li_organization_specialties_filter": "Optional. Filter by LinkedIn company specialties.",
|
||||
"li_organization_description_filter": "Optional. Filter by LinkedIn company description."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
base_url = "https://active-jobs-db.p.rapidapi.com"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
tool = ActiveJobsProvider()
|
||||
|
||||
# Example for searching active jobs
|
||||
jobs = tool.call_endpoint(
|
||||
route="active_jobs",
|
||||
payload={
|
||||
"limit": "10",
|
||||
"offset": "0",
|
||||
"title_filter": "\"Data Engineer\"",
|
||||
"location_filter": "\"United States\" OR \"United Kingdom\"",
|
||||
"description_type": "text"
|
||||
}
|
||||
)
|
||||
print("Active Jobs:", jobs)
|
||||
@@ -1,191 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from agent.tools.data_providers.RapidDataProviderBase import RapidDataProviderBase, EndpointSchema
|
||||
|
||||
|
||||
class AmazonProvider(RapidDataProviderBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"search": {
|
||||
"route": "/search",
|
||||
"method": "GET",
|
||||
"name": "Amazon Product Search",
|
||||
"description": "Search for products on Amazon with various filters and parameters.",
|
||||
"payload": {
|
||||
"query": "Search query (supports both free-form text queries or a product asin)",
|
||||
"page": "Results page to return (default: 1)",
|
||||
"country": "Sets the Amazon domain, marketplace country, language and currency (default: US)",
|
||||
"sort_by": "Return the results in a specific sort order (RELEVANCE, LOWEST_PRICE, HIGHEST_PRICE, REVIEWS, NEWEST, BEST_SELLERS)",
|
||||
"product_condition": "Return products in a specific condition (ALL, NEW, USED, RENEWED, COLLECTIBLE)",
|
||||
"is_prime": "Only return prime products (boolean)",
|
||||
"deals_and_discounts": "Return deals and discounts in a specific condition (NONE, ALL_DISCOUNTS, TODAYS_DEALS)",
|
||||
"category_id": "Find products in a specific category / department (optional)",
|
||||
"category": "Filter by specific numeric Amazon category (optional)",
|
||||
"min_price": "Only return product offers with price greater than a certain value (optional)",
|
||||
"max_price": "Only return product offers with price lower than a certain value (optional)",
|
||||
"brand": "Find products with a specific brand (optional)",
|
||||
"seller_id": "Find products sold by specific seller (optional)",
|
||||
"four_stars_and_up": "Return product listings with ratings of 4 stars & up (optional)",
|
||||
"additional_filters": "Any filters available on the Amazon page but not part of this endpoint's parameters (optional)"
|
||||
}
|
||||
},
|
||||
"product-details": {
|
||||
"route": "/product-details",
|
||||
"method": "GET",
|
||||
"name": "Amazon Product Details",
|
||||
"description": "Get detailed information about specific Amazon products by ASIN.",
|
||||
"payload": {
|
||||
"asin": "Product ASIN for which to get details. Supports batching of up to 10 ASINs in a single request, separated by comma.",
|
||||
"country": "Sets the Amazon domain, marketplace country, language and currency (default: US)",
|
||||
"more_info_query": "A query to search and get more info about the product as part of Product Information, Customer Q&As, and Customer Reviews (optional)",
|
||||
"fields": "A comma separated list of product fields to include in the response (field projection). By default all fields are returned. (optional)"
|
||||
}
|
||||
},
|
||||
"products-by-category": {
|
||||
"route": "/products-by-category",
|
||||
"method": "GET",
|
||||
"name": "Amazon Products by Category",
|
||||
"description": "Get products from a specific Amazon category.",
|
||||
"payload": {
|
||||
"category_id": "The Amazon category for which to return results. Multiple category values can be separated by comma.",
|
||||
"page": "Page to return (default: 1)",
|
||||
"country": "Sets the Amazon domain, marketplace country, language and currency (default: US)",
|
||||
"sort_by": "Return the results in a specific sort order (RELEVANCE, LOWEST_PRICE, HIGHEST_PRICE, REVIEWS, NEWEST, BEST_SELLERS)",
|
||||
"min_price": "Only return product offers with price greater than a certain value (optional)",
|
||||
"max_price": "Only return product offers with price lower than a certain value (optional)",
|
||||
"product_condition": "Return products in a specific condition (ALL, NEW, USED, RENEWED, COLLECTIBLE)",
|
||||
"brand": "Only return products of a specific brand. Multiple brands can be specified as a comma separated list (optional)",
|
||||
"is_prime": "Only return prime products (boolean)",
|
||||
"deals_and_discounts": "Return deals and discounts in a specific condition (NONE, ALL_DISCOUNTS, TODAYS_DEALS)",
|
||||
"four_stars_and_up": "Return product listings with ratings of 4 stars & up (optional)",
|
||||
"additional_filters": "Any filters available on the Amazon page but not part of this endpoint's parameters (optional)"
|
||||
}
|
||||
},
|
||||
"product-reviews": {
|
||||
"route": "/product-reviews",
|
||||
"method": "GET",
|
||||
"name": "Amazon Product Reviews",
|
||||
"description": "Get customer reviews for a specific Amazon product by ASIN.",
|
||||
"payload": {
|
||||
"asin": "Product asin for which to get reviews.",
|
||||
"country": "Sets the Amazon domain, marketplace country, language and currency (default: US)",
|
||||
"page": "Results page to return (default: 1)",
|
||||
"sort_by": "Return reviews in a specific sort order (TOP_REVIEWS, MOST_RECENT)",
|
||||
"star_rating": "Only return reviews with a specific star rating (ALL, 5_STARS, 4_STARS, 3_STARS, 2_STARS, 1_STARS, POSITIVE, CRITICAL)",
|
||||
"verified_purchases_only": "Only return reviews by reviewers who made a verified purchase (boolean)",
|
||||
"images_or_videos_only": "Only return reviews containing images and / or videos (boolean)",
|
||||
"current_format_only": "Only return reviews of the current format (product variant - e.g. Color) (boolean)"
|
||||
}
|
||||
},
|
||||
"seller-profile": {
|
||||
"route": "/seller-profile",
|
||||
"method": "GET",
|
||||
"name": "Amazon Seller Profile",
|
||||
"description": "Get detailed information about a specific Amazon seller by Seller ID.",
|
||||
"payload": {
|
||||
"seller_id": "The Amazon Seller ID for which to get seller profile details",
|
||||
"country": "Sets the Amazon domain, marketplace country, language and currency (default: US)",
|
||||
"fields": "A comma separated list of seller profile fields to include in the response (field projection). By default all fields are returned. (optional)"
|
||||
}
|
||||
},
|
||||
"seller-reviews": {
|
||||
"route": "/seller-reviews",
|
||||
"method": "GET",
|
||||
"name": "Amazon Seller Reviews",
|
||||
"description": "Get customer reviews for a specific Amazon seller by Seller ID.",
|
||||
"payload": {
|
||||
"seller_id": "The Amazon Seller ID for which to get seller reviews",
|
||||
"country": "Sets the Amazon domain, marketplace country, language and currency (default: US)",
|
||||
"star_rating": "Only return reviews with a specific star rating or positive / negative sentiment (ALL, 5_STARS, 4_STARS, 3_STARS, 2_STARS, 1_STARS, POSITIVE, CRITICAL)",
|
||||
"page": "The page of seller feedback results to retrieve (default: 1)",
|
||||
"fields": "A comma separated list of seller review fields to include in the response (field projection). By default all fields are returned. (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
base_url = "https://real-time-amazon-data.p.rapidapi.com"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
tool = AmazonProvider()
|
||||
|
||||
# Example for product search
|
||||
search_result = tool.call_endpoint(
|
||||
route="search",
|
||||
payload={
|
||||
"query": "Phone",
|
||||
"page": 1,
|
||||
"country": "US",
|
||||
"sort_by": "RELEVANCE",
|
||||
"product_condition": "ALL",
|
||||
"is_prime": False,
|
||||
"deals_and_discounts": "NONE"
|
||||
}
|
||||
)
|
||||
print("Search Result:", search_result)
|
||||
|
||||
# Example for product details
|
||||
details_result = tool.call_endpoint(
|
||||
route="product-details",
|
||||
payload={
|
||||
"asin": "B07ZPKBL9V",
|
||||
"country": "US"
|
||||
}
|
||||
)
|
||||
print("Product Details:", details_result)
|
||||
|
||||
# Example for products by category
|
||||
category_result = tool.call_endpoint(
|
||||
route="products-by-category",
|
||||
payload={
|
||||
"category_id": "2478868012",
|
||||
"page": 1,
|
||||
"country": "US",
|
||||
"sort_by": "RELEVANCE",
|
||||
"product_condition": "ALL",
|
||||
"is_prime": False,
|
||||
"deals_and_discounts": "NONE"
|
||||
}
|
||||
)
|
||||
print("Category Products:", category_result)
|
||||
|
||||
# Example for product reviews
|
||||
reviews_result = tool.call_endpoint(
|
||||
route="product-reviews",
|
||||
payload={
|
||||
"asin": "B07ZPKN6YR",
|
||||
"country": "US",
|
||||
"page": 1,
|
||||
"sort_by": "TOP_REVIEWS",
|
||||
"star_rating": "ALL",
|
||||
"verified_purchases_only": False,
|
||||
"images_or_videos_only": False,
|
||||
"current_format_only": False
|
||||
}
|
||||
)
|
||||
print("Product Reviews:", reviews_result)
|
||||
|
||||
# Example for seller profile
|
||||
seller_result = tool.call_endpoint(
|
||||
route="seller-profile",
|
||||
payload={
|
||||
"seller_id": "A02211013Q5HP3OMSZC7W",
|
||||
"country": "US"
|
||||
}
|
||||
)
|
||||
print("Seller Profile:", seller_result)
|
||||
|
||||
# Example for seller reviews
|
||||
seller_reviews_result = tool.call_endpoint(
|
||||
route="seller-reviews",
|
||||
payload={
|
||||
"seller_id": "A02211013Q5HP3OMSZC7W",
|
||||
"country": "US",
|
||||
"star_rating": "ALL",
|
||||
"page": 1
|
||||
}
|
||||
)
|
||||
print("Seller Reviews:", seller_reviews_result)
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from agent.tools.data_providers.RapidDataProviderBase import RapidDataProviderBase, EndpointSchema
|
||||
|
||||
|
||||
class LinkedinProvider(RapidDataProviderBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"person": {
|
||||
"route": "/person",
|
||||
"method": "POST",
|
||||
"name": "Person Data",
|
||||
"description": "Fetches any Linkedin profiles data including skills, certificates, experiences, qualifications and much more.",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"person_urn": {
|
||||
"route": "/person_urn",
|
||||
"method": "POST",
|
||||
"name": "Person Data (Using Urn)",
|
||||
"description": "It takes profile urn instead of profile public identifier in input",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL or URN"
|
||||
}
|
||||
},
|
||||
"person_deep": {
|
||||
"route": "/person_deep",
|
||||
"method": "POST",
|
||||
"name": "Person Data (Deep)",
|
||||
"description": "Fetches all experiences, educations, skills, languages, publications... related to a profile.",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"profile_updates": {
|
||||
"route": "/profile_updates",
|
||||
"method": "GET",
|
||||
"name": "Person Posts (WITH PAGINATION)",
|
||||
"description": "Fetches posts of a linkedin profile alongwith reactions, comments, postLink and reposts data.",
|
||||
"payload": {
|
||||
"profile_url": "LinkedIn Profile URL",
|
||||
"page": "Page number",
|
||||
"reposts": "Include reposts (1 or 0)",
|
||||
"comments": "Include comments (1 or 0)"
|
||||
}
|
||||
},
|
||||
"profile_recent_comments": {
|
||||
"route": "/profile_recent_comments",
|
||||
"method": "POST",
|
||||
"name": "Person Recent Activity (Comments on Posts)",
|
||||
"description": "Fetches 20 most recent comments posted by a linkedin user (per page).",
|
||||
"payload": {
|
||||
"profile_url": "LinkedIn Profile URL",
|
||||
"page": "Page number",
|
||||
"paginationToken": "Token for pagination"
|
||||
}
|
||||
},
|
||||
"comments_from_recent_activity": {
|
||||
"route": "/comments_from_recent_activity",
|
||||
"method": "GET",
|
||||
"name": "Comments from recent activity",
|
||||
"description": "Fetches recent comments posted by a person as per his recent activity tab.",
|
||||
"payload": {
|
||||
"profile_url": "LinkedIn Profile URL",
|
||||
"page": "Page number"
|
||||
}
|
||||
},
|
||||
"person_skills": {
|
||||
"route": "/person_skills",
|
||||
"method": "POST",
|
||||
"name": "Person Skills",
|
||||
"description": "Scraper all skills of a linkedin user",
|
||||
"payload": {
|
||||
"link": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"email_to_linkedin_profile": {
|
||||
"route": "/email_to_linkedin_profile",
|
||||
"method": "POST",
|
||||
"name": "Email to LinkedIn Profile",
|
||||
"description": "Finds LinkedIn profile associated with an email address",
|
||||
"payload": {
|
||||
"email": "Email address to search"
|
||||
}
|
||||
},
|
||||
"company": {
|
||||
"route": "/company",
|
||||
"method": "POST",
|
||||
"name": "Company Data",
|
||||
"description": "Fetches LinkedIn company profile data",
|
||||
"payload": {
|
||||
"link": "LinkedIn Company URL"
|
||||
}
|
||||
},
|
||||
"web_domain": {
|
||||
"route": "/web-domain",
|
||||
"method": "POST",
|
||||
"name": "Web Domain to Company",
|
||||
"description": "Fetches LinkedIn company profile data from a web domain",
|
||||
"payload": {
|
||||
"link": "Website domain (e.g., huzzle.app)"
|
||||
}
|
||||
},
|
||||
"similar_profiles": {
|
||||
"route": "/similar_profiles",
|
||||
"method": "GET",
|
||||
"name": "Similar Profiles",
|
||||
"description": "Fetches profiles similar to a given LinkedIn profile",
|
||||
"payload": {
|
||||
"profileUrl": "LinkedIn Profile URL"
|
||||
}
|
||||
},
|
||||
"company_jobs": {
|
||||
"route": "/company_jobs",
|
||||
"method": "POST",
|
||||
"name": "Company Jobs",
|
||||
"description": "Fetches job listings from a LinkedIn company page",
|
||||
"payload": {
|
||||
"company_url": "LinkedIn Company URL",
|
||||
"count": "Number of job listings to fetch"
|
||||
}
|
||||
},
|
||||
"company_updates": {
|
||||
"route": "/company_updates",
|
||||
"method": "GET",
|
||||
"name": "Company Posts",
|
||||
"description": "Fetches posts from a LinkedIn company page",
|
||||
"payload": {
|
||||
"company_url": "LinkedIn Company URL",
|
||||
"page": "Page number",
|
||||
"reposts": "Include reposts (0, 1, or 2)",
|
||||
"comments": "Include comments (0, 1, or 2)"
|
||||
}
|
||||
},
|
||||
"company_employee": {
|
||||
"route": "/company_employee",
|
||||
"method": "GET",
|
||||
"name": "Company Employees",
|
||||
"description": "Fetches employees of a LinkedIn company using company ID",
|
||||
"payload": {
|
||||
"companyId": "LinkedIn Company ID",
|
||||
"page": "Page number"
|
||||
}
|
||||
},
|
||||
"company_updates_post": {
|
||||
"route": "/company_updates",
|
||||
"method": "POST",
|
||||
"name": "Company Posts (POST)",
|
||||
"description": "Fetches posts from a LinkedIn company page with specific count parameters",
|
||||
"payload": {
|
||||
"company_url": "LinkedIn Company URL",
|
||||
"posts": "Number of posts to fetch",
|
||||
"comments": "Number of comments to fetch per post",
|
||||
"reposts": "Number of reposts to fetch"
|
||||
}
|
||||
},
|
||||
"search_posts_with_filters": {
|
||||
"route": "/search_posts_with_filters",
|
||||
"method": "GET",
|
||||
"name": "Search Posts With Filters",
|
||||
"description": "Searches LinkedIn posts with various filtering options",
|
||||
"payload": {
|
||||
"query": "Keywords/Search terms (text you put in LinkedIn search bar)",
|
||||
"page": "Page number (1-100, each page contains 20 results)",
|
||||
"sort_by": "Sort method: 'relevance' (Top match) or 'date_posted' (Latest)",
|
||||
"author_job_title": "Filter by job title of author (e.g., CEO)",
|
||||
"content_type": "Type of content post contains (photos, videos, liveVideos, collaborativeArticles, documents)",
|
||||
"from_member": "URN of person who posted (comma-separated for multiple)",
|
||||
"from_organization": "ID of organization who posted (comma-separated for multiple)",
|
||||
"author_company": "ID of company author works for (comma-separated for multiple)",
|
||||
"author_industry": "URN of industry author is connected with (comma-separated for multiple)",
|
||||
"mentions_member": "URN of person mentioned in post (comma-separated for multiple)",
|
||||
"mentions_organization": "ID of organization mentioned in post (comma-separated for multiple)"
|
||||
}
|
||||
},
|
||||
"search_jobs": {
|
||||
"route": "/search_jobs",
|
||||
"method": "GET",
|
||||
"name": "Search Jobs",
|
||||
"description": "Searches LinkedIn jobs with various filtering options",
|
||||
"payload": {
|
||||
"query": "Job search keywords (e.g., Software developer)",
|
||||
"page": "Page number",
|
||||
"searchLocationId": "Location ID for job search (get from Suggestion location endpoint)",
|
||||
"easyApply": "Filter for easy apply jobs (true or false)",
|
||||
"experience": "Experience level required (1=Internship, 2=Entry level, 3=Associate, 4=Mid senior, 5=Director, 6=Executive, comma-separated)",
|
||||
"jobType": "Job type (F=Full time, P=Part time, C=Contract, T=Temporary, V=Volunteer, I=Internship, O=Other, comma-separated)",
|
||||
"postedAgo": "Time jobs were posted in seconds (e.g., 3600 for past hour)",
|
||||
"workplaceType": "Workplace type (1=On-Site, 2=Remote, 3=Hybrid, comma-separated)",
|
||||
"sortBy": "Sort method (DD=most recent, R=most relevant)",
|
||||
"companyIdsList": "List of company IDs, comma-separated",
|
||||
"industryIdsList": "List of industry IDs, comma-separated",
|
||||
"functionIdsList": "List of function IDs, comma-separated",
|
||||
"titleIdsList": "List of job title IDs, comma-separated",
|
||||
"locationIdsList": "List of location IDs within specified searchLocationId country, comma-separated"
|
||||
}
|
||||
},
|
||||
"search_people_with_filters": {
|
||||
"route": "/search_people_with_filters",
|
||||
"method": "POST",
|
||||
"name": "Search People With Filters",
|
||||
"description": "Searches LinkedIn profiles with detailed filtering options",
|
||||
"payload": {
|
||||
"keyword": "General search keyword",
|
||||
"page": "Page number",
|
||||
"title_free_text": "Job title to filter by (e.g., CEO)",
|
||||
"company_free_text": "Company name to filter by",
|
||||
"first_name": "First name of person",
|
||||
"last_name": "Last name of person",
|
||||
"current_company_list": "List of current companies (comma-separated IDs)",
|
||||
"past_company_list": "List of past companies (comma-separated IDs)",
|
||||
"location_list": "List of locations (comma-separated IDs)",
|
||||
"language_list": "List of languages (comma-separated)",
|
||||
"service_catagory_list": "List of service categories (comma-separated)",
|
||||
"school_free_text": "School name to filter by",
|
||||
"industry_list": "List of industries (comma-separated IDs)",
|
||||
"school_list": "List of schools (comma-separated IDs)"
|
||||
}
|
||||
},
|
||||
"search_company_with_filters": {
|
||||
"route": "/search_company_with_filters",
|
||||
"method": "POST",
|
||||
"name": "Search Company With Filters",
|
||||
"description": "Searches LinkedIn companies with detailed filtering options",
|
||||
"payload": {
|
||||
"keyword": "General search keyword",
|
||||
"page": "Page number",
|
||||
"company_size_list": "List of company sizes (comma-separated, e.g., A,D)",
|
||||
"hasJobs": "Filter companies with jobs (true or false)",
|
||||
"location_list": "List of location IDs (comma-separated)",
|
||||
"industry_list": "List of industry IDs (comma-separated)"
|
||||
}
|
||||
}
|
||||
}
|
||||
base_url = "https://linkedin-data-scraper.p.rapidapi.com"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
tool = LinkedinProvider()
|
||||
|
||||
result = tool.call_endpoint(
|
||||
route="comments_from_recent_activity",
|
||||
payload={"profile_url": "https://www.linkedin.com/in/adamcohenhillel/", "page": 1}
|
||||
)
|
||||
print(result)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, Any, Optional, TypedDict, Literal
|
||||
|
||||
|
||||
class EndpointSchema(TypedDict):
|
||||
route: str
|
||||
method: Literal['GET', 'POST']
|
||||
name: str
|
||||
description: str
|
||||
payload: Dict[str, Any]
|
||||
|
||||
|
||||
class RapidDataProviderBase:
|
||||
def __init__(self, base_url: str, endpoints: Dict[str, EndpointSchema]):
|
||||
self.base_url = base_url
|
||||
self.endpoints = endpoints
|
||||
|
||||
def get_endpoints(self):
|
||||
return self.endpoints
|
||||
|
||||
def call_endpoint(
|
||||
self,
|
||||
route: str,
|
||||
payload: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""
|
||||
Call an API endpoint with the given parameters and data.
|
||||
|
||||
Args:
|
||||
endpoint (EndpointSchema): The endpoint configuration dictionary
|
||||
params (dict, optional): Query parameters for GET requests
|
||||
payload (dict, optional): JSON payload for POST requests
|
||||
|
||||
Returns:
|
||||
dict: The JSON response from the API
|
||||
"""
|
||||
if route.startswith("/"):
|
||||
route = route[1:]
|
||||
|
||||
endpoint = self.endpoints.get(route)
|
||||
if not endpoint:
|
||||
raise ValueError(f"Endpoint {route} not found")
|
||||
|
||||
url = f"{self.base_url}{endpoint['route']}"
|
||||
|
||||
headers = {
|
||||
"x-rapidapi-key": os.getenv("RAPID_API_KEY"),
|
||||
"x-rapidapi-host": url.split("//")[1].split("/")[0],
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
method = endpoint.get('method', 'GET').upper()
|
||||
|
||||
if method == 'GET':
|
||||
response = requests.get(url, params=payload, headers=headers)
|
||||
elif method == 'POST':
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
return response.json()
|
||||
@@ -1,240 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from agent.tools.data_providers.RapidDataProviderBase import RapidDataProviderBase, EndpointSchema
|
||||
|
||||
|
||||
class TwitterProvider(RapidDataProviderBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"user_info": {
|
||||
"route": "/screenname.php",
|
||||
"method": "GET",
|
||||
"name": "Twitter User Info",
|
||||
"description": "Get information about a Twitter user by screenname or user ID.",
|
||||
"payload": {
|
||||
"screenname": "Twitter username without the @ symbol",
|
||||
"rest_id": "Optional Twitter user's ID. If provided, overwrites screenname parameter."
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"route": "/timeline.php",
|
||||
"method": "GET",
|
||||
"name": "User Timeline",
|
||||
"description": "Get tweets from a user's timeline.",
|
||||
"payload": {
|
||||
"screenname": "Twitter username without the @ symbol",
|
||||
"rest_id": "Optional parameter that overwrites the screenname",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
},
|
||||
"following": {
|
||||
"route": "/following.php",
|
||||
"method": "GET",
|
||||
"name": "User Following",
|
||||
"description": "Get users that a specific user follows.",
|
||||
"payload": {
|
||||
"screenname": "Twitter username without the @ symbol",
|
||||
"rest_id": "Optional parameter that overwrites the screenname",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
},
|
||||
"followers": {
|
||||
"route": "/followers.php",
|
||||
"method": "GET",
|
||||
"name": "User Followers",
|
||||
"description": "Get followers of a specific user.",
|
||||
"payload": {
|
||||
"screenname": "Twitter username without the @ symbol",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"route": "/search.php",
|
||||
"method": "GET",
|
||||
"name": "Twitter Search",
|
||||
"description": "Search for tweets with a specific query.",
|
||||
"payload": {
|
||||
"query": "Search query string",
|
||||
"cursor": "Optional pagination cursor",
|
||||
"search_type": "Optional search type (e.g. 'Top')"
|
||||
}
|
||||
},
|
||||
"replies": {
|
||||
"route": "/replies.php",
|
||||
"method": "GET",
|
||||
"name": "User Replies",
|
||||
"description": "Get replies made by a user.",
|
||||
"payload": {
|
||||
"screenname": "Twitter username without the @ symbol",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
},
|
||||
"check_retweet": {
|
||||
"route": "/checkretweet.php",
|
||||
"method": "GET",
|
||||
"name": "Check Retweet",
|
||||
"description": "Check if a user has retweeted a specific tweet.",
|
||||
"payload": {
|
||||
"screenname": "Twitter username without the @ symbol",
|
||||
"tweet_id": "ID of the tweet to check"
|
||||
}
|
||||
},
|
||||
"tweet": {
|
||||
"route": "/tweet.php",
|
||||
"method": "GET",
|
||||
"name": "Get Tweet",
|
||||
"description": "Get details of a specific tweet by ID.",
|
||||
"payload": {
|
||||
"id": "ID of the tweet"
|
||||
}
|
||||
},
|
||||
"tweet_thread": {
|
||||
"route": "/tweet_thread.php",
|
||||
"method": "GET",
|
||||
"name": "Get Tweet Thread",
|
||||
"description": "Get a thread of tweets starting from a specific tweet ID.",
|
||||
"payload": {
|
||||
"id": "ID of the tweet",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
},
|
||||
"retweets": {
|
||||
"route": "/retweets.php",
|
||||
"method": "GET",
|
||||
"name": "Get Retweets",
|
||||
"description": "Get users who retweeted a specific tweet.",
|
||||
"payload": {
|
||||
"id": "ID of the tweet",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
},
|
||||
"latest_replies": {
|
||||
"route": "/latest_replies.php",
|
||||
"method": "GET",
|
||||
"name": "Get Latest Replies",
|
||||
"description": "Get the latest replies to a specific tweet.",
|
||||
"payload": {
|
||||
"id": "ID of the tweet",
|
||||
"cursor": "Optional pagination cursor"
|
||||
}
|
||||
}
|
||||
}
|
||||
base_url = "https://twitter-api45.p.rapidapi.com"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
tool = TwitterProvider()
|
||||
|
||||
# Example for getting user info
|
||||
user_info = tool.call_endpoint(
|
||||
route="user_info",
|
||||
payload={
|
||||
"screenname": "elonmusk",
|
||||
# "rest_id": "44196397" # Optional, uncomment to use user ID instead of screenname
|
||||
}
|
||||
)
|
||||
print("User Info:", user_info)
|
||||
|
||||
# Example for getting user timeline
|
||||
timeline = tool.call_endpoint(
|
||||
route="timeline",
|
||||
payload={
|
||||
"screenname": "elonmusk",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Timeline:", timeline)
|
||||
|
||||
# Example for getting user following
|
||||
following = tool.call_endpoint(
|
||||
route="following",
|
||||
payload={
|
||||
"screenname": "elonmusk",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Following:", following)
|
||||
|
||||
# Example for getting user followers
|
||||
followers = tool.call_endpoint(
|
||||
route="followers",
|
||||
payload={
|
||||
"screenname": "elonmusk",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Followers:", followers)
|
||||
|
||||
# Example for searching tweets
|
||||
search_results = tool.call_endpoint(
|
||||
route="search",
|
||||
payload={
|
||||
"query": "cybertruck",
|
||||
"search_type": "Top" # Optional, defaults to Top
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Search Results:", search_results)
|
||||
|
||||
# Example for getting user replies
|
||||
replies = tool.call_endpoint(
|
||||
route="replies",
|
||||
payload={
|
||||
"screenname": "elonmusk",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Replies:", replies)
|
||||
|
||||
# Example for checking if user retweeted a tweet
|
||||
check_retweet = tool.call_endpoint(
|
||||
route="check_retweet",
|
||||
payload={
|
||||
"screenname": "elonmusk",
|
||||
"tweet_id": "1671370010743263233"
|
||||
}
|
||||
)
|
||||
print("Check Retweet:", check_retweet)
|
||||
|
||||
# Example for getting tweet details
|
||||
tweet = tool.call_endpoint(
|
||||
route="tweet",
|
||||
payload={
|
||||
"id": "1671370010743263233"
|
||||
}
|
||||
)
|
||||
print("Tweet:", tweet)
|
||||
|
||||
# Example for getting a tweet thread
|
||||
tweet_thread = tool.call_endpoint(
|
||||
route="tweet_thread",
|
||||
payload={
|
||||
"id": "1738106896777699464",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Tweet Thread:", tweet_thread)
|
||||
|
||||
# Example for getting retweets of a tweet
|
||||
retweets = tool.call_endpoint(
|
||||
route="retweets",
|
||||
payload={
|
||||
"id": "1700199139470942473",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Retweets:", retweets)
|
||||
|
||||
# Example for getting latest replies to a tweet
|
||||
latest_replies = tool.call_endpoint(
|
||||
route="latest_replies",
|
||||
payload={
|
||||
"id": "1738106896777699464",
|
||||
# "cursor": "optional-cursor-value" # Optional for pagination
|
||||
}
|
||||
)
|
||||
print("Latest Replies:", latest_replies)
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from agent.tools.data_providers.RapidDataProviderBase import RapidDataProviderBase, EndpointSchema
|
||||
|
||||
|
||||
class YahooFinanceProvider(RapidDataProviderBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"get_tickers": {
|
||||
"route": "/v2/markets/tickers",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance Tickers",
|
||||
"description": "Get financial tickers from Yahoo Finance with various filters and parameters.",
|
||||
"payload": {
|
||||
"page": "Page number for pagination (optional, default: 1)",
|
||||
"type": "Asset class type (required): STOCKS, ETF, MUTUALFUNDS, or FUTURES",
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"route": "/v1/markets/search",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance Search",
|
||||
"description": "Search for financial instruments on Yahoo Finance",
|
||||
"payload": {
|
||||
"search": "Search term (required)",
|
||||
}
|
||||
},
|
||||
"get_news": {
|
||||
"route": "/v2/markets/news",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance News",
|
||||
"description": "Get news related to specific tickers from Yahoo Finance",
|
||||
"payload": {
|
||||
"tickers": "Stock symbol (optional, e.g., AAPL)",
|
||||
"type": "News type (optional): ALL, VIDEO, or PRESS_RELEASE",
|
||||
}
|
||||
},
|
||||
"get_stock_module": {
|
||||
"route": "/v1/markets/stock/modules",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance Stock Module",
|
||||
"description": "Get detailed information about a specific stock module",
|
||||
"payload": {
|
||||
"ticker": "Company ticker symbol (required, e.g., AAPL)",
|
||||
"module": "Module to retrieve (required): asset-profile, financial-data, earnings, etc.",
|
||||
}
|
||||
},
|
||||
"get_sma": {
|
||||
"route": "/v1/markets/indicators/sma",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance SMA Indicator",
|
||||
"description": "Get Simple Moving Average (SMA) indicator data for a stock",
|
||||
"payload": {
|
||||
"symbol": "Stock symbol (required, e.g., AAPL)",
|
||||
"interval": "Time interval (required): 5m, 15m, 30m, 1h, 1d, 1wk, 1mo, 3mo",
|
||||
"series_type": "Series type (required): open, close, high, low",
|
||||
"time_period": "Number of data points used for calculation (required)",
|
||||
"limit": "Limit the number of results (optional, default: 50)",
|
||||
}
|
||||
},
|
||||
"get_rsi": {
|
||||
"route": "/v1/markets/indicators/rsi",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance RSI Indicator",
|
||||
"description": "Get Relative Strength Index (RSI) indicator data for a stock",
|
||||
"payload": {
|
||||
"symbol": "Stock symbol (required, e.g., AAPL)",
|
||||
"interval": "Time interval (required): 5m, 15m, 30m, 1h, 1d, 1wk, 1mo, 3mo",
|
||||
"series_type": "Series type (required): open, close, high, low",
|
||||
"time_period": "Number of data points used for calculation (required)",
|
||||
"limit": "Limit the number of results (optional, default: 50)",
|
||||
}
|
||||
},
|
||||
"get_earnings_calendar": {
|
||||
"route": "/v1/markets/calendar/earnings",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance Earnings Calendar",
|
||||
"description": "Get earnings calendar data for a specific date",
|
||||
"payload": {
|
||||
"date": "Calendar date in yyyy-mm-dd format (optional, e.g., 2023-11-30)",
|
||||
}
|
||||
},
|
||||
"get_insider_trades": {
|
||||
"route": "/v1/markets/insider-trades",
|
||||
"method": "GET",
|
||||
"name": "Yahoo Finance Insider Trades",
|
||||
"description": "Get recent insider trading activity",
|
||||
"payload": {}
|
||||
},
|
||||
}
|
||||
base_url = "https://yahoo-finance15.p.rapidapi.com/api"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
tool = YahooFinanceProvider()
|
||||
|
||||
# Example for getting stock tickers
|
||||
tickers_result = tool.call_endpoint(
|
||||
route="get_tickers",
|
||||
payload={
|
||||
"page": 1,
|
||||
"type": "STOCKS"
|
||||
}
|
||||
)
|
||||
print("Tickers Result:", tickers_result)
|
||||
|
||||
# Example for searching financial instruments
|
||||
search_result = tool.call_endpoint(
|
||||
route="search",
|
||||
payload={
|
||||
"search": "AA"
|
||||
}
|
||||
)
|
||||
print("Search Result:", search_result)
|
||||
|
||||
# Example for getting financial news
|
||||
news_result = tool.call_endpoint(
|
||||
route="get_news",
|
||||
payload={
|
||||
"tickers": "AAPL",
|
||||
"type": "ALL"
|
||||
}
|
||||
)
|
||||
print("News Result:", news_result)
|
||||
|
||||
# Example for getting stock asset profile module
|
||||
stock_module_result = tool.call_endpoint(
|
||||
route="get_stock_module",
|
||||
payload={
|
||||
"ticker": "AAPL",
|
||||
"module": "asset-profile"
|
||||
}
|
||||
)
|
||||
print("Asset Profile Result:", stock_module_result)
|
||||
|
||||
# Example for getting financial data module
|
||||
financial_data_result = tool.call_endpoint(
|
||||
route="get_stock_module",
|
||||
payload={
|
||||
"ticker": "AAPL",
|
||||
"module": "financial-data"
|
||||
}
|
||||
)
|
||||
print("Financial Data Result:", financial_data_result)
|
||||
|
||||
# Example for getting SMA indicator data
|
||||
sma_result = tool.call_endpoint(
|
||||
route="get_sma",
|
||||
payload={
|
||||
"symbol": "AAPL",
|
||||
"interval": "5m",
|
||||
"series_type": "close",
|
||||
"time_period": "50",
|
||||
"limit": "50"
|
||||
}
|
||||
)
|
||||
print("SMA Result:", sma_result)
|
||||
|
||||
# Example for getting RSI indicator data
|
||||
rsi_result = tool.call_endpoint(
|
||||
route="get_rsi",
|
||||
payload={
|
||||
"symbol": "AAPL",
|
||||
"interval": "5m",
|
||||
"series_type": "close",
|
||||
"time_period": "50",
|
||||
"limit": "50"
|
||||
}
|
||||
)
|
||||
print("RSI Result:", rsi_result)
|
||||
|
||||
# Example for getting earnings calendar data
|
||||
earnings_calendar_result = tool.call_endpoint(
|
||||
route="get_earnings_calendar",
|
||||
payload={
|
||||
"date": "2023-11-30"
|
||||
}
|
||||
)
|
||||
print("Earnings Calendar Result:", earnings_calendar_result)
|
||||
|
||||
# Example for getting insider trades
|
||||
insider_trades_result = tool.call_endpoint(
|
||||
route="get_insider_trades",
|
||||
payload={}
|
||||
)
|
||||
print("Insider Trades Result:", insider_trades_result)
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
from typing import Dict
|
||||
import logging
|
||||
|
||||
from agent.tools.data_providers.RapidDataProviderBase import RapidDataProviderBase, EndpointSchema
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZillowProvider(RapidDataProviderBase):
|
||||
def __init__(self):
|
||||
endpoints: Dict[str, EndpointSchema] = {
|
||||
"search": {
|
||||
"route": "/search",
|
||||
"method": "GET",
|
||||
"name": "Zillow Property Search",
|
||||
"description": "Search for properties by neighborhood, city, or ZIP code with various filters.",
|
||||
"payload": {
|
||||
"location": "Location can be an address, neighborhood, city, or ZIP code (required)",
|
||||
"page": "Page number for pagination (optional, default: 0)",
|
||||
"output": "Output format: json, csv, xlsx (optional, default: json)",
|
||||
"status": "Status of properties: forSale, forRent, recentlySold (optional, default: forSale)",
|
||||
"sortSelection": "Sorting criteria (optional, default: priorityscore)",
|
||||
"listing_type": "Listing type: by_agent, by_owner_other (optional, default: by_agent)",
|
||||
"doz": "Days on Zillow: any, 1, 7, 14, 30, 90, 6m, 12m, 24m, 36m (optional, default: any)",
|
||||
"price_min": "Minimum price (optional)",
|
||||
"price_max": "Maximum price (optional)",
|
||||
"sqft_min": "Minimum square footage (optional)",
|
||||
"sqft_max": "Maximum square footage (optional)",
|
||||
"beds_min": "Minimum number of bedrooms (optional)",
|
||||
"beds_max": "Maximum number of bedrooms (optional)",
|
||||
"baths_min": "Minimum number of bathrooms (optional)",
|
||||
"baths_max": "Maximum number of bathrooms (optional)",
|
||||
"built_min": "Minimum year built (optional)",
|
||||
"built_max": "Maximum year built (optional)",
|
||||
"lotSize_min": "Minimum lot size in sqft (optional)",
|
||||
"lotSize_max": "Maximum lot size in sqft (optional)",
|
||||
"keywords": "Keywords to search for (optional)"
|
||||
}
|
||||
},
|
||||
"search_address": {
|
||||
"route": "/search_address",
|
||||
"method": "GET",
|
||||
"name": "Zillow Address Search",
|
||||
"description": "Search for a specific property by its full address.",
|
||||
"payload": {
|
||||
"address": "Full property address (required)"
|
||||
}
|
||||
},
|
||||
"propertyV2": {
|
||||
"route": "/propertyV2",
|
||||
"method": "GET",
|
||||
"name": "Zillow Property Details",
|
||||
"description": "Get detailed information about a specific property by zpid or URL.",
|
||||
"payload": {
|
||||
"zpid": "Zillow property ID (optional if URL is provided)",
|
||||
"url": "Property details URL (optional if zpid is provided)"
|
||||
}
|
||||
},
|
||||
"zestimate_history": {
|
||||
"route": "/zestimate_history",
|
||||
"method": "GET",
|
||||
"name": "Zillow Zestimate History",
|
||||
"description": "Get historical Zestimate values for a specific property.",
|
||||
"payload": {
|
||||
"zpid": "Zillow property ID (optional if URL is provided)",
|
||||
"url": "Property details URL (optional if zpid is provided)"
|
||||
}
|
||||
},
|
||||
"similar_properties": {
|
||||
"route": "/similar_properties",
|
||||
"method": "GET",
|
||||
"name": "Zillow Similar Properties",
|
||||
"description": "Find properties similar to a specific property.",
|
||||
"payload": {
|
||||
"zpid": "Zillow property ID (optional if URL or address is provided)",
|
||||
"url": "Property details URL (optional if zpid or address is provided)",
|
||||
"address": "Property address (optional if zpid or URL is provided)"
|
||||
}
|
||||
},
|
||||
"mortgage_rates": {
|
||||
"route": "/mortgage/rates",
|
||||
"method": "GET",
|
||||
"name": "Zillow Mortgage Rates",
|
||||
"description": "Get current mortgage rates for different loan programs and conditions.",
|
||||
"payload": {
|
||||
"program": "Loan program (required): Fixed30Year, Fixed20Year, Fixed15Year, Fixed10Year, ARM3, ARM5, ARM7, etc.",
|
||||
"state": "State abbreviation (optional, default: US)",
|
||||
"refinance": "Whether this is for refinancing (optional, default: false)",
|
||||
"loanType": "Type of loan: Conventional, etc. (optional)",
|
||||
"loanAmount": "Loan amount category: Micro, SmallConforming, Conforming, SuperConforming, Jumbo (optional)",
|
||||
"loanToValue": "Loan to value ratio: Normal, High, VeryHigh (optional)",
|
||||
"creditScore": "Credit score category: Low, High, VeryHigh (optional)",
|
||||
"duration": "Duration in days (optional, default: 30)"
|
||||
}
|
||||
},
|
||||
}
|
||||
base_url = "https://zillow56.p.rapidapi.com"
|
||||
super().__init__(base_url, endpoints)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dotenv import load_dotenv
|
||||
from time import sleep
|
||||
load_dotenv()
|
||||
tool = ZillowProvider()
|
||||
|
||||
# Example for searching properties in Houston
|
||||
search_result = tool.call_endpoint(
|
||||
route="search",
|
||||
payload={
|
||||
"location": "houston, tx",
|
||||
"status": "forSale",
|
||||
"sortSelection": "priorityscore",
|
||||
"listing_type": "by_agent",
|
||||
"doz": "any"
|
||||
}
|
||||
)
|
||||
logger.debug("Search Result: %s", search_result)
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
sleep(1)
|
||||
# Example for searching by address
|
||||
address_result = tool.call_endpoint(
|
||||
route="search_address",
|
||||
payload={
|
||||
"address": "1161 Natchez Dr College Station Texas 77845"
|
||||
}
|
||||
)
|
||||
logger.debug("Address Search Result: %s", address_result)
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
sleep(1)
|
||||
# Example for getting property details
|
||||
property_result = tool.call_endpoint(
|
||||
route="propertyV2",
|
||||
payload={
|
||||
"zpid": "7594920"
|
||||
}
|
||||
)
|
||||
logger.debug("Property Details Result: %s", property_result)
|
||||
sleep(1)
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
|
||||
# Example for getting zestimate history
|
||||
zestimate_result = tool.call_endpoint(
|
||||
route="zestimate_history",
|
||||
payload={
|
||||
"zpid": "20476226"
|
||||
}
|
||||
)
|
||||
logger.debug("Zestimate History Result: %s", zestimate_result)
|
||||
sleep(1)
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
# Example for getting similar properties
|
||||
similar_result = tool.call_endpoint(
|
||||
route="similar_properties",
|
||||
payload={
|
||||
"zpid": "28253016"
|
||||
}
|
||||
)
|
||||
logger.debug("Similar Properties Result: %s", similar_result)
|
||||
sleep(1)
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
logger.debug("***")
|
||||
# Example for getting mortgage rates
|
||||
mortgage_result = tool.call_endpoint(
|
||||
route="mortgage_rates",
|
||||
payload={
|
||||
"program": "Fixed30Year",
|
||||
"state": "US",
|
||||
"refinance": "false",
|
||||
"loanType": "Conventional",
|
||||
"loanAmount": "Conforming",
|
||||
"loanToValue": "Normal",
|
||||
"creditScore": "Low",
|
||||
"duration": "30"
|
||||
}
|
||||
)
|
||||
logger.debug("Mortgage Rates Result: %s", mortgage_result)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
from app.tool.computer_use_tool import ComputerUseTool
|
||||
from app.daytona.sandbox import create_sandbox
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
# 创建沙箱和工具
|
||||
sandbox = create_sandbox(password="123456")
|
||||
base_url=sandbox.get_preview_link(8000)
|
||||
print(f"Sandbox base URL: {base_url}")
|
||||
|
||||
print(f"Sandbox ID: {sandbox.id}")
|
||||
computer_tool = ComputerUseTool.create_with_sandbox(sandbox)
|
||||
|
||||
# 执行截图操作
|
||||
result = await computer_tool.execute(action="screenshot")
|
||||
print(result)
|
||||
|
||||
# 清理资源(可选)
|
||||
await computer_tool.cleanup()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main()) # 运行异步主函数
|
||||
@@ -0,0 +1,81 @@
|
||||
from daytona import Daytona, DaytonaConfig,CreateSandboxFromImageParams,Resources, Image
|
||||
# Using environment variables (DAYTONA_API_KEY, DAYTONA_API_URL, DAYTONA_TARGET)
|
||||
# daytona = Daytona()
|
||||
# Using explicit configuration
|
||||
config = DaytonaConfig(
|
||||
api_key="dtn_bedf8ed9953f0b5c410c042090e1002a56ba8129b573c92f5607aef04b08c82a",
|
||||
api_url="https://app.daytona.io/api",
|
||||
target="us"
|
||||
)
|
||||
daytona = Daytona(config)
|
||||
params = CreateSandboxFromImageParams(
|
||||
image="kortix/suna:0.1.3",
|
||||
# image=Image.debian_slim("3.12"),
|
||||
public=True,
|
||||
labels=None,
|
||||
env_vars={
|
||||
"CHROME_PERSISTENT_SESSION": "true",
|
||||
"RESOLUTION": "1024x768x24",
|
||||
"RESOLUTION_WIDTH": "1024",
|
||||
"RESOLUTION_HEIGHT": "768",
|
||||
"VNC_PASSWORD": "123456",
|
||||
"ANONYMIZED_TELEMETRY": "false",
|
||||
"CHROME_PATH": "",
|
||||
"CHROME_USER_DATA": "",
|
||||
"CHROME_DEBUGGING_PORT": "9222",
|
||||
"CHROME_DEBUGGING_HOST": "localhost",
|
||||
"CHROME_CDP": ""
|
||||
},
|
||||
resources=Resources(
|
||||
cpu=1,
|
||||
memory=1,
|
||||
disk=1,
|
||||
),
|
||||
auto_stop_interval=15,
|
||||
auto_archive_interval=24 * 60,
|
||||
)
|
||||
# Create the sandbox
|
||||
sandbox = daytona.create(params)
|
||||
print(f"Sandbox created with ID: {sandbox.id}")
|
||||
|
||||
|
||||
# from daytona import Daytona
|
||||
|
||||
# def main():
|
||||
# # Initialize the SDK (uses environment variables by default)
|
||||
# daytona = Daytona(config)
|
||||
|
||||
# # Create a new sandbox
|
||||
# sandbox = daytona.create()
|
||||
|
||||
# # Execute a command
|
||||
# response = sandbox.process.exec("echo 'Hello, World!'")
|
||||
# print(response.result)
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
# from daytona import Daytona, DaytonaConfig
|
||||
|
||||
# Define the configuration
|
||||
|
||||
# config = DaytonaConfig(api_key="dtn_bedf8ed9953f0b5c410c042090e1002a56ba8129b573c92f5607aef04b08c82a")
|
||||
|
||||
# # Initialize the Daytona client
|
||||
|
||||
# daytona = Daytona(config)
|
||||
|
||||
# # Create the Sandbox instance
|
||||
|
||||
# sandbox = daytona.create()
|
||||
|
||||
# # Run the code securely inside the Sandbox
|
||||
|
||||
# response = sandbox.process.code_run('print("Hello World from code!")')
|
||||
# if response.exit_code != 0:
|
||||
# print(f"Error: {response.exit_code} {response.result}")
|
||||
# else:
|
||||
# print(response.result)
|
||||
|
||||
# Clean up
|
||||
|
||||
# sandbox.delete()
|
||||
Reference in New Issue
Block a user