mirror of https://github.com/kortix-ai/suna.git
268 lines
10 KiB
Python
268 lines
10 KiB
Python
import os
|
|
import shutil
|
|
import click
|
|
import questionary
|
|
from typing import List, Dict, Optional, Tuple
|
|
import time
|
|
import pkg_resources
|
|
import requests
|
|
from packaging import version
|
|
import re
|
|
|
|
MODULES = {
|
|
"llm": {
|
|
"required": True,
|
|
"files": ["llm.py"],
|
|
"description": "LLM Interface - Core module for interacting with large language models (OpenAI, Anthropic, 100+ LLMs using the OpenAI Input/Output Format powered by LiteLLM). Handles API calls, response streaming, and model-specific configurations."
|
|
},
|
|
"tool": {
|
|
"required": True,
|
|
"files": [
|
|
"tool.py",
|
|
"tool_registry.py"
|
|
],
|
|
"description": "Tool System Foundation - Defines the base architecture for creating and managing tools. Includes the tool registry for registering, organizing, and accessing tool functions."
|
|
},
|
|
"processors": {
|
|
"required": True,
|
|
"files": [
|
|
"processor/base_processors.py",
|
|
"processor/llm_response_processor.py",
|
|
"processor/standard/standard_tool_parser.py",
|
|
"processor/standard/standard_tool_executor.py",
|
|
"processor/standard/standard_results_adder.py",
|
|
"processor/xml/xml_tool_parser.py",
|
|
"processor/xml/xml_tool_executor.py",
|
|
"processor/xml/xml_results_adder.py"
|
|
],
|
|
"description": "Response Processing System - Handles parsing and executing LLM responses, managing tool calls, and processing results. Supports both standard OpenAI-style function calling and XML-based tool execution patterns."
|
|
},
|
|
"thread_management": {
|
|
"required": True,
|
|
"files": [
|
|
"thread_manager.py",
|
|
"thread_viewer_ui.py"
|
|
],
|
|
"description": "Conversation Management System - Handles message threading, conversation history, and provides a UI for viewing conversation threads. Manages the flow of messages between the user, LLM, and tools."
|
|
},
|
|
"state_management": {
|
|
"required": False,
|
|
"files": ["state_manager.py"],
|
|
"description": "State Persistence System - Provides thread-safe storage and retrieval of conversation state, tool data, and other persistent information. Enables maintaining context across sessions and managing shared state between components."
|
|
}
|
|
}
|
|
|
|
STARTER_EXAMPLES = {
|
|
"simple_web_dev_example_agent": {
|
|
"description": "Interactive web development agent with file and terminal manipulation capabilities. Demonstrates both standard and XML-based tool calling patterns.",
|
|
"files": {
|
|
"agent.py": "agents/simple_web_dev/agent.py",
|
|
"tools/files_tool.py": "agents/simple_web_dev/tools/files_tool.py",
|
|
"tools/terminal_tool.py": "agents/simple_web_dev/tools/terminal_tool.py",
|
|
".env.example": "agents/.env.example"
|
|
}
|
|
}
|
|
}
|
|
|
|
PACKAGE_NAME = "agentpress"
|
|
PYPI_URL = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
|
|
|
|
def check_for_updates() -> Tuple[Optional[str], Optional[str], bool]:
|
|
"""
|
|
Check if there's a newer version available on PyPI
|
|
Returns: (current_version, latest_version, update_available)
|
|
"""
|
|
try:
|
|
current_version = pkg_resources.get_distribution(PACKAGE_NAME).version
|
|
response = requests.get(PYPI_URL, timeout=2)
|
|
response.raise_for_status() # Raise exception for bad status codes
|
|
|
|
latest_version = response.json()["info"]["version"]
|
|
|
|
# Compare versions properly using packaging.version
|
|
current_ver = version.parse(current_version)
|
|
latest_ver = version.parse(latest_version)
|
|
|
|
return current_version, latest_version, latest_ver > current_ver
|
|
|
|
except requests.RequestException:
|
|
# Handle network-related errors silently
|
|
return None, None, False
|
|
except Exception as e:
|
|
# Log other unexpected errors but don't break the CLI
|
|
click.echo(f"Warning: Failed to check for updates: {str(e)}", err=True)
|
|
return None, None, False
|
|
|
|
def show_welcome():
|
|
"""Display welcome message with ASCII art"""
|
|
click.clear()
|
|
|
|
# Check for updates
|
|
current_version, latest_version, update_available = check_for_updates()
|
|
|
|
click.echo("""
|
|
╔═══════════════════════════════════════════╗
|
|
║ Welcome to AgentPress ║
|
|
║ Your AI Agent Building Blocks ║
|
|
╚═══════════════════════════════════════════╝
|
|
""")
|
|
|
|
if update_available and current_version and latest_version:
|
|
click.echo(
|
|
f"\n📢 Update available! "
|
|
f"{click.style(f'v{current_version}', fg='yellow')} → "
|
|
f"{click.style(f'v{latest_version}', fg='green')}"
|
|
)
|
|
click.echo("Run: pip install --upgrade agentpress\n")
|
|
|
|
time.sleep(1)
|
|
|
|
def copy_module_files(src_dir: str, dest_dir: str, files: List[str]):
|
|
"""Copy module files from package to destination"""
|
|
os.makedirs(dest_dir, exist_ok=True)
|
|
|
|
with click.progressbar(files, label='Copying files') as file_list:
|
|
for file in file_list:
|
|
src = os.path.join(src_dir, file)
|
|
dst = os.path.join(dest_dir, file)
|
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
shutil.copy2(src, dst)
|
|
|
|
def copy_example_files(src_dir: str, dest_dir: str, files: Dict[str, str]):
|
|
"""Copy example files from package to destination"""
|
|
for dest_path, src_path in files.items():
|
|
src = os.path.join(src_dir, src_path)
|
|
dst = os.path.join(dest_dir, dest_path)
|
|
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
shutil.copy2(src, dst)
|
|
click.echo(f" ✓ Created {dest_path}")
|
|
|
|
def update_file_paths(file_path: str, replacements: Dict[str, str]):
|
|
"""Update file paths in the given file"""
|
|
with open(file_path, 'r') as f:
|
|
content = f.read()
|
|
|
|
for old, new in replacements.items():
|
|
# Escape special characters in the old string
|
|
escaped_old = re.escape(old)
|
|
content = re.sub(escaped_old, new, content)
|
|
|
|
with open(file_path, 'w') as f:
|
|
f.write(content)
|
|
|
|
@click.group()
|
|
def cli():
|
|
"""AgentPress CLI - Initialize your AgentPress modules"""
|
|
pass
|
|
|
|
@cli.command()
|
|
def init():
|
|
"""Initialize AgentPress modules in your project"""
|
|
show_welcome()
|
|
|
|
# Set components directory name to 'agentpress'
|
|
components_dir = "agentpress"
|
|
|
|
if os.path.exists(components_dir):
|
|
if not questionary.confirm(
|
|
f"Directory '{components_dir}' already exists. Continue anyway?",
|
|
default=False
|
|
).ask():
|
|
click.echo("Setup cancelled.")
|
|
return
|
|
|
|
# Ask about starter examples
|
|
click.echo("\n📚 Starter Examples")
|
|
example_choices = [
|
|
{
|
|
"name": f"{name}: {example['description']}",
|
|
"value": name
|
|
}
|
|
for name, example in STARTER_EXAMPLES.items()
|
|
]
|
|
example_choices.append({"name": "None - I'll start from scratch", "value": None})
|
|
|
|
selected_example = questionary.select(
|
|
"Would you like to start with an example?",
|
|
choices=example_choices
|
|
).ask()
|
|
|
|
# Get package directory
|
|
package_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Show all modules status
|
|
click.echo("\n🔧 AgentPress Modules Configuration\n")
|
|
|
|
# Show required modules including state_manager
|
|
click.echo("📦 Required Modules (pre-selected):")
|
|
required_modules = {name: module for name, module in MODULES.items()
|
|
if module["required"] or name == "state_management"}
|
|
for name, module in required_modules.items():
|
|
click.echo(f" ✓ {click.style(name, fg='green')} - {module['description']}")
|
|
|
|
# Create selections dict with required modules pre-selected
|
|
selections = {name: True for name in required_modules.keys()}
|
|
|
|
click.echo("\n🚀 Setting up your AgentPress...")
|
|
time.sleep(0.5)
|
|
|
|
try:
|
|
# Copy selected modules
|
|
selected_modules = [name for name, selected in selections.items() if selected]
|
|
all_files = []
|
|
for module in selected_modules:
|
|
all_files.extend(MODULES[module]["files"])
|
|
|
|
# Create components directory and copy module files
|
|
components_dir_path = os.path.abspath(components_dir)
|
|
copy_module_files(package_dir, components_dir_path, all_files)
|
|
|
|
# Update paths in thread_manager.py and state_manager.py
|
|
project_dir = os.getcwd()
|
|
thread_manager_path = os.path.join(components_dir_path, "thread_manager.py")
|
|
state_manager_path = os.path.join(components_dir_path, "state_manager.py")
|
|
|
|
if os.path.exists(thread_manager_path):
|
|
update_file_paths(thread_manager_path, {
|
|
'threads_dir: str = "threads"': f'threads_dir: str = "{os.path.join(project_dir, "threads")}"'
|
|
})
|
|
|
|
if os.path.exists(state_manager_path):
|
|
update_file_paths(state_manager_path, {
|
|
'store_file: str = "state.json"': f'store_file: str = "{os.path.join(project_dir, "state.json")}"'
|
|
})
|
|
|
|
# Copy example only if a valid example (not None) was selected
|
|
if selected_example and selected_example in STARTER_EXAMPLES:
|
|
click.echo(f"\n📝 Creating {selected_example}...")
|
|
copy_example_files(
|
|
package_dir,
|
|
os.getcwd(), # Use current working directory
|
|
STARTER_EXAMPLES[selected_example]["files"]
|
|
)
|
|
# Create threads directory
|
|
os.makedirs(os.path.join(project_dir, "threads"), exist_ok=True)
|
|
|
|
click.echo("\n✨ Success! Your AgentPress is ready.")
|
|
click.echo(f"\n📁 Components created in: {click.style(components_dir_path, fg='green')}")
|
|
if selected_example and selected_example in STARTER_EXAMPLES:
|
|
click.echo(f"📁 Example agent files created in the current directory.")
|
|
|
|
click.echo("\n🔥 Quick start:")
|
|
click.echo("Check out the Quick Start guide at:")
|
|
click.echo("https://github.com/kortix-ai/agentpress#quick-start")
|
|
|
|
if selected_example:
|
|
click.echo(f"\nRun the example agent:")
|
|
click.echo(" python agent.py")
|
|
|
|
|
|
except Exception as e:
|
|
click.echo(f"\n❌ Error during setup: {str(e)}", err=True)
|
|
return
|
|
|
|
def main():
|
|
cli()
|
|
|
|
if __name__ == '__main__':
|
|
main() |