openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from openhands.sdk.llm.utils.model_features import (
|
|
4
|
-
get_features,
|
|
5
|
-
model_matches,
|
|
6
|
-
normalize_model_name,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@pytest.mark.parametrize(
|
|
11
|
-
"raw,expected",
|
|
12
|
-
[
|
|
13
|
-
(" OPENAI/gpt-4o ", "gpt-4o"),
|
|
14
|
-
("anthropic/claude-3-7-sonnet", "claude-3-7-sonnet"),
|
|
15
|
-
("litellm_proxy/gemini-2.5-pro", "gemini-2.5-pro"),
|
|
16
|
-
("qwen3-coder-480b-a35b-instruct", "qwen3-coder-480b-a35b-instruct"),
|
|
17
|
-
("gpt-5", "gpt-5"),
|
|
18
|
-
("openai/GLM-4.5-GGUF", "glm-4.5"),
|
|
19
|
-
("openrouter/gpt-4o-mini", "gpt-4o-mini"),
|
|
20
|
-
(
|
|
21
|
-
"bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0",
|
|
22
|
-
"anthropic.claude-3-5-sonnet-20241022-v2",
|
|
23
|
-
),
|
|
24
|
-
("", ""),
|
|
25
|
-
(None, ""), # type: ignore[arg-type]
|
|
26
|
-
],
|
|
27
|
-
)
|
|
28
|
-
def test_normalize_model_name(raw, expected):
|
|
29
|
-
assert normalize_model_name(raw) == expected
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.mark.parametrize(
|
|
33
|
-
"name,pattern,expected",
|
|
34
|
-
[
|
|
35
|
-
("gpt-4o", "gpt-4o*", True),
|
|
36
|
-
("openai/gpt-4o", "gpt-4o*", True),
|
|
37
|
-
("litellm_proxy/gpt-4o-mini", "gpt-4o*", True),
|
|
38
|
-
("claude-3-7-sonnet-20250219", "claude-3-7-sonnet*", True),
|
|
39
|
-
("o1-2024-12-17", "o1*", True),
|
|
40
|
-
("grok-4-0709", "grok-4-0709", True),
|
|
41
|
-
("grok-4-0801", "grok-4-0709", False),
|
|
42
|
-
],
|
|
43
|
-
)
|
|
44
|
-
def test_model_matches(name, pattern, expected):
|
|
45
|
-
assert model_matches(name, [pattern]) is expected
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@pytest.mark.parametrize(
|
|
49
|
-
"model,expected_function_calling",
|
|
50
|
-
[
|
|
51
|
-
("gpt-4o", True),
|
|
52
|
-
("gpt-4o-mini", True),
|
|
53
|
-
("claude-3-5-sonnet", True),
|
|
54
|
-
("claude-3-7-sonnet", True),
|
|
55
|
-
("gemini-2.5-pro", True),
|
|
56
|
-
(
|
|
57
|
-
"llama-3.1-70b",
|
|
58
|
-
False,
|
|
59
|
-
), # Most open source models don't support native function calling
|
|
60
|
-
("unknown-model", False), # Default to False for unknown models
|
|
61
|
-
],
|
|
62
|
-
)
|
|
63
|
-
def test_function_calling_support(model, expected_function_calling):
|
|
64
|
-
features = get_features(model)
|
|
65
|
-
assert features.supports_function_calling == expected_function_calling
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@pytest.mark.parametrize(
|
|
69
|
-
"model,expected_reasoning",
|
|
70
|
-
[
|
|
71
|
-
("o1-2024-12-17", True),
|
|
72
|
-
("o1", True),
|
|
73
|
-
("o3-mini", True),
|
|
74
|
-
("o3", True),
|
|
75
|
-
("gpt-4o", False),
|
|
76
|
-
("claude-3-5-sonnet", False),
|
|
77
|
-
("gemini-1.5-pro", False),
|
|
78
|
-
("unknown-model", False),
|
|
79
|
-
],
|
|
80
|
-
)
|
|
81
|
-
def test_reasoning_effort_support(model, expected_reasoning):
|
|
82
|
-
features = get_features(model)
|
|
83
|
-
assert features.supports_reasoning_effort == expected_reasoning
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@pytest.mark.parametrize(
|
|
87
|
-
"model,expected_cache",
|
|
88
|
-
[
|
|
89
|
-
("claude-3-5-sonnet", True),
|
|
90
|
-
("claude-3-7-sonnet", True),
|
|
91
|
-
("claude-3-haiku-20240307", True),
|
|
92
|
-
("claude-3-opus-20240229", True),
|
|
93
|
-
("gpt-4o", False), # OpenAI doesn't support explicit prompt caching
|
|
94
|
-
("gemini-1.5-pro", False),
|
|
95
|
-
("unknown-model", False),
|
|
96
|
-
],
|
|
97
|
-
)
|
|
98
|
-
def test_prompt_cache_support(model, expected_cache):
|
|
99
|
-
features = get_features(model)
|
|
100
|
-
assert features.supports_prompt_cache == expected_cache
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@pytest.mark.parametrize(
|
|
104
|
-
"model,expected_stop_words",
|
|
105
|
-
[
|
|
106
|
-
("gpt-4o", True),
|
|
107
|
-
("gpt-4o-mini", True),
|
|
108
|
-
("claude-3-5-sonnet", True),
|
|
109
|
-
("gemini-1.5-pro", True),
|
|
110
|
-
("llama-3.1-70b", True),
|
|
111
|
-
("unknown-model", True), # Most models support stop words
|
|
112
|
-
],
|
|
113
|
-
)
|
|
114
|
-
def test_stop_words_support(model, expected_stop_words):
|
|
115
|
-
features = get_features(model)
|
|
116
|
-
assert features.supports_stop_words == expected_stop_words
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_get_features_with_provider_prefix():
|
|
120
|
-
"""Test that get_features works with provider prefixes."""
|
|
121
|
-
# Test with various provider prefixes
|
|
122
|
-
assert get_features("openai/gpt-4o").supports_function_calling is True
|
|
123
|
-
assert get_features("anthropic/claude-3-5-sonnet").supports_function_calling is True
|
|
124
|
-
assert get_features("litellm_proxy/gpt-4o").supports_function_calling is True
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def test_get_features_case_insensitive():
|
|
128
|
-
"""Test that get_features is case insensitive."""
|
|
129
|
-
features_lower = get_features("gpt-4o")
|
|
130
|
-
features_upper = get_features("GPT-4O")
|
|
131
|
-
features_mixed = get_features("Gpt-4O")
|
|
132
|
-
|
|
133
|
-
assert (
|
|
134
|
-
features_lower.supports_function_calling
|
|
135
|
-
== features_upper.supports_function_calling
|
|
136
|
-
)
|
|
137
|
-
assert (
|
|
138
|
-
features_lower.supports_reasoning_effort
|
|
139
|
-
== features_upper.supports_reasoning_effort
|
|
140
|
-
)
|
|
141
|
-
assert (
|
|
142
|
-
features_lower.supports_function_calling
|
|
143
|
-
== features_mixed.supports_function_calling
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def test_get_features_with_version_suffixes():
|
|
148
|
-
"""Test that get_features handles version suffixes correctly."""
|
|
149
|
-
# Test that version suffixes are handled properly
|
|
150
|
-
base_features = get_features("claude-3-5-sonnet")
|
|
151
|
-
versioned_features = get_features("claude-3-5-sonnet-20241022")
|
|
152
|
-
|
|
153
|
-
assert (
|
|
154
|
-
base_features.supports_function_calling
|
|
155
|
-
== versioned_features.supports_function_calling
|
|
156
|
-
)
|
|
157
|
-
assert (
|
|
158
|
-
base_features.supports_reasoning_effort
|
|
159
|
-
== versioned_features.supports_reasoning_effort
|
|
160
|
-
)
|
|
161
|
-
assert (
|
|
162
|
-
base_features.supports_prompt_cache == versioned_features.supports_prompt_cache
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def test_model_matches_multiple_patterns():
|
|
167
|
-
"""Test model_matches with multiple patterns."""
|
|
168
|
-
patterns = ["gpt-4*", "claude-3*", "gemini-*"]
|
|
169
|
-
|
|
170
|
-
assert model_matches("gpt-4o", patterns) is True
|
|
171
|
-
assert model_matches("claude-3-5-sonnet", patterns) is True
|
|
172
|
-
assert model_matches("gemini-1.5-pro", patterns) is True
|
|
173
|
-
assert model_matches("llama-3.1-70b", patterns) is False
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def test_model_matches_exact_match():
|
|
177
|
-
"""Test model_matches with exact patterns (no wildcards)."""
|
|
178
|
-
patterns = ["gpt-4o", "claude-3-5-sonnet"]
|
|
179
|
-
|
|
180
|
-
assert model_matches("gpt-4o", patterns) is True
|
|
181
|
-
assert model_matches("claude-3-5-sonnet", patterns) is True
|
|
182
|
-
assert model_matches("gpt-4o-mini", patterns) is False
|
|
183
|
-
assert model_matches("claude-3-haiku", patterns) is False
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def test_normalize_model_name_edge_cases():
|
|
187
|
-
"""Test normalize_model_name with edge cases."""
|
|
188
|
-
# Test with multiple slashes
|
|
189
|
-
assert normalize_model_name("provider/sub/model-name") == "model-name"
|
|
190
|
-
|
|
191
|
-
# Test with colons and special characters
|
|
192
|
-
assert normalize_model_name("provider/model:version:tag") == "model"
|
|
193
|
-
|
|
194
|
-
# Test with whitespace and case
|
|
195
|
-
assert normalize_model_name(" PROVIDER/Model-Name ") == "model-name"
|
|
196
|
-
|
|
197
|
-
# Test with underscores and hyphens
|
|
198
|
-
assert normalize_model_name("provider/model_name-v1") == "model_name-v1"
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def test_get_features_unknown_model():
|
|
202
|
-
"""Test get_features with completely unknown model."""
|
|
203
|
-
features = get_features("completely-unknown-model-12345")
|
|
204
|
-
|
|
205
|
-
# Unknown models should have conservative defaults
|
|
206
|
-
assert features.supports_function_calling is False
|
|
207
|
-
assert features.supports_reasoning_effort is False
|
|
208
|
-
assert features.supports_prompt_cache is False
|
|
209
|
-
assert features.supports_stop_words is True # Most models support stop words
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def test_get_features_empty_model():
|
|
213
|
-
"""Test get_features with empty or None model."""
|
|
214
|
-
features_empty = get_features("")
|
|
215
|
-
features_none = get_features(None) # type: ignore[arg-type]
|
|
216
|
-
|
|
217
|
-
# Both should return conservative defaults
|
|
218
|
-
assert features_empty.supports_function_calling is False
|
|
219
|
-
assert features_none.supports_function_calling is False
|
|
220
|
-
assert features_empty.supports_reasoning_effort is False
|
|
221
|
-
assert features_none.supports_reasoning_effort is False
|
openhands/sdk/tool/__init__.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""OpenHands runtime package."""
|
|
2
|
-
|
|
3
|
-
from openhands.sdk.tool.builtins import BUILT_IN_TOOLS, FinishTool, ThinkTool
|
|
4
|
-
from openhands.sdk.tool.schema import (
|
|
5
|
-
Action,
|
|
6
|
-
ActionBase,
|
|
7
|
-
MCPActionBase,
|
|
8
|
-
Observation,
|
|
9
|
-
ObservationBase,
|
|
10
|
-
)
|
|
11
|
-
from openhands.sdk.tool.tool import (
|
|
12
|
-
Tool,
|
|
13
|
-
ToolAnnotations,
|
|
14
|
-
ToolExecutor,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
__all__ = [
|
|
19
|
-
"Tool",
|
|
20
|
-
"ToolAnnotations",
|
|
21
|
-
"ToolExecutor",
|
|
22
|
-
"ActionBase",
|
|
23
|
-
"MCPActionBase",
|
|
24
|
-
"Action",
|
|
25
|
-
"ObservationBase",
|
|
26
|
-
"Observation",
|
|
27
|
-
"FinishTool",
|
|
28
|
-
"ThinkTool",
|
|
29
|
-
"BUILT_IN_TOOLS",
|
|
30
|
-
]
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""Implementing essential tools that doesn't interact with the environment.
|
|
2
|
-
|
|
3
|
-
These are built in and are *required* for the agent to work.
|
|
4
|
-
|
|
5
|
-
For tools that require interacting with the environment, add them to `openhands/tools`.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from openhands.sdk.tool.builtins.finish import (
|
|
9
|
-
FinishAction,
|
|
10
|
-
FinishExecutor,
|
|
11
|
-
FinishObservation,
|
|
12
|
-
FinishTool,
|
|
13
|
-
)
|
|
14
|
-
from openhands.sdk.tool.builtins.think import (
|
|
15
|
-
ThinkAction,
|
|
16
|
-
ThinkExecutor,
|
|
17
|
-
ThinkObservation,
|
|
18
|
-
ThinkTool,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
BUILT_IN_TOOLS = [FinishTool, ThinkTool]
|
|
23
|
-
|
|
24
|
-
__all__ = [
|
|
25
|
-
"BUILT_IN_TOOLS",
|
|
26
|
-
"FinishTool",
|
|
27
|
-
"FinishAction",
|
|
28
|
-
"FinishObservation",
|
|
29
|
-
"FinishExecutor",
|
|
30
|
-
"ThinkTool",
|
|
31
|
-
"ThinkAction",
|
|
32
|
-
"ThinkObservation",
|
|
33
|
-
"ThinkExecutor",
|
|
34
|
-
]
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from pydantic import Field
|
|
2
|
-
|
|
3
|
-
from openhands.sdk.llm.message import ImageContent, TextContent
|
|
4
|
-
from openhands.sdk.tool.tool import (
|
|
5
|
-
ActionBase,
|
|
6
|
-
ObservationBase,
|
|
7
|
-
Tool,
|
|
8
|
-
ToolAnnotations,
|
|
9
|
-
ToolExecutor,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class FinishAction(ActionBase):
|
|
14
|
-
message: str = Field(description="Final message to send to the user.")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class FinishObservation(ObservationBase):
|
|
18
|
-
message: str = Field(description="Final message sent to the user.")
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
def agent_observation(self) -> list[TextContent | ImageContent]:
|
|
22
|
-
return [TextContent(text=self.message)]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
TOOL_DESCRIPTION = """Signals the completion of the current task or conversation.
|
|
26
|
-
|
|
27
|
-
Use this tool when:
|
|
28
|
-
- You have successfully completed the user's requested task
|
|
29
|
-
- You cannot proceed further due to technical limitations or missing information
|
|
30
|
-
|
|
31
|
-
The message should include:
|
|
32
|
-
- A clear summary of actions taken and their results
|
|
33
|
-
- Any next steps for the user
|
|
34
|
-
- Explanation if you're unable to complete the task
|
|
35
|
-
- Any follow-up questions if more information is needed
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class FinishExecutor(ToolExecutor):
|
|
40
|
-
def __call__(self, action: FinishAction) -> FinishObservation:
|
|
41
|
-
return FinishObservation(message=action.message)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
FinishTool = Tool(
|
|
45
|
-
name="finish",
|
|
46
|
-
input_schema=FinishAction,
|
|
47
|
-
output_schema=FinishObservation,
|
|
48
|
-
description=TOOL_DESCRIPTION,
|
|
49
|
-
executor=FinishExecutor(),
|
|
50
|
-
annotations=ToolAnnotations(
|
|
51
|
-
title="finish",
|
|
52
|
-
readOnlyHint=True,
|
|
53
|
-
destructiveHint=False,
|
|
54
|
-
idempotentHint=True,
|
|
55
|
-
openWorldHint=False,
|
|
56
|
-
),
|
|
57
|
-
)
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
from pydantic import Field
|
|
2
|
-
|
|
3
|
-
from openhands.sdk.llm.message import ImageContent, TextContent
|
|
4
|
-
from openhands.sdk.tool.tool import (
|
|
5
|
-
ActionBase,
|
|
6
|
-
ObservationBase,
|
|
7
|
-
Tool,
|
|
8
|
-
ToolAnnotations,
|
|
9
|
-
ToolExecutor,
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ThinkAction(ActionBase):
|
|
14
|
-
"""Action for logging a thought without making any changes."""
|
|
15
|
-
|
|
16
|
-
thought: str = Field(description="The thought to log.")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ThinkObservation(ObservationBase):
|
|
20
|
-
"""Observation returned after logging a thought."""
|
|
21
|
-
|
|
22
|
-
content: str = Field(
|
|
23
|
-
default="Your thought has been logged.", description="Confirmation message."
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
@property
|
|
27
|
-
def agent_observation(self) -> list[TextContent | ImageContent]:
|
|
28
|
-
return [TextContent(text=self.content)]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
THINK_DESCRIPTION = """Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed.
|
|
32
|
-
|
|
33
|
-
Common use cases:
|
|
34
|
-
1. When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective.
|
|
35
|
-
2. After receiving test results, use this tool to brainstorm ways to fix failing tests.
|
|
36
|
-
3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs.
|
|
37
|
-
4. When designing a new feature, use this tool to think through architecture decisions and implementation details.
|
|
38
|
-
5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses.
|
|
39
|
-
|
|
40
|
-
The tool simply logs your thought process for better transparency and does not execute any code or make changes.""" # noqa: E501
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class ThinkExecutor(ToolExecutor):
|
|
44
|
-
def __call__(self, _: ThinkAction) -> ThinkObservation:
|
|
45
|
-
return ThinkObservation()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ThinkTool = Tool(
|
|
49
|
-
name="think",
|
|
50
|
-
description=THINK_DESCRIPTION,
|
|
51
|
-
input_schema=ThinkAction,
|
|
52
|
-
output_schema=ThinkObservation,
|
|
53
|
-
executor=ThinkExecutor(),
|
|
54
|
-
annotations=ToolAnnotations(
|
|
55
|
-
readOnlyHint=True,
|
|
56
|
-
destructiveHint=False,
|
|
57
|
-
idempotentHint=True,
|
|
58
|
-
openWorldHint=False,
|
|
59
|
-
),
|
|
60
|
-
)
|
openhands/sdk/tool/schema.py
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
from typing import Annotated, Any, TypeVar
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel, ConfigDict, Field, create_model
|
|
4
|
-
|
|
5
|
-
from openhands.sdk.llm import ImageContent, TextContent
|
|
6
|
-
from openhands.sdk.tool.security_prompt import (
|
|
7
|
-
SECURITY_RISK_DESC,
|
|
8
|
-
SECURITY_RISK_LITERAL,
|
|
9
|
-
)
|
|
10
|
-
from openhands.sdk.utils.discriminated_union import (
|
|
11
|
-
DiscriminatedUnionMixin,
|
|
12
|
-
DiscriminatedUnionType,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
S = TypeVar("S", bound="Schema")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def py_type(spec: dict[str, Any]) -> Any:
|
|
20
|
-
"""Map JSON schema types to Python types."""
|
|
21
|
-
t = spec.get("type")
|
|
22
|
-
if t == "array":
|
|
23
|
-
items = spec.get("items", {})
|
|
24
|
-
inner = py_type(items) if isinstance(items, dict) else Any
|
|
25
|
-
return list[inner] # type: ignore[index]
|
|
26
|
-
if t == "object":
|
|
27
|
-
return dict[str, Any]
|
|
28
|
-
_map = {
|
|
29
|
-
"string": str,
|
|
30
|
-
"integer": int,
|
|
31
|
-
"number": float,
|
|
32
|
-
"boolean": bool,
|
|
33
|
-
}
|
|
34
|
-
if t in _map:
|
|
35
|
-
return _map[t]
|
|
36
|
-
return Any
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _process_schema_node(node, defs):
|
|
40
|
-
"""Recursively process a schema node to simplify and resolve $ref.
|
|
41
|
-
|
|
42
|
-
https://www.reddit.com/r/mcp/comments/1kjo9gt/toolinputschema_conversion_from_pydanticmodel/
|
|
43
|
-
https://gist.github.com/leandromoreira/3de4819e4e4df9422d87f1d3e7465c16
|
|
44
|
-
"""
|
|
45
|
-
# Handle $ref references
|
|
46
|
-
if "$ref" in node:
|
|
47
|
-
ref_path = node["$ref"]
|
|
48
|
-
if ref_path.startswith("#/$defs/"):
|
|
49
|
-
ref_name = ref_path.split("/")[-1]
|
|
50
|
-
if ref_name in defs:
|
|
51
|
-
# Process the referenced definition
|
|
52
|
-
return _process_schema_node(defs[ref_name], defs)
|
|
53
|
-
|
|
54
|
-
# Start with a new schema object
|
|
55
|
-
result = {}
|
|
56
|
-
|
|
57
|
-
# Copy the basic properties
|
|
58
|
-
if "type" in node:
|
|
59
|
-
result["type"] = node["type"]
|
|
60
|
-
|
|
61
|
-
# Handle anyOf (often used for optional fields with None)
|
|
62
|
-
if "anyOf" in node:
|
|
63
|
-
non_null_types = [t for t in node["anyOf"] if t.get("type") != "null"]
|
|
64
|
-
if non_null_types:
|
|
65
|
-
# Process the first non-null type
|
|
66
|
-
processed = _process_schema_node(non_null_types[0], defs)
|
|
67
|
-
result.update(processed)
|
|
68
|
-
|
|
69
|
-
# Handle description
|
|
70
|
-
if "description" in node:
|
|
71
|
-
result["description"] = node["description"]
|
|
72
|
-
|
|
73
|
-
# Handle object properties recursively
|
|
74
|
-
if node.get("type") == "object" and "properties" in node:
|
|
75
|
-
result["type"] = "object"
|
|
76
|
-
result["properties"] = {}
|
|
77
|
-
|
|
78
|
-
# Process each property
|
|
79
|
-
for prop_name, prop_schema in node["properties"].items():
|
|
80
|
-
result["properties"][prop_name] = _process_schema_node(prop_schema, defs)
|
|
81
|
-
|
|
82
|
-
# Add required fields if present
|
|
83
|
-
if "required" in node:
|
|
84
|
-
result["required"] = node["required"]
|
|
85
|
-
|
|
86
|
-
# Handle arrays
|
|
87
|
-
if node.get("type") == "array" and "items" in node:
|
|
88
|
-
result["type"] = "array"
|
|
89
|
-
result["items"] = _process_schema_node(node["items"], defs)
|
|
90
|
-
|
|
91
|
-
# Handle enum
|
|
92
|
-
if "enum" in node:
|
|
93
|
-
result["enum"] = node["enum"]
|
|
94
|
-
|
|
95
|
-
return result
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class Schema(BaseModel):
|
|
99
|
-
"""Base schema for input action / output observation."""
|
|
100
|
-
|
|
101
|
-
model_config = ConfigDict(extra="forbid")
|
|
102
|
-
|
|
103
|
-
@classmethod
|
|
104
|
-
def to_mcp_schema(cls) -> dict[str, Any]:
|
|
105
|
-
"""Convert to JSON schema format compatible with MCP."""
|
|
106
|
-
full_schema = cls.model_json_schema()
|
|
107
|
-
# This will get rid of all "anyOf" in the schema,
|
|
108
|
-
# so it is fully compatible with MCP tool schema
|
|
109
|
-
return _process_schema_node(full_schema, full_schema.get("$defs", {}))
|
|
110
|
-
|
|
111
|
-
@classmethod
|
|
112
|
-
def from_mcp_schema(
|
|
113
|
-
cls: type[S], model_name: str, schema: dict[str, Any]
|
|
114
|
-
) -> type["S"]:
|
|
115
|
-
"""Create a Schema subclass from an MCP/JSON Schema object.
|
|
116
|
-
|
|
117
|
-
For non-required fields, we annotate as `T | None`
|
|
118
|
-
so explicit nulls are allowed.
|
|
119
|
-
"""
|
|
120
|
-
assert isinstance(schema, dict), "Schema must be a dict"
|
|
121
|
-
assert schema.get("type") == "object", "Only object schemas are supported"
|
|
122
|
-
|
|
123
|
-
props: dict[str, Any] = schema.get("properties", {}) or {}
|
|
124
|
-
required = set(schema.get("required", []) or [])
|
|
125
|
-
|
|
126
|
-
fields: dict[str, tuple] = {}
|
|
127
|
-
for fname, spec in props.items():
|
|
128
|
-
spec = spec if isinstance(spec, dict) else {}
|
|
129
|
-
tp = py_type(spec)
|
|
130
|
-
|
|
131
|
-
# Add description if present
|
|
132
|
-
desc: str | None = spec.get("description")
|
|
133
|
-
|
|
134
|
-
# Required → bare type, ellipsis sentinel
|
|
135
|
-
# Optional → make nullable via `| None`, default None
|
|
136
|
-
if fname in required:
|
|
137
|
-
anno = tp
|
|
138
|
-
default = ...
|
|
139
|
-
else:
|
|
140
|
-
anno = tp | None # allow explicit null in addition to omission
|
|
141
|
-
default = None
|
|
142
|
-
|
|
143
|
-
fields[fname] = (
|
|
144
|
-
anno,
|
|
145
|
-
Field(default=default, description=desc)
|
|
146
|
-
if desc
|
|
147
|
-
else Field(default=default),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
return create_model(model_name, __base__=cls, **fields) # type: ignore[return-value]
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class ActionBase(Schema, DiscriminatedUnionMixin):
|
|
154
|
-
"""Base schema for input action."""
|
|
155
|
-
|
|
156
|
-
# NOTE: We make it optional since some weaker
|
|
157
|
-
# LLMs may not be able to fill it out correctly.
|
|
158
|
-
# https://github.com/All-Hands-AI/OpenHands/issues/10797
|
|
159
|
-
security_risk: SECURITY_RISK_LITERAL = Field(
|
|
160
|
-
default="UNKNOWN", description=SECURITY_RISK_DESC
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
@classmethod
|
|
164
|
-
def to_mcp_schema(cls) -> dict[str, Any]:
|
|
165
|
-
"""Convert to JSON schema format compatible with MCP."""
|
|
166
|
-
schema = super().to_mcp_schema()
|
|
167
|
-
|
|
168
|
-
# We need to move the fields from ActionBase to the END of the properties
|
|
169
|
-
# We use these properties to generate the llm schema for tool calling
|
|
170
|
-
# and we want the ActionBase fields to be at the end
|
|
171
|
-
# e.g. LLM should already outputs the argument for tools
|
|
172
|
-
# BEFORE it predicts security_risk
|
|
173
|
-
assert "properties" in schema, "Schema must have properties"
|
|
174
|
-
for field_name in ActionBase.model_fields.keys():
|
|
175
|
-
if field_name in schema["properties"]:
|
|
176
|
-
v = schema["properties"].pop(field_name)
|
|
177
|
-
schema["properties"][field_name] = v
|
|
178
|
-
return schema
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
class MCPActionBase(ActionBase):
|
|
182
|
-
"""Base schema for MCP input action."""
|
|
183
|
-
|
|
184
|
-
model_config = ConfigDict(extra="allow")
|
|
185
|
-
|
|
186
|
-
# Collect all fields from ActionBase and its parents
|
|
187
|
-
_parent_fields: frozenset[str] = frozenset(
|
|
188
|
-
fname
|
|
189
|
-
for base in ActionBase.__mro__
|
|
190
|
-
if issubclass(base, BaseModel)
|
|
191
|
-
for fname in {
|
|
192
|
-
**base.model_fields,
|
|
193
|
-
**base.model_computed_fields,
|
|
194
|
-
}.keys()
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
def to_mcp_arguments(self) -> dict:
|
|
198
|
-
"""Dump model excluding parent ActionBase fields.
|
|
199
|
-
|
|
200
|
-
This is used to convert this action to MCP tool call arguments.
|
|
201
|
-
The parent fields (e.g., safety_risk, kind) are not part of the MCP tool schema
|
|
202
|
-
but are only used for our internal processing.
|
|
203
|
-
"""
|
|
204
|
-
data = self.model_dump(exclude_none=True)
|
|
205
|
-
for f in self._parent_fields:
|
|
206
|
-
data.pop(f, None)
|
|
207
|
-
return data
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Action = Annotated[ActionBase, DiscriminatedUnionType[ActionBase]]
|
|
211
|
-
"""Type annotation for values that can be any implementation of ActionBase.
|
|
212
|
-
|
|
213
|
-
In most situations, this is equivalent to ActionBase. However, when used in Pydantic
|
|
214
|
-
BaseModels as a field annotation, it enables polymorphic deserialization by delaying the
|
|
215
|
-
discriminator resolution until runtime.
|
|
216
|
-
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class ObservationBase(Schema, DiscriminatedUnionMixin):
|
|
220
|
-
"""Base schema for output observation."""
|
|
221
|
-
|
|
222
|
-
model_config = ConfigDict(extra="allow")
|
|
223
|
-
|
|
224
|
-
@property
|
|
225
|
-
def agent_observation(self) -> list[TextContent | ImageContent]:
|
|
226
|
-
"""Get the observation string to show to the agent."""
|
|
227
|
-
raise NotImplementedError("Subclasses must implement agent_observation")
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Observation = Annotated[ObservationBase, DiscriminatedUnionType[ObservationBase]]
|
|
231
|
-
"""Type annotation for values that can be any implementation of ObservationBase.
|
|
232
|
-
|
|
233
|
-
In most situations, this is equivalent to ObservationBase. However, when used in
|
|
234
|
-
Pydantic BaseModels as a field annotation, it enables polymorphic deserialization by
|
|
235
|
-
delaying the discriminator resolution until runtime.
|
|
236
|
-
"""
|