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.

Files changed (124) hide show
  1. openhands-1.0.1.dist-info/METADATA +52 -0
  2. openhands-1.0.1.dist-info/RECORD +31 -0
  3. {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
  4. openhands-1.0.1.dist-info/entry_points.txt +2 -0
  5. openhands_cli/__init__.py +8 -0
  6. openhands_cli/agent_chat.py +186 -0
  7. openhands_cli/argparsers/main_parser.py +56 -0
  8. openhands_cli/argparsers/serve_parser.py +31 -0
  9. openhands_cli/gui_launcher.py +220 -0
  10. openhands_cli/listeners/__init__.py +4 -0
  11. openhands_cli/listeners/loading_listener.py +63 -0
  12. openhands_cli/listeners/pause_listener.py +83 -0
  13. openhands_cli/llm_utils.py +57 -0
  14. openhands_cli/locations.py +13 -0
  15. openhands_cli/pt_style.py +30 -0
  16. openhands_cli/runner.py +178 -0
  17. openhands_cli/setup.py +116 -0
  18. openhands_cli/simple_main.py +59 -0
  19. openhands_cli/tui/__init__.py +5 -0
  20. openhands_cli/tui/settings/mcp_screen.py +217 -0
  21. openhands_cli/tui/settings/settings_screen.py +202 -0
  22. openhands_cli/tui/settings/store.py +93 -0
  23. openhands_cli/tui/status.py +109 -0
  24. openhands_cli/tui/tui.py +100 -0
  25. openhands_cli/tui/utils.py +14 -0
  26. openhands_cli/user_actions/__init__.py +17 -0
  27. openhands_cli/user_actions/agent_action.py +95 -0
  28. openhands_cli/user_actions/exit_session.py +18 -0
  29. openhands_cli/user_actions/settings_action.py +171 -0
  30. openhands_cli/user_actions/types.py +18 -0
  31. openhands_cli/user_actions/utils.py +199 -0
  32. openhands/__init__.py +0 -1
  33. openhands/sdk/__init__.py +0 -45
  34. openhands/sdk/agent/__init__.py +0 -8
  35. openhands/sdk/agent/agent/__init__.py +0 -6
  36. openhands/sdk/agent/agent/agent.py +0 -349
  37. openhands/sdk/agent/base.py +0 -103
  38. openhands/sdk/context/__init__.py +0 -28
  39. openhands/sdk/context/agent_context.py +0 -153
  40. openhands/sdk/context/condenser/__init__.py +0 -5
  41. openhands/sdk/context/condenser/condenser.py +0 -73
  42. openhands/sdk/context/condenser/no_op_condenser.py +0 -13
  43. openhands/sdk/context/manager.py +0 -5
  44. openhands/sdk/context/microagents/__init__.py +0 -26
  45. openhands/sdk/context/microagents/exceptions.py +0 -11
  46. openhands/sdk/context/microagents/microagent.py +0 -345
  47. openhands/sdk/context/microagents/types.py +0 -70
  48. openhands/sdk/context/utils/__init__.py +0 -8
  49. openhands/sdk/context/utils/prompt.py +0 -52
  50. openhands/sdk/context/view.py +0 -116
  51. openhands/sdk/conversation/__init__.py +0 -12
  52. openhands/sdk/conversation/conversation.py +0 -207
  53. openhands/sdk/conversation/state.py +0 -50
  54. openhands/sdk/conversation/types.py +0 -6
  55. openhands/sdk/conversation/visualizer.py +0 -300
  56. openhands/sdk/event/__init__.py +0 -27
  57. openhands/sdk/event/base.py +0 -148
  58. openhands/sdk/event/condenser.py +0 -49
  59. openhands/sdk/event/llm_convertible.py +0 -265
  60. openhands/sdk/event/types.py +0 -5
  61. openhands/sdk/event/user_action.py +0 -12
  62. openhands/sdk/event/utils.py +0 -30
  63. openhands/sdk/llm/__init__.py +0 -19
  64. openhands/sdk/llm/exceptions.py +0 -108
  65. openhands/sdk/llm/llm.py +0 -867
  66. openhands/sdk/llm/llm_registry.py +0 -116
  67. openhands/sdk/llm/message.py +0 -216
  68. openhands/sdk/llm/metadata.py +0 -34
  69. openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
  70. openhands/sdk/llm/utils/metrics.py +0 -311
  71. openhands/sdk/llm/utils/model_features.py +0 -153
  72. openhands/sdk/llm/utils/retry_mixin.py +0 -122
  73. openhands/sdk/llm/utils/telemetry.py +0 -252
  74. openhands/sdk/logger.py +0 -167
  75. openhands/sdk/mcp/__init__.py +0 -20
  76. openhands/sdk/mcp/client.py +0 -113
  77. openhands/sdk/mcp/definition.py +0 -69
  78. openhands/sdk/mcp/tool.py +0 -104
  79. openhands/sdk/mcp/utils.py +0 -59
  80. openhands/sdk/tests/llm/test_llm.py +0 -447
  81. openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
  82. openhands/sdk/tests/llm/test_model_features.py +0 -221
  83. openhands/sdk/tool/__init__.py +0 -30
  84. openhands/sdk/tool/builtins/__init__.py +0 -34
  85. openhands/sdk/tool/builtins/finish.py +0 -57
  86. openhands/sdk/tool/builtins/think.py +0 -60
  87. openhands/sdk/tool/schema.py +0 -236
  88. openhands/sdk/tool/security_prompt.py +0 -5
  89. openhands/sdk/tool/tool.py +0 -142
  90. openhands/sdk/utils/__init__.py +0 -14
  91. openhands/sdk/utils/discriminated_union.py +0 -210
  92. openhands/sdk/utils/json.py +0 -48
  93. openhands/sdk/utils/truncate.py +0 -44
  94. openhands/tools/__init__.py +0 -44
  95. openhands/tools/execute_bash/__init__.py +0 -30
  96. openhands/tools/execute_bash/constants.py +0 -31
  97. openhands/tools/execute_bash/definition.py +0 -166
  98. openhands/tools/execute_bash/impl.py +0 -38
  99. openhands/tools/execute_bash/metadata.py +0 -101
  100. openhands/tools/execute_bash/terminal/__init__.py +0 -22
  101. openhands/tools/execute_bash/terminal/factory.py +0 -113
  102. openhands/tools/execute_bash/terminal/interface.py +0 -189
  103. openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
  104. openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
  105. openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
  106. openhands/tools/execute_bash/utils/command.py +0 -150
  107. openhands/tools/str_replace_editor/__init__.py +0 -17
  108. openhands/tools/str_replace_editor/definition.py +0 -158
  109. openhands/tools/str_replace_editor/editor.py +0 -683
  110. openhands/tools/str_replace_editor/exceptions.py +0 -41
  111. openhands/tools/str_replace_editor/impl.py +0 -66
  112. openhands/tools/str_replace_editor/utils/__init__.py +0 -0
  113. openhands/tools/str_replace_editor/utils/config.py +0 -2
  114. openhands/tools/str_replace_editor/utils/constants.py +0 -9
  115. openhands/tools/str_replace_editor/utils/encoding.py +0 -135
  116. openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
  117. openhands/tools/str_replace_editor/utils/history.py +0 -122
  118. openhands/tools/str_replace_editor/utils/shell.py +0 -72
  119. openhands/tools/task_tracker/__init__.py +0 -16
  120. openhands/tools/task_tracker/definition.py +0 -336
  121. openhands/tools/utils/__init__.py +0 -1
  122. openhands-0.0.0.dist-info/METADATA +0 -3
  123. openhands-0.0.0.dist-info/RECORD +0 -94
  124. 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
@@ -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
- )
@@ -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
- """
@@ -1,5 +0,0 @@
1
- from typing import Literal
2
-
3
-
4
- SECURITY_RISK_DESC = "The LLM's assessment of the safety risk of this action."
5
- SECURITY_RISK_LITERAL = Literal["LOW", "MEDIUM", "HIGH", "UNKNOWN"]