mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
9 Commits
fix-cli-co
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c1484ecf3 | ||
|
|
33c80d816e | ||
|
|
c164063d19 | ||
|
|
30f09f4aec | ||
|
|
cb87340cb8 | ||
|
|
15886acc3f | ||
|
|
c64eef19df | ||
|
|
be0bb3f388 | ||
|
|
c020268f5b |
107
docs/usage/team-cli.mdx
Normal file
107
docs/usage/team-cli.mdx
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Team CLI
|
||||
---
|
||||
|
||||
# OpenHands Team CLI
|
||||
|
||||
The Team CLI provides a command-line interface for interacting with the OpenHands HTTP and WebSocket APIs. It allows you to create conversations, list existing conversations, and join conversations to interact with the agent.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To use the Team CLI, you need to have OpenHands installed. You can then use the `team` command to access the Team CLI:
|
||||
|
||||
```bash
|
||||
openhands team [command] [options]
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The Team CLI uses the following environment variables for configuration:
|
||||
|
||||
- `OPENHANDS_API_URL`: The base URL for the OpenHands API (default: `https://staging.all-hands.dev`)
|
||||
- `OPENHANDS_API_KEY`: The API key for authentication (if required)
|
||||
|
||||
You can also specify these values using command-line options:
|
||||
|
||||
```bash
|
||||
openhands team --url https://app.all-hands.dev --api-key your-api-key [command] [options]
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### List Conversations
|
||||
|
||||
List all available conversations:
|
||||
|
||||
```bash
|
||||
openhands team list [options]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `-l, --limit`: Maximum number of conversations to list (default: 20)
|
||||
|
||||
### Create a Conversation
|
||||
|
||||
Create a new conversation:
|
||||
|
||||
```bash
|
||||
openhands team create [options]
|
||||
```
|
||||
|
||||
Options:
|
||||
- `-r, --repository`: Repository name (format: owner/repo)
|
||||
- `-g, --git-provider`: Git provider (github or gitlab)
|
||||
- `-b, --branch`: Branch name
|
||||
- `-m, --message`: Initial user message
|
||||
- `-i, --instructions`: Conversation instructions
|
||||
- `-j, --join`: Join the conversation after creation
|
||||
|
||||
### Join a Conversation
|
||||
|
||||
Join an existing conversation:
|
||||
|
||||
```bash
|
||||
openhands team join [conversation_id]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
List all conversations:
|
||||
|
||||
```bash
|
||||
openhands team list
|
||||
```
|
||||
|
||||
Create a new conversation with a GitHub repository:
|
||||
|
||||
```bash
|
||||
openhands team create -r All-Hands-AI/OpenHands -m "Help me understand the codebase"
|
||||
```
|
||||
|
||||
Create a conversation and join it immediately:
|
||||
|
||||
```bash
|
||||
openhands team create -m "Let's build a web app" -j
|
||||
```
|
||||
|
||||
Join an existing conversation:
|
||||
|
||||
```bash
|
||||
openhands team join abc123def456
|
||||
```
|
||||
|
||||
## Using with a Remote Server
|
||||
|
||||
To use the Team CLI with a remote OpenHands server:
|
||||
|
||||
```bash
|
||||
export OPENHANDS_API_URL="https://app.all-hands.dev"
|
||||
export OPENHANDS_API_KEY="your-api-key"
|
||||
openhands team list
|
||||
```
|
||||
|
||||
Or specify the URL and API key directly:
|
||||
|
||||
```bash
|
||||
openhands team --url https://app.all-hands.dev --api-key your-api-key list
|
||||
```
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
@@ -454,6 +453,37 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_arguments()
|
||||
|
||||
# Check if team command is used
|
||||
if hasattr(args, 'command') and args.command == 'team':
|
||||
# Import and run the team CLI directly
|
||||
import sys
|
||||
|
||||
from openhands.cli.team import main as team_main
|
||||
|
||||
# Get arguments after 'team'
|
||||
team_args = []
|
||||
if len(sys.argv) > 2:
|
||||
# Pass all arguments after 'team'
|
||||
team_args = sys.argv[2:]
|
||||
|
||||
if not team_args:
|
||||
# If no additional arguments, show help message
|
||||
print('OpenHands Team CLI')
|
||||
print('=================')
|
||||
print('To use the team CLI, run one of the following commands:')
|
||||
print(' openhands team list - List all conversations')
|
||||
print(' openhands team create - Create a new conversation')
|
||||
print(' openhands team join <id> - Join an existing conversation')
|
||||
print()
|
||||
print("For more information, run 'openhands team --help'")
|
||||
return
|
||||
|
||||
# Run the team CLI with the arguments
|
||||
team_main(team_args)
|
||||
return
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
|
||||
549
openhands/cli/team.py
Normal file
549
openhands/cli/team.py
Normal file
@@ -0,0 +1,549 @@
|
||||
"""Team CLI interface for OpenHands.
|
||||
|
||||
This module provides a CLI interface for interacting with the OpenHands HTTP and WebSocket APIs.
|
||||
It allows creating conversations and showing the current list of conversations/statuses.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional
|
||||
|
||||
import aiohttp
|
||||
import socketio
|
||||
from prompt_toolkit import print_formatted_text
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.shortcuts import clear
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from openhands.cli.tui import (
|
||||
display_banner,
|
||||
display_event,
|
||||
display_welcome_message,
|
||||
read_prompt_input,
|
||||
)
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.serialization import event_from_dict, event_to_dict
|
||||
|
||||
|
||||
class TeamClient:
|
||||
"""Client for interacting with the OpenHands HTTP and WebSocket APIs."""
|
||||
|
||||
def __init__(self, base_url: str, api_key: Optional[str] = None):
|
||||
"""Initialize the TeamClient.
|
||||
|
||||
Args:
|
||||
base_url: The base URL for the OpenHands API.
|
||||
api_key: Optional API key for authentication.
|
||||
"""
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_key = api_key
|
||||
self.sio = socketio.AsyncClient()
|
||||
self.console = Console()
|
||||
self.headers = {}
|
||||
if api_key:
|
||||
self.headers['Authorization'] = f'Bearer {api_key}'
|
||||
|
||||
async def list_conversations(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
"""List conversations.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of conversations to return.
|
||||
|
||||
Returns:
|
||||
List of conversation objects.
|
||||
"""
|
||||
async with aiohttp.ClientSession(headers=self.headers) as session:
|
||||
async with session.get(
|
||||
f'{self.base_url}/api/conversations?limit={limit}'
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f'Failed to list conversations: {error_text}')
|
||||
data = await response.json()
|
||||
return data.get('results', [])
|
||||
|
||||
async def create_conversation(
|
||||
self,
|
||||
repository: Optional[str] = None,
|
||||
git_provider: Optional[str] = None,
|
||||
selected_branch: Optional[str] = None,
|
||||
initial_user_msg: Optional[str] = None,
|
||||
conversation_instructions: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Create a new conversation.
|
||||
|
||||
Args:
|
||||
repository: Optional repository name (owner/repo).
|
||||
git_provider: Optional git provider (github or gitlab).
|
||||
selected_branch: Optional branch name.
|
||||
initial_user_msg: Optional initial user message.
|
||||
conversation_instructions: Optional conversation instructions.
|
||||
|
||||
Returns:
|
||||
The conversation ID.
|
||||
"""
|
||||
payload = {
|
||||
'repository': repository,
|
||||
'git_provider': git_provider,
|
||||
'selected_branch': selected_branch,
|
||||
'initial_user_msg': initial_user_msg,
|
||||
'conversation_instructions': conversation_instructions,
|
||||
}
|
||||
# Remove None values
|
||||
payload = {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
async with aiohttp.ClientSession(headers=self.headers) as session:
|
||||
async with session.post(
|
||||
f'{self.base_url}/api/conversations', json=payload
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f'Failed to create conversation: {error_text}')
|
||||
data = await response.json()
|
||||
return data.get('conversation_id')
|
||||
|
||||
async def get_conversation(self, conversation_id: str) -> dict[str, Any]:
|
||||
"""Get conversation details.
|
||||
|
||||
Args:
|
||||
conversation_id: The conversation ID.
|
||||
|
||||
Returns:
|
||||
Conversation details.
|
||||
"""
|
||||
async with aiohttp.ClientSession(headers=self.headers) as session:
|
||||
async with session.get(
|
||||
f'{self.base_url}/api/conversations/{conversation_id}'
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f'Failed to get conversation: {error_text}')
|
||||
return await response.json()
|
||||
|
||||
async def connect_to_conversation(
|
||||
self, conversation_id: str, latest_event_id: int = -1
|
||||
) -> None:
|
||||
"""Connect to a conversation via WebSocket.
|
||||
|
||||
Args:
|
||||
conversation_id: The conversation ID.
|
||||
latest_event_id: The latest event ID to start from.
|
||||
"""
|
||||
|
||||
# Set up event handlers
|
||||
@self.sio.event
|
||||
async def connect():
|
||||
self.console.print('[green]Connected to conversation[/green]')
|
||||
|
||||
@self.sio.event
|
||||
async def disconnect():
|
||||
self.console.print('[yellow]Disconnected from conversation[/yellow]')
|
||||
|
||||
@self.sio.event
|
||||
async def oh_event(data):
|
||||
event = event_from_dict(data)
|
||||
# Create a dummy config object to satisfy the type checker
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
|
||||
dummy_config = OpenHandsConfig()
|
||||
display_event(event, dummy_config)
|
||||
|
||||
# Connect to the WebSocket
|
||||
query = {
|
||||
'conversation_id': conversation_id,
|
||||
'latest_event_id': str(latest_event_id),
|
||||
}
|
||||
if self.api_key:
|
||||
query['session_api_key'] = self.api_key
|
||||
|
||||
await self.sio.connect(
|
||||
f'{self.base_url}',
|
||||
headers=self.headers,
|
||||
transports=['websocket'],
|
||||
socketio_path='socket.io',
|
||||
wait_timeout=10,
|
||||
query=query,
|
||||
)
|
||||
|
||||
async def send_message(self, message: str) -> None:
|
||||
"""Send a message to the conversation.
|
||||
|
||||
Args:
|
||||
message: The message to send.
|
||||
"""
|
||||
event = MessageAction(content=message)
|
||||
event_dict = event_to_dict(event)
|
||||
await self.sio.emit('oh_user_action', event_dict)
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the WebSocket."""
|
||||
await self.sio.disconnect()
|
||||
|
||||
|
||||
async def list_conversations_cmd(client: TeamClient, args: argparse.Namespace) -> None:
|
||||
"""List conversations command.
|
||||
|
||||
Args:
|
||||
client: The TeamClient instance.
|
||||
args: Command line arguments.
|
||||
"""
|
||||
conversations = await client.list_conversations(limit=args.limit)
|
||||
|
||||
if not conversations:
|
||||
print('No conversations found.')
|
||||
return
|
||||
|
||||
table = Table(title='Conversations')
|
||||
table.add_column('ID', style='cyan')
|
||||
table.add_column('Title', style='green')
|
||||
table.add_column('Status', style='magenta')
|
||||
table.add_column('Repository', style='blue')
|
||||
table.add_column('Last Updated', style='yellow')
|
||||
table.add_column('Created', style='yellow')
|
||||
|
||||
for convo in conversations:
|
||||
# Format dates
|
||||
created_at = datetime.fromisoformat(convo['created_at'].replace('Z', '+00:00'))
|
||||
last_updated_at = datetime.fromisoformat(
|
||||
convo['last_updated_at'].replace('Z', '+00:00')
|
||||
)
|
||||
|
||||
created_str = created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
updated_str = last_updated_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Add row to table
|
||||
table.add_row(
|
||||
convo['conversation_id'],
|
||||
convo['title'],
|
||||
convo['status'],
|
||||
convo.get('selected_repository', ''),
|
||||
updated_str,
|
||||
created_str,
|
||||
)
|
||||
|
||||
client.console.print(table)
|
||||
|
||||
|
||||
async def create_conversation_cmd(client: TeamClient, args: argparse.Namespace) -> None:
|
||||
"""Create a conversation command.
|
||||
|
||||
Args:
|
||||
client: The TeamClient instance.
|
||||
args: Command line arguments.
|
||||
"""
|
||||
initial_message = args.message
|
||||
|
||||
# If no message provided, prompt for one
|
||||
if not initial_message:
|
||||
print_formatted_text(HTML('<green>Enter your initial message:</green>'))
|
||||
initial_message = input('> ')
|
||||
|
||||
try:
|
||||
conversation_id = await client.create_conversation(
|
||||
repository=args.repository,
|
||||
git_provider=args.git_provider,
|
||||
selected_branch=args.branch,
|
||||
initial_user_msg=initial_message,
|
||||
conversation_instructions=args.instructions,
|
||||
)
|
||||
|
||||
print_formatted_text(
|
||||
HTML(f'<green>Conversation created with ID: {conversation_id}</green>')
|
||||
)
|
||||
|
||||
if args.join:
|
||||
await join_conversation_cmd(
|
||||
client, argparse.Namespace(conversation_id=conversation_id)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print_formatted_text(HTML(f'<red>Error creating conversation: {str(e)}</red>'))
|
||||
|
||||
|
||||
async def join_conversation_cmd(client: TeamClient, args: argparse.Namespace) -> None:
|
||||
"""Join a conversation command.
|
||||
|
||||
Args:
|
||||
client: The TeamClient instance.
|
||||
args: Command line arguments.
|
||||
"""
|
||||
conversation_id = args.conversation_id
|
||||
|
||||
try:
|
||||
# Get conversation details
|
||||
conversation = await client.get_conversation(conversation_id)
|
||||
|
||||
# Clear screen and show banner
|
||||
clear()
|
||||
display_banner(session_id=conversation_id)
|
||||
|
||||
# Show conversation title
|
||||
title = conversation.get('title', 'Untitled Conversation')
|
||||
display_welcome_message(f'Joined conversation: {title}')
|
||||
|
||||
# Connect to the WebSocket
|
||||
await client.connect_to_conversation(conversation_id)
|
||||
|
||||
# Main conversation loop
|
||||
try:
|
||||
while True:
|
||||
next_message = await read_prompt_input(
|
||||
AgentState.AWAITING_USER_INPUT.value
|
||||
)
|
||||
|
||||
if not next_message.strip():
|
||||
continue
|
||||
|
||||
if next_message.lower() in ['exit', 'quit', '/exit', '/quit']:
|
||||
break
|
||||
|
||||
await client.send_message(next_message)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print('\nDisconnecting...')
|
||||
finally:
|
||||
await client.disconnect()
|
||||
|
||||
except Exception as e:
|
||||
print_formatted_text(HTML(f'<red>Error joining conversation: {str(e)}</red>'))
|
||||
|
||||
|
||||
def get_base_url() -> str:
|
||||
"""Get the base URL for the OpenHands API.
|
||||
|
||||
Returns:
|
||||
The base URL.
|
||||
"""
|
||||
# Check environment variables first
|
||||
base_url = os.environ.get('OPENHANDS_API_URL')
|
||||
if base_url:
|
||||
return base_url
|
||||
|
||||
# Default to staging server
|
||||
return 'https://staging.all-hands.dev'
|
||||
|
||||
|
||||
def get_api_key() -> Optional[str]:
|
||||
"""Get the API key for authentication.
|
||||
|
||||
Returns:
|
||||
The API key, or None if not found.
|
||||
"""
|
||||
return os.environ.get('OPENHANDS_API_KEY')
|
||||
|
||||
|
||||
def setup_parser() -> argparse.ArgumentParser:
|
||||
"""Set up the argument parser for the team CLI.
|
||||
|
||||
Returns:
|
||||
The argument parser.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description='OpenHands Team CLI')
|
||||
parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
# Server configuration
|
||||
parser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or https://staging.all-hands.dev)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key', help='OpenHands API key (default: $OPENHANDS_API_KEY)'
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Command to run')
|
||||
|
||||
# List conversations command
|
||||
list_parser = subparsers.add_parser(
|
||||
'list',
|
||||
help='List conversations',
|
||||
description='List all available conversations',
|
||||
)
|
||||
list_parser.add_argument(
|
||||
'-l',
|
||||
'--limit',
|
||||
type=int,
|
||||
default=20,
|
||||
help='Maximum number of conversations to list',
|
||||
)
|
||||
# Add help formatter
|
||||
list_parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
# Create conversation command
|
||||
create_parser = subparsers.add_parser(
|
||||
'create',
|
||||
help='Create a new conversation',
|
||||
description='Create a new conversation with optional repository and message',
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'-r', '--repository', help='Repository name (owner/repo)'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'-g', '--git-provider', help='Git provider (github or gitlab)'
|
||||
)
|
||||
create_parser.add_argument('-b', '--branch', help='Branch name')
|
||||
create_parser.add_argument('-m', '--message', help='Initial user message')
|
||||
create_parser.add_argument('-i', '--instructions', help='Conversation instructions')
|
||||
create_parser.add_argument(
|
||||
'-j', '--join', action='store_true', help='Join the conversation after creation'
|
||||
)
|
||||
# Add help formatter
|
||||
create_parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
# Join conversation command
|
||||
join_parser = subparsers.add_parser(
|
||||
'join',
|
||||
help='Join an existing conversation',
|
||||
description='Join an existing conversation by ID',
|
||||
)
|
||||
join_parser.add_argument('conversation_id', help='Conversation ID')
|
||||
# Add help formatter
|
||||
join_parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
async def main_async(args: argparse.Namespace) -> None:
|
||||
"""Main async function for the team CLI.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
# Get base URL and API key
|
||||
base_url = args.url or get_base_url()
|
||||
api_key = args.api_key or get_api_key()
|
||||
|
||||
# Create client
|
||||
client = TeamClient(base_url, api_key)
|
||||
|
||||
# Run command
|
||||
if args.command == 'list':
|
||||
await list_conversations_cmd(client, args)
|
||||
elif args.command == 'create':
|
||||
await create_conversation_cmd(client, args)
|
||||
elif args.command == 'join':
|
||||
await join_conversation_cmd(client, args)
|
||||
else:
|
||||
print('No command specified. Use --help for usage information.')
|
||||
|
||||
|
||||
def main(args: Optional[list[str]] = None) -> None:
|
||||
"""Main function for the team CLI.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
parser = setup_parser()
|
||||
|
||||
# If no arguments provided, show help
|
||||
if not args or len(args) == 0:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
# Special case for subcommand help
|
||||
if (
|
||||
len(args) >= 2
|
||||
and args[0] in ['list', 'create', 'join']
|
||||
and args[1] in ['-h', '--help']
|
||||
):
|
||||
# Create a new parser just for this subcommand
|
||||
if args[0] == 'list':
|
||||
subparser = argparse.ArgumentParser(
|
||||
description='List all available conversations',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
subparser.add_argument(
|
||||
'-l',
|
||||
'--limit',
|
||||
type=int,
|
||||
default=20,
|
||||
help='Maximum number of conversations to list',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or https://staging.all-hands.dev)',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'--api-key',
|
||||
help='OpenHands API key (default: $OPENHANDS_API_KEY)',
|
||||
)
|
||||
subparser.print_help()
|
||||
return
|
||||
elif args[0] == 'create':
|
||||
subparser = argparse.ArgumentParser(
|
||||
description='Create a new conversation with optional repository and message',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
subparser.add_argument(
|
||||
'-r',
|
||||
'--repository',
|
||||
help='Repository name (owner/repo)',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'-g',
|
||||
'--git-provider',
|
||||
help='Git provider (github or gitlab)',
|
||||
)
|
||||
subparser.add_argument('-b', '--branch', help='Branch name')
|
||||
subparser.add_argument('-m', '--message', help='Initial user message')
|
||||
subparser.add_argument(
|
||||
'-i',
|
||||
'--instructions',
|
||||
help='Conversation instructions',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'-j',
|
||||
'--join',
|
||||
action='store_true',
|
||||
help='Join the conversation after creation',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or https://staging.all-hands.dev)',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'--api-key',
|
||||
help='OpenHands API key (default: $OPENHANDS_API_KEY)',
|
||||
)
|
||||
subparser.print_help()
|
||||
return
|
||||
elif args[0] == 'join':
|
||||
subparser = argparse.ArgumentParser(
|
||||
description='Join an existing conversation by ID',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
subparser.add_argument('conversation_id', help='Conversation ID')
|
||||
subparser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or https://staging.all-hands.dev)',
|
||||
)
|
||||
subparser.add_argument(
|
||||
'--api-key',
|
||||
help='OpenHands API key (default: $OPENHANDS_API_KEY)',
|
||||
)
|
||||
subparser.print_help()
|
||||
return
|
||||
|
||||
try:
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
||||
# If no command specified, show help
|
||||
if not parsed_args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
# Run the command
|
||||
asyncio.run(main_async(parsed_args))
|
||||
except KeyboardInterrupt:
|
||||
print('\nOperation cancelled by user.')
|
||||
except Exception as e:
|
||||
print(f'Error: {str(e)}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
7
openhands/cli/team_cli.sh
Executable file
7
openhands/cli/team_cli.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get the Python executable
|
||||
PYTHON_EXE=$(which python)
|
||||
|
||||
# Run the team CLI
|
||||
$PYTHON_EXE -m openhands.cli.team "$@"
|
||||
76
openhands/cli/team_create.py
Normal file
76
openhands/cli/team_create.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Create conversation command for the OpenHands Team CLI."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from openhands.cli.team import TeamClient
|
||||
|
||||
|
||||
def setup_parser() -> argparse.ArgumentParser:
|
||||
"""Set up the argument parser for the create command.
|
||||
|
||||
Returns:
|
||||
The argument parser.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Create a new conversation with optional repository and message',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument('-r', '--repository', help='Repository name (owner/repo)')
|
||||
parser.add_argument('-g', '--git-provider', help='Git provider (github or gitlab)')
|
||||
parser.add_argument('-b', '--branch', help='Branch name')
|
||||
parser.add_argument('-m', '--message', help='Initial user message')
|
||||
parser.add_argument('-i', '--instructions', help='Conversation instructions')
|
||||
parser.add_argument(
|
||||
'-j', '--join', action='store_true', help='Join the conversation after creation'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or http://localhost:3000)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key', help='OpenHands API key (default: $OPENHANDS_API_KEY)'
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
async def create_conversation(args: argparse.Namespace) -> None:
|
||||
"""Create a conversation command.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
# Create client
|
||||
client = TeamClient(args.url, args.api_key)
|
||||
|
||||
try:
|
||||
# Create conversation
|
||||
await client.create_conversation(
|
||||
repository=args.repository,
|
||||
git_provider=args.git_provider,
|
||||
selected_branch=args.branch,
|
||||
initial_user_msg=args.message,
|
||||
conversation_instructions=args.instructions,
|
||||
)
|
||||
except Exception as e:
|
||||
print(f'Error creating conversation: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(args: Optional[list[str]] = None) -> None:
|
||||
"""Main function for the create command.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
parser = setup_parser()
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
||||
import asyncio
|
||||
|
||||
asyncio.run(create_conversation(parsed_args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
63
openhands/cli/team_join.py
Normal file
63
openhands/cli/team_join.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Join conversation command for the OpenHands Team CLI."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from openhands.cli.team import TeamClient, join_conversation_cmd
|
||||
|
||||
|
||||
def setup_parser() -> argparse.ArgumentParser:
|
||||
"""Set up the argument parser for the join command.
|
||||
|
||||
Returns:
|
||||
The argument parser.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Join an existing conversation by ID',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument('conversation_id', help='Conversation ID')
|
||||
parser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or http://localhost:3000)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key', help='OpenHands API key (default: $OPENHANDS_API_KEY)'
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
async def join_conversation(args: argparse.Namespace) -> None:
|
||||
"""Join a conversation command.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
# Create client
|
||||
client = TeamClient(args.url, args.api_key)
|
||||
|
||||
try:
|
||||
# Join conversation
|
||||
await join_conversation_cmd(client, args)
|
||||
except Exception as e:
|
||||
print(f'Error joining conversation: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(args: Optional[list[str]] = None) -> None:
|
||||
"""Main function for the join command.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
parser = setup_parser()
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
||||
import asyncio
|
||||
|
||||
asyncio.run(join_conversation(parsed_args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
69
openhands/cli/team_list.py
Normal file
69
openhands/cli/team_list.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""List conversations command for the OpenHands Team CLI."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from openhands.cli.team import TeamClient
|
||||
|
||||
|
||||
def setup_parser() -> argparse.ArgumentParser:
|
||||
"""Set up the argument parser for the list command.
|
||||
|
||||
Returns:
|
||||
The argument parser.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='List all available conversations',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l',
|
||||
'--limit',
|
||||
type=int,
|
||||
default=20,
|
||||
help='Maximum number of conversations to list',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--url',
|
||||
help='OpenHands API URL (default: $OPENHANDS_API_URL or http://localhost:3000)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--api-key', help='OpenHands API key (default: $OPENHANDS_API_KEY)'
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
async def list_conversations(args: argparse.Namespace) -> None:
|
||||
"""List conversations command.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
# Create client
|
||||
client = TeamClient(args.url, args.api_key)
|
||||
|
||||
try:
|
||||
# List conversations
|
||||
await client.list_conversations(limit=args.limit)
|
||||
except Exception as e:
|
||||
print(f'Error listing conversations: {e}')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main(args: Optional[list[str]] = None) -> None:
|
||||
"""Main function for the list command.
|
||||
|
||||
Args:
|
||||
args: Command line arguments.
|
||||
"""
|
||||
parser = setup_parser()
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
||||
import asyncio
|
||||
|
||||
asyncio.run(list_conversations(parsed_args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -744,13 +744,26 @@ def get_parser() -> argparse.ArgumentParser:
|
||||
type=bool,
|
||||
default=False,
|
||||
)
|
||||
# Add team subcommand
|
||||
subparsers = parser.add_subparsers(dest='command')
|
||||
subparsers.add_parser(
|
||||
'team', help='Use team mode to interact with the OpenHands API'
|
||||
)
|
||||
# We'll handle the team subcommands separately
|
||||
return parser
|
||||
|
||||
|
||||
def parse_arguments() -> argparse.Namespace:
|
||||
"""Parse command line arguments."""
|
||||
parser = get_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if 'team' command is present
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'team':
|
||||
# Only parse known arguments, ignoring any team-specific arguments
|
||||
args, _ = parser.parse_known_args()
|
||||
else:
|
||||
# Parse all arguments normally
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version:
|
||||
print(f'OpenHands version: {__version__}')
|
||||
|
||||
@@ -48,6 +48,7 @@ dirhash = "*"
|
||||
tornado = "*"
|
||||
python-dotenv = "*"
|
||||
rapidfuzz = "^3.9.0"
|
||||
rich = "^13.7.0"
|
||||
whatthepatch = "^1.0.6"
|
||||
protobuf = "^5.0.0,<6.0.0" # Updated to support newer opentelemetry
|
||||
opentelemetry-api = "^1.33.1"
|
||||
|
||||
Reference in New Issue
Block a user