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
openhands/sdk/tool/tool.py
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
from typing import Any, Generic, TypeVar
|
|
2
|
-
|
|
3
|
-
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
5
|
-
|
|
6
|
-
from openhands.sdk.tool.schema import ActionBase, ObservationBase
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ActionT = TypeVar("ActionT", bound=ActionBase)
|
|
10
|
-
ObservationT = TypeVar("ObservationT", bound=ObservationBase)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class ToolAnnotations(BaseModel):
|
|
14
|
-
"""Annotations to provide hints about the tool's behavior.
|
|
15
|
-
|
|
16
|
-
Based on Model Context Protocol (MCP) spec:
|
|
17
|
-
https://github.com/modelcontextprotocol/modelcontextprotocol/blob/caf3424488b10b4a7b1f8cb634244a450a1f4400/schema/2025-06-18/schema.ts#L838
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
title: str | None = Field(
|
|
21
|
-
default=None, description="A human-readable title for the tool."
|
|
22
|
-
)
|
|
23
|
-
readOnlyHint: bool = Field(
|
|
24
|
-
default=False,
|
|
25
|
-
description="If true, the tool does not modify its environment. Default: false",
|
|
26
|
-
)
|
|
27
|
-
destructiveHint: bool = Field(
|
|
28
|
-
default=True,
|
|
29
|
-
description="If true, the tool may perform destructive updates to its environment. If false, the tool performs only additive updates. (This property is meaningful only when `readOnlyHint == false`) Default: true", # noqa: E501
|
|
30
|
-
)
|
|
31
|
-
idempotentHint: bool = Field(
|
|
32
|
-
default=False,
|
|
33
|
-
description="If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment. (This property is meaningful only when `readOnlyHint == false`) Default: false", # noqa: E501
|
|
34
|
-
)
|
|
35
|
-
openWorldHint: bool = Field(
|
|
36
|
-
default=True,
|
|
37
|
-
description="If true, this tool may interact with an 'open world' of external entities. If false, the tool's domain of interaction is closed. For example, the world of a web search tool is open, whereas that of a memory tool is not. Default: true", # noqa: E501
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class ToolExecutor(Generic[ActionT, ObservationT]):
|
|
42
|
-
"""Executor function type for a Tool."""
|
|
43
|
-
|
|
44
|
-
def __call__(self, action: ActionT) -> ObservationT:
|
|
45
|
-
raise NotImplementedError
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class Tool(Generic[ActionT, ObservationT]):
|
|
49
|
-
"""Tool that wraps an executor function with input/output validation and schema.
|
|
50
|
-
|
|
51
|
-
- Normalize input/output schemas (class or dict) into both model+schema.
|
|
52
|
-
- Validate inputs before execute.
|
|
53
|
-
- Coerce outputs only if an output model is defined; else return vanilla JSON.
|
|
54
|
-
- Export MCP tool description.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
def __init__(
|
|
58
|
-
self,
|
|
59
|
-
*,
|
|
60
|
-
name: str,
|
|
61
|
-
description: str,
|
|
62
|
-
input_schema: type[ActionBase],
|
|
63
|
-
output_schema: type[ObservationBase] | None = None,
|
|
64
|
-
title: str | None = None,
|
|
65
|
-
annotations: ToolAnnotations | None = None,
|
|
66
|
-
_meta: dict[str, Any] | None = None,
|
|
67
|
-
executor: ToolExecutor | None = None,
|
|
68
|
-
):
|
|
69
|
-
self.name = name
|
|
70
|
-
self.description = description
|
|
71
|
-
self.annotations = annotations
|
|
72
|
-
self._meta = _meta
|
|
73
|
-
self.title = title or name
|
|
74
|
-
|
|
75
|
-
# Schemas
|
|
76
|
-
self.action_type: type[ActionBase] = input_schema
|
|
77
|
-
self.input_schema: dict[str, Any] = input_schema.to_mcp_schema()
|
|
78
|
-
self.observation_type: type[ObservationBase] | None = output_schema
|
|
79
|
-
self.output_schema: dict[str, Any] | None = (
|
|
80
|
-
output_schema.to_mcp_schema() if output_schema else None
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
self.executor = executor
|
|
84
|
-
|
|
85
|
-
def set_executor(self, executor: ToolExecutor) -> "Tool":
|
|
86
|
-
"""Set or replace the executor function."""
|
|
87
|
-
self.executor = executor
|
|
88
|
-
return self
|
|
89
|
-
|
|
90
|
-
def call(self, action: ActionT) -> ObservationBase:
|
|
91
|
-
"""Validate input, execute, and coerce output.
|
|
92
|
-
|
|
93
|
-
We always return some ObservationBase subclass, but not always the
|
|
94
|
-
generic ObservationT.
|
|
95
|
-
"""
|
|
96
|
-
if self.executor is None:
|
|
97
|
-
raise NotImplementedError(f"Tool '{self.name}' has no executor")
|
|
98
|
-
|
|
99
|
-
# Execute
|
|
100
|
-
result = self.executor(action)
|
|
101
|
-
|
|
102
|
-
# Coerce output only if we declared a model; else wrap in base ObservationBase
|
|
103
|
-
if self.observation_type:
|
|
104
|
-
if isinstance(result, self.observation_type):
|
|
105
|
-
return result
|
|
106
|
-
return self.observation_type.model_validate(result)
|
|
107
|
-
else:
|
|
108
|
-
# When no output schema is defined, wrap the result in ObservationBase
|
|
109
|
-
if isinstance(result, ObservationBase):
|
|
110
|
-
return result
|
|
111
|
-
elif isinstance(result, BaseModel):
|
|
112
|
-
return ObservationBase.model_validate(result.model_dump())
|
|
113
|
-
elif isinstance(result, dict):
|
|
114
|
-
return ObservationBase.model_validate(result)
|
|
115
|
-
raise TypeError(
|
|
116
|
-
"Output must be dict or BaseModel when no output schema is defined"
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
def to_mcp_tool(self) -> dict[str, Any]:
|
|
120
|
-
out = {
|
|
121
|
-
"name": self.name,
|
|
122
|
-
"description": self.description,
|
|
123
|
-
"inputSchema": self.input_schema,
|
|
124
|
-
}
|
|
125
|
-
if self.annotations:
|
|
126
|
-
out["annotations"] = self.annotations
|
|
127
|
-
if self._meta is not None:
|
|
128
|
-
out["_meta"] = self._meta
|
|
129
|
-
if self.output_schema:
|
|
130
|
-
out["outputSchema"] = self.output_schema
|
|
131
|
-
return out
|
|
132
|
-
|
|
133
|
-
def to_openai_tool(self) -> ChatCompletionToolParam:
|
|
134
|
-
"""Convert an MCP tool to an OpenAI tool."""
|
|
135
|
-
return ChatCompletionToolParam(
|
|
136
|
-
type="function",
|
|
137
|
-
function=ChatCompletionToolParamFunctionChunk(
|
|
138
|
-
name=self.name,
|
|
139
|
-
description=self.description,
|
|
140
|
-
parameters=self.input_schema,
|
|
141
|
-
),
|
|
142
|
-
)
|
openhands/sdk/utils/__init__.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""Utility functions for the OpenHands SDK."""
|
|
2
|
-
|
|
3
|
-
from .truncate import (
|
|
4
|
-
DEFAULT_TEXT_CONTENT_LIMIT,
|
|
5
|
-
DEFAULT_TRUNCATE_NOTICE,
|
|
6
|
-
maybe_truncate,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
__all__ = [
|
|
11
|
-
"DEFAULT_TEXT_CONTENT_LIMIT",
|
|
12
|
-
"DEFAULT_TRUNCATE_NOTICE",
|
|
13
|
-
"maybe_truncate",
|
|
14
|
-
]
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"""Utility for creating and managing discriminated unions of Pydantic models.
|
|
2
|
-
|
|
3
|
-
Pydantic provides native support for disciriminated unions via the `Union` type and
|
|
4
|
-
the `discriminator` argument to `Field`. However, this requires that all possible
|
|
5
|
-
types in the union be known at the time the field is defined. This can be limiting
|
|
6
|
-
in scenarios where new types may be defined later.
|
|
7
|
-
|
|
8
|
-
To address this, we provide a `DiscriminatedUnionMixin` base class that models can
|
|
9
|
-
inherit from to automatically register themselves as part of a discriminated union.
|
|
10
|
-
We also provide a `DiscriminatedUnionType` type wrapper that can be used in Pydantic
|
|
11
|
-
models to indicate that a field should be treated as a discriminated union of all
|
|
12
|
-
subclasses of a given base class.
|
|
13
|
-
|
|
14
|
-
Importantly, this approach allows us to _delay_ the resolution of the union types
|
|
15
|
-
until validation time, meaning that new subclasses can be defined and registered
|
|
16
|
-
at any time before validation occurs.
|
|
17
|
-
|
|
18
|
-
Example usage:
|
|
19
|
-
|
|
20
|
-
from typing import Annotated
|
|
21
|
-
from pydantic import BaseModel
|
|
22
|
-
|
|
23
|
-
from openhands.sdk.utils.discriminated_union import (
|
|
24
|
-
DiscriminatedUnionMixin,
|
|
25
|
-
DiscriminatedUnionType
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
# The base class for the union is tagged with DiscriminatedUnionMixin
|
|
29
|
-
class Animal(BaseModel, DiscriminatedUnionMixin):
|
|
30
|
-
name: str
|
|
31
|
-
|
|
32
|
-
# We develop a special type to represent the discriminated union of all Animals.
|
|
33
|
-
# This acts just like Animal, but the annotation tells Pydantic to treat it as a
|
|
34
|
-
# discriminated union of all subclasses of Animal (defined so far or in the future).
|
|
35
|
-
AnyAnimal = Annotated[Animal, DiscriminatedUnionType[Animal]]
|
|
36
|
-
|
|
37
|
-
class Dog(Animal):
|
|
38
|
-
breed: str
|
|
39
|
-
|
|
40
|
-
class Cat(Animal):
|
|
41
|
-
color: str
|
|
42
|
-
|
|
43
|
-
class Zoo(BaseModel):
|
|
44
|
-
residents: list[AnyAnimal]
|
|
45
|
-
|
|
46
|
-
# Even animals defined after the Zoo class can be included without issue
|
|
47
|
-
class Mouse(Animal):
|
|
48
|
-
size: str
|
|
49
|
-
|
|
50
|
-
zoo = Zoo(residents=[
|
|
51
|
-
Dog(name="Fido", breed="Labrador"),
|
|
52
|
-
Cat(name="Whiskers", color="Tabby"),
|
|
53
|
-
Mouse(name="Jerry", size="Small")
|
|
54
|
-
])
|
|
55
|
-
|
|
56
|
-
serialized_zoo = zoo.model_dump_json()
|
|
57
|
-
deserialized_zoo = Zoo.model_validate_json(serialized_zoo)
|
|
58
|
-
|
|
59
|
-
assert zoo == deserialized_zoo
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
from __future__ import annotations
|
|
63
|
-
|
|
64
|
-
from typing import Any, Generic, TypeVar, cast
|
|
65
|
-
|
|
66
|
-
from pydantic import BaseModel, computed_field
|
|
67
|
-
from pydantic_core import core_schema
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
T = TypeVar("T", bound="DiscriminatedUnionMixin")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class DiscriminatedUnionMixin(BaseModel):
|
|
74
|
-
"""A Base class for members of tagged unions discriminated by the class name.
|
|
75
|
-
|
|
76
|
-
This class provides automatic subclass registration and discriminated union
|
|
77
|
-
functionality. Each subclass is automatically registered when defined and
|
|
78
|
-
can be used for polymorphic serialization/deserialization.
|
|
79
|
-
|
|
80
|
-
Child classes will automatically have a type field defined, which is used as a
|
|
81
|
-
discriminator for union types.
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
@computed_field # type: ignore
|
|
85
|
-
@property
|
|
86
|
-
def kind(self) -> str:
|
|
87
|
-
"""Property to create kind field from class name when serializing."""
|
|
88
|
-
return f"{self.__class__.__module__}.{self.__class__.__qualname__}"
|
|
89
|
-
|
|
90
|
-
@classmethod
|
|
91
|
-
def target_subclass(cls, kind: str) -> type[DiscriminatedUnionMixin] | None:
|
|
92
|
-
"""Get the subclass corresponding to a given kind name."""
|
|
93
|
-
worklist = [cls]
|
|
94
|
-
while worklist:
|
|
95
|
-
current = worklist.pop()
|
|
96
|
-
if f"{current.__module__}.{current.__qualname__}" == kind:
|
|
97
|
-
return current
|
|
98
|
-
worklist.extend(current.__subclasses__())
|
|
99
|
-
return None
|
|
100
|
-
|
|
101
|
-
@classmethod
|
|
102
|
-
def model_validate(
|
|
103
|
-
cls: type[T],
|
|
104
|
-
obj: Any,
|
|
105
|
-
*,
|
|
106
|
-
strict=None,
|
|
107
|
-
from_attributes=None,
|
|
108
|
-
context=None,
|
|
109
|
-
**kwargs,
|
|
110
|
-
) -> T:
|
|
111
|
-
"""Custom model validation using registered subclasses for deserialization."""
|
|
112
|
-
# If we have a 'kind' field but no discriminated union handling,
|
|
113
|
-
# we still need to remove it to avoid extra field errors
|
|
114
|
-
if isinstance(obj, dict) and "kind" in obj:
|
|
115
|
-
kind = obj.get("kind")
|
|
116
|
-
assert isinstance(kind, str)
|
|
117
|
-
obj_without_kind = {k: v for k, v in obj.items() if k != "kind"}
|
|
118
|
-
if (target_class := cls.target_subclass(kind)) is not None:
|
|
119
|
-
return cast(
|
|
120
|
-
T,
|
|
121
|
-
target_class.model_validate(
|
|
122
|
-
obj_without_kind,
|
|
123
|
-
strict=strict,
|
|
124
|
-
from_attributes=from_attributes,
|
|
125
|
-
context=context,
|
|
126
|
-
**kwargs,
|
|
127
|
-
),
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
# Fallback to default validation
|
|
131
|
-
return cast(
|
|
132
|
-
T,
|
|
133
|
-
super().model_validate(
|
|
134
|
-
obj,
|
|
135
|
-
strict=strict,
|
|
136
|
-
from_attributes=from_attributes,
|
|
137
|
-
context=context,
|
|
138
|
-
**kwargs,
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
@classmethod
|
|
143
|
-
def model_validate_json(
|
|
144
|
-
cls: type[T],
|
|
145
|
-
json_data: str | bytes | bytearray,
|
|
146
|
-
*,
|
|
147
|
-
strict=None,
|
|
148
|
-
context=None,
|
|
149
|
-
**kwargs,
|
|
150
|
-
) -> T:
|
|
151
|
-
"""Custom JSON validation that uses our custom model_validate method."""
|
|
152
|
-
import json
|
|
153
|
-
|
|
154
|
-
# Parse JSON to dict first
|
|
155
|
-
if isinstance(json_data, bytes):
|
|
156
|
-
json_data = json_data.decode("utf-8")
|
|
157
|
-
|
|
158
|
-
obj = json.loads(json_data)
|
|
159
|
-
|
|
160
|
-
# Use our custom model_validate method
|
|
161
|
-
return cls.model_validate(
|
|
162
|
-
obj,
|
|
163
|
-
strict=strict,
|
|
164
|
-
context=context,
|
|
165
|
-
**kwargs,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
class DiscriminatedUnionType(Generic[T]):
|
|
170
|
-
"""A type wrapper that enables discriminated union validation for Pydantic fields.
|
|
171
|
-
|
|
172
|
-
The wrapped type must be a subclass of `DiscriminatedUnionMixin`.
|
|
173
|
-
"""
|
|
174
|
-
|
|
175
|
-
def __init__(self, cls: type[T]):
|
|
176
|
-
self.base_class = cls
|
|
177
|
-
self.__origin__ = cls
|
|
178
|
-
self.__args__ = ()
|
|
179
|
-
|
|
180
|
-
def __get_pydantic_core_schema__(self, source_type, handler):
|
|
181
|
-
"""Define custom Pydantic core schema for this type.
|
|
182
|
-
|
|
183
|
-
This schema uses a custom validator function to handle discriminated union
|
|
184
|
-
deserialization based on the 'kind' field.
|
|
185
|
-
"""
|
|
186
|
-
|
|
187
|
-
# Importantly, this validation function calls the base class model validation
|
|
188
|
-
# _at validation time_, meaning that even if new subclasses are registered after
|
|
189
|
-
# the fact they will still be recognized.
|
|
190
|
-
def validate(v):
|
|
191
|
-
if isinstance(v, self.base_class):
|
|
192
|
-
return v
|
|
193
|
-
if isinstance(v, dict) and "kind" in v:
|
|
194
|
-
return self.base_class.model_validate(v)
|
|
195
|
-
return self.base_class(**v)
|
|
196
|
-
|
|
197
|
-
return core_schema.no_info_plain_validator_function(validate)
|
|
198
|
-
|
|
199
|
-
def __repr__(self):
|
|
200
|
-
return f"DiscriminatedUnion[{self.base_class.__name__}]"
|
|
201
|
-
|
|
202
|
-
def __class_getitem__(cls, params):
|
|
203
|
-
"""Support type-style subscript syntax like DiscriminatedUnionType[cls]."""
|
|
204
|
-
return cls(params)
|
|
205
|
-
|
|
206
|
-
def __instancecheck__(self, instance):
|
|
207
|
-
return isinstance(instance, self.base_class)
|
|
208
|
-
|
|
209
|
-
def __subclasscheck__(self, subclass):
|
|
210
|
-
return issubclass(subclass, self.base_class)
|
openhands/sdk/utils/json.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
from litellm.types.utils import ModelResponse
|
|
6
|
-
|
|
7
|
-
from openhands.sdk.llm.exceptions import LLMResponseError
|
|
8
|
-
from openhands.sdk.llm.utils.metrics import Metrics
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class OpenHandsJSONEncoder(json.JSONEncoder):
|
|
12
|
-
"""Custom JSON encoder that handles datetime and other OH objects"""
|
|
13
|
-
|
|
14
|
-
def default(self, o: object) -> Any:
|
|
15
|
-
if isinstance(o, datetime):
|
|
16
|
-
return o.isoformat()
|
|
17
|
-
if isinstance(o, Metrics):
|
|
18
|
-
return o.get()
|
|
19
|
-
if isinstance(o, ModelResponse):
|
|
20
|
-
return o.model_dump()
|
|
21
|
-
return super().default(o)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Create a single reusable encoder instance
|
|
25
|
-
_json_encoder = OpenHandsJSONEncoder()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def dumps(obj, **kwargs):
|
|
29
|
-
"""Serialize an object to str format"""
|
|
30
|
-
if not kwargs:
|
|
31
|
-
return _json_encoder.encode(obj)
|
|
32
|
-
|
|
33
|
-
# Create a copy of the kwargs to avoid modifying the original
|
|
34
|
-
encoder_kwargs = kwargs.copy()
|
|
35
|
-
|
|
36
|
-
# If cls is specified, use it; otherwise use our custom encoder
|
|
37
|
-
if "cls" not in encoder_kwargs:
|
|
38
|
-
encoder_kwargs["cls"] = OpenHandsJSONEncoder
|
|
39
|
-
|
|
40
|
-
return json.dumps(obj, **encoder_kwargs)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def loads(json_str, **kwargs):
|
|
44
|
-
"""Create a JSON object from str"""
|
|
45
|
-
try:
|
|
46
|
-
return json.loads(json_str, **kwargs)
|
|
47
|
-
except json.JSONDecodeError:
|
|
48
|
-
raise LLMResponseError("No valid JSON object found in response.")
|
openhands/sdk/utils/truncate.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Utility functions for truncating text content."""
|
|
2
|
-
|
|
3
|
-
# Default truncation limits
|
|
4
|
-
DEFAULT_TEXT_CONTENT_LIMIT = 50_000
|
|
5
|
-
|
|
6
|
-
# Default truncation notice
|
|
7
|
-
DEFAULT_TRUNCATE_NOTICE = (
|
|
8
|
-
"<response clipped><NOTE>Due to the max output limit, only part of the full "
|
|
9
|
-
"response has been shown to you.</NOTE>"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def maybe_truncate(
|
|
14
|
-
content: str,
|
|
15
|
-
truncate_after: int | None = None,
|
|
16
|
-
truncate_notice: str = DEFAULT_TRUNCATE_NOTICE,
|
|
17
|
-
) -> str:
|
|
18
|
-
"""
|
|
19
|
-
Truncate the middle of content if it exceeds the specified length.
|
|
20
|
-
|
|
21
|
-
Keeps the head and tail of the content to preserve context at both ends.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
content: The text content to potentially truncate
|
|
25
|
-
truncate_after: Maximum length before truncation. If None, no truncation occurs
|
|
26
|
-
truncate_notice: Notice to insert in the middle when content is truncated
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
Original content if under limit, or truncated content with head and tail
|
|
30
|
-
preserved
|
|
31
|
-
"""
|
|
32
|
-
if not truncate_after or len(content) <= truncate_after or truncate_after < 0:
|
|
33
|
-
return content
|
|
34
|
-
|
|
35
|
-
# Calculate how much space we have for actual content
|
|
36
|
-
available_chars = truncate_after - len(truncate_notice)
|
|
37
|
-
half = available_chars // 2
|
|
38
|
-
|
|
39
|
-
# Give extra character to head if odd number
|
|
40
|
-
head_chars = half + (available_chars % 2)
|
|
41
|
-
tail_chars = half
|
|
42
|
-
|
|
43
|
-
# Keep head and tail, insert notice in the middle
|
|
44
|
-
return content[:head_chars] + truncate_notice + content[-tail_chars:]
|
openhands/tools/__init__.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Runtime tools package."""
|
|
2
|
-
|
|
3
|
-
from openhands.tools.execute_bash import (
|
|
4
|
-
BashExecutor,
|
|
5
|
-
BashTool,
|
|
6
|
-
ExecuteBashAction,
|
|
7
|
-
ExecuteBashObservation,
|
|
8
|
-
execute_bash_tool,
|
|
9
|
-
)
|
|
10
|
-
from openhands.tools.str_replace_editor import (
|
|
11
|
-
FileEditorExecutor,
|
|
12
|
-
FileEditorTool,
|
|
13
|
-
StrReplaceEditorAction,
|
|
14
|
-
StrReplaceEditorObservation,
|
|
15
|
-
str_replace_editor_tool,
|
|
16
|
-
)
|
|
17
|
-
from openhands.tools.task_tracker import (
|
|
18
|
-
TaskTrackerAction,
|
|
19
|
-
TaskTrackerExecutor,
|
|
20
|
-
TaskTrackerObservation,
|
|
21
|
-
TaskTrackerTool,
|
|
22
|
-
task_tracker_tool,
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
__all__ = [
|
|
27
|
-
"execute_bash_tool",
|
|
28
|
-
"ExecuteBashAction",
|
|
29
|
-
"ExecuteBashObservation",
|
|
30
|
-
"BashExecutor",
|
|
31
|
-
"BashTool",
|
|
32
|
-
"str_replace_editor_tool",
|
|
33
|
-
"StrReplaceEditorAction",
|
|
34
|
-
"StrReplaceEditorObservation",
|
|
35
|
-
"FileEditorExecutor",
|
|
36
|
-
"FileEditorTool",
|
|
37
|
-
"task_tracker_tool",
|
|
38
|
-
"TaskTrackerAction",
|
|
39
|
-
"TaskTrackerObservation",
|
|
40
|
-
"TaskTrackerExecutor",
|
|
41
|
-
"TaskTrackerTool",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
__version__ = "1.0.0a0"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Core tool interface
|
|
2
|
-
from openhands.tools.execute_bash.definition import (
|
|
3
|
-
BashTool,
|
|
4
|
-
ExecuteBashAction,
|
|
5
|
-
ExecuteBashObservation,
|
|
6
|
-
execute_bash_tool,
|
|
7
|
-
)
|
|
8
|
-
from openhands.tools.execute_bash.impl import BashExecutor
|
|
9
|
-
|
|
10
|
-
# Terminal session architecture - import from sessions package
|
|
11
|
-
from openhands.tools.execute_bash.terminal import (
|
|
12
|
-
TerminalCommandStatus,
|
|
13
|
-
TerminalSession,
|
|
14
|
-
create_terminal_session,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
__all__ = [
|
|
19
|
-
# === Core Tool Interface ===
|
|
20
|
-
"BashTool",
|
|
21
|
-
"execute_bash_tool",
|
|
22
|
-
"ExecuteBashAction",
|
|
23
|
-
"ExecuteBashObservation",
|
|
24
|
-
"BashExecutor",
|
|
25
|
-
# === Terminal Session Architecture ===
|
|
26
|
-
"TerminalSession",
|
|
27
|
-
"TerminalCommandStatus",
|
|
28
|
-
"TerminalSession",
|
|
29
|
-
"create_terminal_session",
|
|
30
|
-
]
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
CMD_OUTPUT_PS1_BEGIN = "\n###PS1JSON###\n"
|
|
5
|
-
CMD_OUTPUT_PS1_END = "\n###PS1END###"
|
|
6
|
-
CMD_OUTPUT_METADATA_PS1_REGEX = re.compile(
|
|
7
|
-
f"^{CMD_OUTPUT_PS1_BEGIN.strip()}(.*?){CMD_OUTPUT_PS1_END.strip()}",
|
|
8
|
-
re.DOTALL | re.MULTILINE,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
# Default max size for command output content
|
|
12
|
-
# to prevent too large observations from being saved in the stream
|
|
13
|
-
# This matches the default max_message_chars in LLM class
|
|
14
|
-
MAX_CMD_OUTPUT_SIZE: int = 30000
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Common timeout message that can be used across different timeout scenarios
|
|
18
|
-
TIMEOUT_MESSAGE_TEMPLATE = (
|
|
19
|
-
"You may wait longer to see additional output by sending empty command '', "
|
|
20
|
-
"send other commands to interact with the current process, send keys "
|
|
21
|
-
'("C-c", "C-z", "C-d") '
|
|
22
|
-
"to interrupt/kill the previous command before sending your new command, "
|
|
23
|
-
"or use the timeout parameter in execute_bash for future commands."
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
# How long to wait with no new output before considering it a no-change timeout
|
|
27
|
-
NO_CHANGE_TIMEOUT_SECONDS = 30
|
|
28
|
-
|
|
29
|
-
# How often to poll for new output in seconds
|
|
30
|
-
POLL_INTERVAL = 0.5
|
|
31
|
-
HISTORY_LIMIT = 10_000
|