mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c1484ecf3 | |||
| 33c80d816e | |||
| c164063d19 | |||
| 30f09f4aec | |||
| cb87340cb8 | |||
| 15886acc3f | |||
| c64eef19df | |||
| be0bb3f388 | |||
| c020268f5b |
@@ -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
|
||||||
|
```
|
||||||
+31
-1
@@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
|
||||||
from prompt_toolkit import print_formatted_text
|
from prompt_toolkit import print_formatted_text
|
||||||
from prompt_toolkit.formatted_text import HTML
|
from prompt_toolkit.formatted_text import HTML
|
||||||
@@ -454,6 +453,37 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
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()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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()
|
||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get the Python executable
|
||||||
|
PYTHON_EXE=$(which python)
|
||||||
|
|
||||||
|
# Run the team CLI
|
||||||
|
$PYTHON_EXE -m openhands.cli.team "$@"
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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,
|
type=bool,
|
||||||
default=False,
|
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
|
return parser
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments() -> argparse.Namespace:
|
def parse_arguments() -> argparse.Namespace:
|
||||||
"""Parse command line arguments."""
|
"""Parse command line arguments."""
|
||||||
parser = get_parser()
|
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:
|
if args.version:
|
||||||
print(f'OpenHands version: {__version__}')
|
print(f'OpenHands version: {__version__}')
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ dirhash = "*"
|
|||||||
tornado = "*"
|
tornado = "*"
|
||||||
python-dotenv = "*"
|
python-dotenv = "*"
|
||||||
rapidfuzz = "^3.9.0"
|
rapidfuzz = "^3.9.0"
|
||||||
|
rich = "^13.7.0"
|
||||||
whatthepatch = "^1.0.6"
|
whatthepatch = "^1.0.6"
|
||||||
protobuf = "^5.0.0,<6.0.0" # Updated to support newer opentelemetry
|
protobuf = "^5.0.0,<6.0.0" # Updated to support newer opentelemetry
|
||||||
opentelemetry-api = "^1.33.1"
|
opentelemetry-api = "^1.33.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user