debug computer use tool

This commit is contained in:
GhostC
2025-07-06 15:13:41 +08:00
parent 2a6287422d
commit d7e3c14b60
21 changed files with 625 additions and 1641 deletions
+9
View File
@@ -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():
+57 -57
View File
@@ -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
+39 -39
View File
@@ -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
View File
@@ -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
+25 -25
View File
@@ -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
"""
+19
View File
@@ -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]:
+3
View File
@@ -0,0 +1,3 @@
structlog
daytona
litellm
+29 -28
View File
@@ -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
View File
@@ -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
View File
@@ -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."""
+35 -13
View File
@@ -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)
-191
View File
@@ -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)
-250
View File
@@ -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()
-240
View File
@@ -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)
-187
View File
@@ -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)
+22
View File
@@ -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()) # 运行异步主函数
+81
View File
@@ -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()