Skip to content

Commit af09fee

Browse files
Port over minimal set of agent, config, context, microagent; adjust existing tool definition (#18)
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 6887c51 commit af09fee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2402
-86
lines changed

.github/workflows/tests.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: Run tests
22
on:
33
pull_request:
44
branches: ["**"]
5-
pull_request:
6-
branches: ["**"]
75

86
permissions:
97
contents: write
@@ -26,19 +24,20 @@ jobs:
2624

2725
- name: Install dependencies
2826
run: uv sync --frozen --group dev
29-
30-
- name: Install project (editable)
31-
run: uv pip install -e .
32-
27+
3328
- name: Run tests with coverage
34-
run: uv run pytest -v --cov=openhands --cov-report=xml:coverage.xml --cov-report=term-missing tests/
29+
run: CI=true uv run pytest -vvxss --basetemp="${{ runner.temp }}/pytest" -o tmp_path_retention=none -o tmp_path_retention_count=0 --cov=openhands --cov-report=term-missing tests/
30+
31+
- name: Build coverage XML (separate step, lower mem)
32+
if: always()
33+
run: uv run coverage xml -i -o coverage.xml
3534

3635
- name: Code Coverage Summary
3736
if: ${{ always() && hashFiles('coverage.xml') != '' }}
3837
continue-on-error: true
3938
uses: irongut/CodeCoverageSummary@v1.3.0
4039
with:
41-
files: coverage.xml
40+
filename: coverage.xml
4241
badge: true
4342
fail_below_min: false
4443
format: markdown

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,21 @@ This folder contains my tasks of completely refactor [OpenHands](https://github.
1717
- read [openhands/core/runtime/tools/str_replace_editor/impl.py](openhands/core/runtime/tools/str_replace_editor/impl.py) for tool execute_fn
1818
- read [openhands/core/runtime/tools/str_replace_editor/definition.py](openhands/core/runtime/tools/str_replace_editor/definition.py) for how do we define a tool
1919
- read [openhands/core/runtime/tools/str_replace_editor/__init__.py](openhands/core/runtime/tools/str_replace_editor/__init__.py) for how we define each tool module
20+
- tools: `str_replace_editor`, `execute_bash`
21+
- minimal config (OpenHandsConfig, LLMConfig, MCPConfig): `openhands/core/config`
22+
- core set of LLM (w/o tests): `openhands/core/llm`
23+
- core set of microagent functionality (w/o full integration):
24+
- `openhands/core/context`: redesigned the triggering of microagents w.r.t. agents into the concept of two types context
25+
- EnvContext (triggered at the begining of a convo)
26+
- MessageContext (triggered at each user message)
27+
- `openhands-v1/openhands/core/microagents`: old code from V1 that loads microagents from folders, etc
28+
- minimal implementation of codeact agent: `openhands-v1/openhands/core/agenthub/codeact_agent`
2029
- ...
30+
31+
32+
**Check hello world example**
33+
34+
```bash
35+
uv sync
36+
uv run python examples/hello.py
37+
```

examples/hello_world.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
from pydantic import SecretStr
3+
from openhands.core import (
4+
OpenHandsConfig,
5+
Conversation,
6+
LLMConfig,
7+
Message,
8+
TextContent,
9+
LLM,
10+
Tool,
11+
get_logger,
12+
CodeActAgent,
13+
)
14+
from openhands.core.runtime.tools import (
15+
BashExecutor,
16+
FileEditorExecutor,
17+
execute_bash_tool,
18+
str_replace_editor_tool,
19+
)
20+
21+
logger = get_logger(__name__)
22+
23+
# Configure LLM
24+
api_key = os.getenv("LITELLM_API_KEY")
25+
assert api_key is not None, "LITELLM_API_KEY environment variable is not set."
26+
config = OpenHandsConfig(
27+
llm=LLMConfig(
28+
model="litellm_proxy/anthropic/claude-sonnet-4-20250514",
29+
base_url="https://llm-proxy.eval.all-hands.dev",
30+
api_key=SecretStr(api_key),
31+
)
32+
)
33+
llm = LLM(config=config.llm)
34+
35+
# Tools
36+
cwd = os.getcwd()
37+
bash = BashExecutor(working_dir=cwd)
38+
file_editor = FileEditorExecutor()
39+
tools: list[Tool] = [
40+
execute_bash_tool.set_executor(executor=bash),
41+
str_replace_editor_tool.set_executor(executor=file_editor),
42+
]
43+
44+
# Agent
45+
agent = CodeActAgent(llm=llm, tools=tools)
46+
conversation = Conversation(agent=agent)
47+
48+
conversation.send_message(
49+
message=Message(
50+
role="user",
51+
content=[
52+
TextContent(
53+
text="Hello! Can you create a new Python file named hello.py that prints 'Hello, World!'?"
54+
)
55+
],
56+
)
57+
)

openhands/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,37 @@
11
"""OpenHands package."""
2+
3+
import os
4+
from pathlib import Path
5+
from importlib.metadata import PackageNotFoundError, version
6+
7+
__package_name__ = "openhands-ai"
8+
9+
10+
def get_version():
11+
# Try getting the version from pyproject.toml
12+
try:
13+
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14+
candidate_paths = [
15+
Path(root_dir) / "pyproject.toml",
16+
Path(root_dir) / "openhands" / "pyproject.toml",
17+
]
18+
for file_path in candidate_paths:
19+
if file_path.is_file():
20+
with open(file_path, "r") as f:
21+
for line in f:
22+
if line.strip().startswith("version ="):
23+
return line.split("=", 1)[1].strip().strip('"').strip("'")
24+
except FileNotFoundError:
25+
pass
26+
27+
try:
28+
return version(__package_name__)
29+
except (ImportError, PackageNotFoundError):
30+
pass
31+
return "unknown"
32+
33+
34+
try:
35+
__version__ = get_version()
36+
except Exception:
37+
__version__ = "unknown"

openhands/core/__init__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from .agenthub import AgentBase, CodeActAgent
2+
from .llm import LLM, Message, TextContent, ImageContent
3+
from .runtime import Tool, ActionBase, ObservationBase
4+
from .config import OpenHandsConfig, LLMConfig, MCPConfig
5+
from .logger import get_logger
6+
from .conversation import Conversation
7+
8+
__all__ = [
9+
"LLM",
10+
"Message",
11+
"TextContent",
12+
"ImageContent",
13+
"Tool",
14+
"AgentBase",
15+
"CodeActAgent",
16+
"ActionBase",
17+
"ObservationBase",
18+
"OpenHandsConfig",
19+
"LLMConfig",
20+
"MCPConfig",
21+
"get_logger",
22+
"Conversation",
23+
]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .codeact_agent import CodeActAgent
2+
from .agent import AgentBase
3+
from .history import AgentHistory
4+
5+
__all__ = [
6+
"CodeActAgent",
7+
"AgentBase",
8+
"AgentHistory",
9+
]

openhands/core/agenthub/agent.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Callable
2+
3+
from openhands.core.llm import LLM
4+
from openhands.core.runtime import Tool, ActionBase, ObservationBase
5+
from openhands.core.context.env_context import EnvContext
6+
from openhands.core.llm.message import Message
7+
from openhands.core.logger import get_logger
8+
9+
logger = get_logger(__name__)
10+
11+
12+
class AgentBase:
13+
def __init__(
14+
self,
15+
llm: LLM,
16+
tools: list[Tool],
17+
env_context: EnvContext | None = None,
18+
) -> None:
19+
"""Initializes a new instance of the Agent class."""
20+
self._llm = llm
21+
self._tools = tools
22+
self._name_to_tool: dict[str, Tool] = {}
23+
for tool in tools:
24+
if tool.name in self._name_to_tool:
25+
raise ValueError(f"Duplicate tool name: {tool.name}")
26+
logger.debug(f"Registering tool: {tool}")
27+
self._name_to_tool[tool.name] = tool
28+
self._env_context = env_context
29+
30+
@property
31+
def name(self) -> str:
32+
"""Returns the name of the Agent."""
33+
return self.__class__.__name__
34+
35+
@property
36+
def llm(self) -> LLM:
37+
"""Returns the LLM instance used by the Agent."""
38+
return self._llm
39+
40+
@property
41+
def tools(self) -> list[Tool]:
42+
"""Returns the list of tools available to the Agent."""
43+
return self._tools
44+
45+
def get_tool(self, name: str) -> Tool | None:
46+
"""Returns the tool with the given name, or None if not found."""
47+
return self._name_to_tool.get(name)
48+
49+
@property
50+
def env_context(self) -> EnvContext | None:
51+
"""Returns the environment context used by the Agent."""
52+
return self._env_context
53+
54+
def reset(self) -> None:
55+
"""Resets the Agent's internal state."""
56+
pass
57+
58+
def run(
59+
self,
60+
user_input: Message,
61+
on_event: Callable[[Message | ActionBase | ObservationBase], None]
62+
| None = None,
63+
) -> None:
64+
"""Runs the Agent with the given input and returns the output.
65+
66+
The agent will stop when it reaches a terminal state, such as
67+
completing its task by calling "finish" or messaging the user by calling "message".
68+
Implementations should invoke `on_event` (if provided) for any
69+
Messages, Actions, or Observations they produce.
70+
"""
71+
raise NotImplementedError("Subclasses must implement this method.")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .codeact_agent import CodeActAgent
2+
3+
__all__ = [
4+
"CodeActAgent",
5+
]

0 commit comments

Comments
 (0)