openhands-sdk 1.7.3__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.
- openhands/sdk/__init__.py +111 -0
- openhands/sdk/agent/__init__.py +8 -0
- openhands/sdk/agent/agent.py +650 -0
- openhands/sdk/agent/base.py +457 -0
- openhands/sdk/agent/prompts/in_context_learning_example.j2 +169 -0
- openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +3 -0
- openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
- openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
- openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +2 -0
- openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
- openhands/sdk/agent/prompts/security_policy.j2 +22 -0
- openhands/sdk/agent/prompts/security_risk_assessment.j2 +21 -0
- openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
- openhands/sdk/agent/prompts/system_prompt.j2 +132 -0
- openhands/sdk/agent/prompts/system_prompt_interactive.j2 +14 -0
- openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +40 -0
- openhands/sdk/agent/prompts/system_prompt_planning.j2 +40 -0
- openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +122 -0
- openhands/sdk/agent/utils.py +228 -0
- openhands/sdk/context/__init__.py +28 -0
- openhands/sdk/context/agent_context.py +264 -0
- openhands/sdk/context/condenser/__init__.py +18 -0
- openhands/sdk/context/condenser/base.py +100 -0
- openhands/sdk/context/condenser/llm_summarizing_condenser.py +248 -0
- openhands/sdk/context/condenser/no_op_condenser.py +14 -0
- openhands/sdk/context/condenser/pipeline_condenser.py +56 -0
- openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +59 -0
- openhands/sdk/context/condenser/utils.py +149 -0
- openhands/sdk/context/prompts/__init__.py +6 -0
- openhands/sdk/context/prompts/prompt.py +114 -0
- openhands/sdk/context/prompts/templates/ask_agent_template.j2 +11 -0
- openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +8 -0
- openhands/sdk/context/prompts/templates/system_message_suffix.j2 +32 -0
- openhands/sdk/context/skills/__init__.py +28 -0
- openhands/sdk/context/skills/exceptions.py +11 -0
- openhands/sdk/context/skills/skill.py +720 -0
- openhands/sdk/context/skills/trigger.py +36 -0
- openhands/sdk/context/skills/types.py +48 -0
- openhands/sdk/context/view.py +503 -0
- openhands/sdk/conversation/__init__.py +40 -0
- openhands/sdk/conversation/base.py +281 -0
- openhands/sdk/conversation/conversation.py +152 -0
- openhands/sdk/conversation/conversation_stats.py +85 -0
- openhands/sdk/conversation/event_store.py +157 -0
- openhands/sdk/conversation/events_list_base.py +17 -0
- openhands/sdk/conversation/exceptions.py +50 -0
- openhands/sdk/conversation/fifo_lock.py +133 -0
- openhands/sdk/conversation/impl/__init__.py +5 -0
- openhands/sdk/conversation/impl/local_conversation.py +665 -0
- openhands/sdk/conversation/impl/remote_conversation.py +956 -0
- openhands/sdk/conversation/persistence_const.py +9 -0
- openhands/sdk/conversation/response_utils.py +41 -0
- openhands/sdk/conversation/secret_registry.py +126 -0
- openhands/sdk/conversation/serialization_diff.py +0 -0
- openhands/sdk/conversation/state.py +392 -0
- openhands/sdk/conversation/stuck_detector.py +311 -0
- openhands/sdk/conversation/title_utils.py +191 -0
- openhands/sdk/conversation/types.py +45 -0
- openhands/sdk/conversation/visualizer/__init__.py +12 -0
- openhands/sdk/conversation/visualizer/base.py +67 -0
- openhands/sdk/conversation/visualizer/default.py +373 -0
- openhands/sdk/critic/__init__.py +15 -0
- openhands/sdk/critic/base.py +38 -0
- openhands/sdk/critic/impl/__init__.py +12 -0
- openhands/sdk/critic/impl/agent_finished.py +83 -0
- openhands/sdk/critic/impl/empty_patch.py +49 -0
- openhands/sdk/critic/impl/pass_critic.py +42 -0
- openhands/sdk/event/__init__.py +42 -0
- openhands/sdk/event/base.py +149 -0
- openhands/sdk/event/condenser.py +82 -0
- openhands/sdk/event/conversation_error.py +25 -0
- openhands/sdk/event/conversation_state.py +104 -0
- openhands/sdk/event/llm_completion_log.py +39 -0
- openhands/sdk/event/llm_convertible/__init__.py +20 -0
- openhands/sdk/event/llm_convertible/action.py +139 -0
- openhands/sdk/event/llm_convertible/message.py +142 -0
- openhands/sdk/event/llm_convertible/observation.py +141 -0
- openhands/sdk/event/llm_convertible/system.py +61 -0
- openhands/sdk/event/token.py +16 -0
- openhands/sdk/event/types.py +11 -0
- openhands/sdk/event/user_action.py +21 -0
- openhands/sdk/git/exceptions.py +43 -0
- openhands/sdk/git/git_changes.py +249 -0
- openhands/sdk/git/git_diff.py +129 -0
- openhands/sdk/git/models.py +21 -0
- openhands/sdk/git/utils.py +189 -0
- openhands/sdk/hooks/__init__.py +30 -0
- openhands/sdk/hooks/config.py +180 -0
- openhands/sdk/hooks/conversation_hooks.py +227 -0
- openhands/sdk/hooks/executor.py +155 -0
- openhands/sdk/hooks/manager.py +170 -0
- openhands/sdk/hooks/types.py +40 -0
- openhands/sdk/io/__init__.py +6 -0
- openhands/sdk/io/base.py +48 -0
- openhands/sdk/io/cache.py +85 -0
- openhands/sdk/io/local.py +119 -0
- openhands/sdk/io/memory.py +54 -0
- openhands/sdk/llm/__init__.py +45 -0
- openhands/sdk/llm/exceptions/__init__.py +45 -0
- openhands/sdk/llm/exceptions/classifier.py +50 -0
- openhands/sdk/llm/exceptions/mapping.py +54 -0
- openhands/sdk/llm/exceptions/types.py +101 -0
- openhands/sdk/llm/llm.py +1140 -0
- openhands/sdk/llm/llm_registry.py +122 -0
- openhands/sdk/llm/llm_response.py +59 -0
- openhands/sdk/llm/message.py +656 -0
- openhands/sdk/llm/mixins/fn_call_converter.py +1288 -0
- openhands/sdk/llm/mixins/non_native_fc.py +97 -0
- openhands/sdk/llm/options/__init__.py +1 -0
- openhands/sdk/llm/options/chat_options.py +93 -0
- openhands/sdk/llm/options/common.py +19 -0
- openhands/sdk/llm/options/responses_options.py +67 -0
- openhands/sdk/llm/router/__init__.py +10 -0
- openhands/sdk/llm/router/base.py +117 -0
- openhands/sdk/llm/router/impl/multimodal.py +76 -0
- openhands/sdk/llm/router/impl/random.py +22 -0
- openhands/sdk/llm/streaming.py +9 -0
- openhands/sdk/llm/utils/metrics.py +312 -0
- openhands/sdk/llm/utils/model_features.py +192 -0
- openhands/sdk/llm/utils/model_info.py +90 -0
- openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
- openhands/sdk/llm/utils/retry_mixin.py +128 -0
- openhands/sdk/llm/utils/telemetry.py +362 -0
- openhands/sdk/llm/utils/unverified_models.py +156 -0
- openhands/sdk/llm/utils/verified_models.py +65 -0
- openhands/sdk/logger/__init__.py +22 -0
- openhands/sdk/logger/logger.py +195 -0
- openhands/sdk/logger/rolling.py +113 -0
- openhands/sdk/mcp/__init__.py +24 -0
- openhands/sdk/mcp/client.py +76 -0
- openhands/sdk/mcp/definition.py +106 -0
- openhands/sdk/mcp/exceptions.py +19 -0
- openhands/sdk/mcp/tool.py +270 -0
- openhands/sdk/mcp/utils.py +83 -0
- openhands/sdk/observability/__init__.py +4 -0
- openhands/sdk/observability/laminar.py +166 -0
- openhands/sdk/observability/utils.py +20 -0
- openhands/sdk/py.typed +0 -0
- openhands/sdk/secret/__init__.py +19 -0
- openhands/sdk/secret/secrets.py +92 -0
- openhands/sdk/security/__init__.py +6 -0
- openhands/sdk/security/analyzer.py +111 -0
- openhands/sdk/security/confirmation_policy.py +61 -0
- openhands/sdk/security/llm_analyzer.py +29 -0
- openhands/sdk/security/risk.py +100 -0
- openhands/sdk/tool/__init__.py +34 -0
- openhands/sdk/tool/builtins/__init__.py +34 -0
- openhands/sdk/tool/builtins/finish.py +106 -0
- openhands/sdk/tool/builtins/think.py +117 -0
- openhands/sdk/tool/registry.py +184 -0
- openhands/sdk/tool/schema.py +286 -0
- openhands/sdk/tool/spec.py +39 -0
- openhands/sdk/tool/tool.py +481 -0
- openhands/sdk/utils/__init__.py +22 -0
- openhands/sdk/utils/async_executor.py +115 -0
- openhands/sdk/utils/async_utils.py +39 -0
- openhands/sdk/utils/cipher.py +68 -0
- openhands/sdk/utils/command.py +90 -0
- openhands/sdk/utils/deprecation.py +166 -0
- openhands/sdk/utils/github.py +44 -0
- openhands/sdk/utils/json.py +48 -0
- openhands/sdk/utils/models.py +570 -0
- openhands/sdk/utils/paging.py +63 -0
- openhands/sdk/utils/pydantic_diff.py +85 -0
- openhands/sdk/utils/pydantic_secrets.py +64 -0
- openhands/sdk/utils/truncate.py +117 -0
- openhands/sdk/utils/visualize.py +58 -0
- openhands/sdk/workspace/__init__.py +17 -0
- openhands/sdk/workspace/base.py +158 -0
- openhands/sdk/workspace/local.py +189 -0
- openhands/sdk/workspace/models.py +35 -0
- openhands/sdk/workspace/remote/__init__.py +8 -0
- openhands/sdk/workspace/remote/async_remote_workspace.py +149 -0
- openhands/sdk/workspace/remote/base.py +164 -0
- openhands/sdk/workspace/remote/remote_workspace_mixin.py +323 -0
- openhands/sdk/workspace/workspace.py +49 -0
- openhands_sdk-1.7.3.dist-info/METADATA +17 -0
- openhands_sdk-1.7.3.dist-info/RECORD +180 -0
- openhands_sdk-1.7.3.dist-info/WHEEL +5 -0
- openhands_sdk-1.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cipher utility for preventing accidental secret disclosure in serialized data
|
|
3
|
+
|
|
4
|
+
SECURITY WARNINGS:
|
|
5
|
+
- The secret key is a string for ease of use but should contain at least 256
|
|
6
|
+
bits of entropy
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
from base64 import b64encode
|
|
11
|
+
|
|
12
|
+
from cryptography.fernet import Fernet
|
|
13
|
+
from pydantic import SecretStr
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Cipher:
|
|
17
|
+
"""
|
|
18
|
+
Simple encryption utility for preventing accidental secret disclosure.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, secret_key: str):
|
|
22
|
+
self.secret_key = secret_key
|
|
23
|
+
self._fernet: Fernet | None = None
|
|
24
|
+
|
|
25
|
+
def encrypt(self, secret: SecretStr | None) -> str | None:
|
|
26
|
+
if secret is None:
|
|
27
|
+
return None
|
|
28
|
+
secret_value = secret.get_secret_value().encode()
|
|
29
|
+
fernet = self._get_fernet()
|
|
30
|
+
result = fernet.encrypt(secret_value).decode()
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
def decrypt(self, secret: str | None) -> SecretStr | None:
|
|
34
|
+
"""
|
|
35
|
+
Decrypt a secret value, returning None if decryption fails.
|
|
36
|
+
|
|
37
|
+
This handles cases where existing conversations were serialized with different
|
|
38
|
+
encryption keys or contain invalid encrypted data. A warning is logged when
|
|
39
|
+
decryption fails and a None is returned. This mimics the case where
|
|
40
|
+
no cipher was defined so secrets where redacted.
|
|
41
|
+
"""
|
|
42
|
+
if secret is None:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
fernet = self._get_fernet()
|
|
46
|
+
decrypted = fernet.decrypt(secret.encode()).decode()
|
|
47
|
+
return SecretStr(decrypted)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
# Import here to avoid circular imports
|
|
50
|
+
from openhands.sdk.logger import get_logger
|
|
51
|
+
|
|
52
|
+
logger = get_logger(__name__)
|
|
53
|
+
logger.warning(
|
|
54
|
+
f"Failed to decrypt secret value (setting to None): {e}. "
|
|
55
|
+
"This may occur when loading conversations encrypted with a different "
|
|
56
|
+
"key or when upgrading from older versions."
|
|
57
|
+
)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def _get_fernet(self):
|
|
61
|
+
fernet = self._fernet
|
|
62
|
+
if fernet is None:
|
|
63
|
+
secret_key = self.secret_key.encode()
|
|
64
|
+
# Hash the key to make sure we have a 256 bit value
|
|
65
|
+
fernet_key = b64encode(hashlib.sha256(secret_key).digest())
|
|
66
|
+
fernet = Fernet(fernet_key)
|
|
67
|
+
object.__setattr__(self, "_fernet", fernet)
|
|
68
|
+
return fernet
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
from openhands.sdk.logger import get_logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def execute_command(
|
|
13
|
+
cmd: list[str] | str,
|
|
14
|
+
env: dict[str, str] | None = None,
|
|
15
|
+
cwd: str | None = None,
|
|
16
|
+
timeout: float | None = None,
|
|
17
|
+
print_output: bool = True,
|
|
18
|
+
) -> subprocess.CompletedProcess:
|
|
19
|
+
# For string commands, use shell=True to handle shell operators properly
|
|
20
|
+
if isinstance(cmd, str):
|
|
21
|
+
cmd_to_run = cmd
|
|
22
|
+
use_shell = True
|
|
23
|
+
logger.info("$ %s", cmd)
|
|
24
|
+
else:
|
|
25
|
+
cmd_to_run = cmd
|
|
26
|
+
use_shell = False
|
|
27
|
+
logger.info("$ %s", " ".join(shlex.quote(c) for c in cmd))
|
|
28
|
+
|
|
29
|
+
proc = subprocess.Popen(
|
|
30
|
+
cmd_to_run,
|
|
31
|
+
cwd=cwd,
|
|
32
|
+
env=env,
|
|
33
|
+
stdout=subprocess.PIPE,
|
|
34
|
+
stderr=subprocess.PIPE,
|
|
35
|
+
text=True,
|
|
36
|
+
bufsize=1,
|
|
37
|
+
shell=use_shell,
|
|
38
|
+
)
|
|
39
|
+
if proc is None:
|
|
40
|
+
raise RuntimeError("Failed to start process")
|
|
41
|
+
|
|
42
|
+
# Read line by line, echo to parent stdout/stderr
|
|
43
|
+
stdout_lines: list[str] = []
|
|
44
|
+
stderr_lines: list[str] = []
|
|
45
|
+
if proc.stdout is None or proc.stderr is None:
|
|
46
|
+
raise RuntimeError("Failed to capture stdout/stderr")
|
|
47
|
+
|
|
48
|
+
def read_stream(stream, lines, output_stream):
|
|
49
|
+
try:
|
|
50
|
+
for line in stream:
|
|
51
|
+
if print_output:
|
|
52
|
+
output_stream.write(line)
|
|
53
|
+
output_stream.flush()
|
|
54
|
+
lines.append(line)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to read stream: {e}")
|
|
57
|
+
|
|
58
|
+
# Read stdout and stderr concurrently to avoid deadlock
|
|
59
|
+
stdout_thread = threading.Thread(
|
|
60
|
+
target=read_stream, args=(proc.stdout, stdout_lines, sys.stdout)
|
|
61
|
+
)
|
|
62
|
+
stderr_thread = threading.Thread(
|
|
63
|
+
target=read_stream, args=(proc.stderr, stderr_lines, sys.stderr)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
stdout_thread.start()
|
|
67
|
+
stderr_thread.start()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
proc.wait(timeout=timeout)
|
|
71
|
+
except subprocess.TimeoutExpired:
|
|
72
|
+
proc.kill()
|
|
73
|
+
stdout_thread.join()
|
|
74
|
+
stderr_thread.join()
|
|
75
|
+
return subprocess.CompletedProcess(
|
|
76
|
+
cmd_to_run,
|
|
77
|
+
-1, # Indicate timeout with -1 exit code
|
|
78
|
+
"".join(stdout_lines),
|
|
79
|
+
"".join(stderr_lines),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
stdout_thread.join(timeout=timeout)
|
|
83
|
+
stderr_thread.join(timeout=timeout)
|
|
84
|
+
|
|
85
|
+
return subprocess.CompletedProcess(
|
|
86
|
+
cmd_to_run,
|
|
87
|
+
proc.returncode,
|
|
88
|
+
"".join(stdout_lines),
|
|
89
|
+
"".join(stderr_lines),
|
|
90
|
+
)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from datetime import date
|
|
6
|
+
from functools import cache
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version as get_version
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from deprecation import (
|
|
11
|
+
DeprecatedWarning,
|
|
12
|
+
UnsupportedWarning,
|
|
13
|
+
deprecated as _deprecated,
|
|
14
|
+
)
|
|
15
|
+
from packaging import version as pkg_version
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@cache
|
|
22
|
+
def _current_version() -> str:
|
|
23
|
+
try:
|
|
24
|
+
return get_version("openhands-sdk")
|
|
25
|
+
except PackageNotFoundError:
|
|
26
|
+
return "0.0.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def deprecated(
|
|
30
|
+
*,
|
|
31
|
+
deprecated_in: str,
|
|
32
|
+
removed_in: str | date | None,
|
|
33
|
+
current_version: str | None = None,
|
|
34
|
+
details: str = "",
|
|
35
|
+
) -> Callable[[_FuncT], _FuncT]:
|
|
36
|
+
"""Return a decorator that deprecates a callable with explicit metadata.
|
|
37
|
+
|
|
38
|
+
Use this helper when you can annotate a function, method, or property with
|
|
39
|
+
`@deprecated(...)`. It transparently forwards to :func:`deprecation.deprecated`
|
|
40
|
+
while filling in the SDK's current version metadata unless custom values are
|
|
41
|
+
supplied.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
base_decorator = _deprecated(
|
|
45
|
+
deprecated_in=deprecated_in,
|
|
46
|
+
removed_in=removed_in,
|
|
47
|
+
current_version=current_version or _current_version(),
|
|
48
|
+
details=details,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def decorator(func: _FuncT) -> _FuncT:
|
|
52
|
+
return cast(_FuncT, base_decorator(func))
|
|
53
|
+
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _should_warn(
|
|
58
|
+
*,
|
|
59
|
+
deprecated_in: str | None,
|
|
60
|
+
removed_in: str | date | None,
|
|
61
|
+
current_version: str | None,
|
|
62
|
+
) -> tuple[bool, bool]:
|
|
63
|
+
is_deprecated = False
|
|
64
|
+
is_unsupported = False
|
|
65
|
+
|
|
66
|
+
if isinstance(removed_in, date):
|
|
67
|
+
if date.today() >= removed_in:
|
|
68
|
+
is_unsupported = True
|
|
69
|
+
else:
|
|
70
|
+
is_deprecated = True
|
|
71
|
+
elif current_version:
|
|
72
|
+
current = pkg_version.parse(current_version)
|
|
73
|
+
if removed_in and current >= pkg_version.parse(str(removed_in)):
|
|
74
|
+
is_unsupported = True
|
|
75
|
+
elif deprecated_in and current >= pkg_version.parse(deprecated_in):
|
|
76
|
+
is_deprecated = True
|
|
77
|
+
else:
|
|
78
|
+
is_deprecated = True
|
|
79
|
+
|
|
80
|
+
return is_deprecated, is_unsupported
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def warn_deprecated(
|
|
84
|
+
feature: str,
|
|
85
|
+
*,
|
|
86
|
+
deprecated_in: str,
|
|
87
|
+
removed_in: str | date | None,
|
|
88
|
+
current_version: str | None = None,
|
|
89
|
+
details: str = "",
|
|
90
|
+
stacklevel: int = 2,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Emit a deprecation warning for dynamic access to a legacy feature.
|
|
93
|
+
|
|
94
|
+
Prefer this helper when a decorator is not practical—e.g. attribute accessors,
|
|
95
|
+
data migrations, or other runtime paths that must conditionally warn. Provide
|
|
96
|
+
explicit version metadata so the SDK reports consistent messages and upgrades
|
|
97
|
+
to :class:`deprecation.UnsupportedWarning` after the removal threshold.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
current_version = current_version or _current_version()
|
|
101
|
+
is_deprecated, is_unsupported = _should_warn(
|
|
102
|
+
deprecated_in=deprecated_in,
|
|
103
|
+
removed_in=removed_in,
|
|
104
|
+
current_version=current_version,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if not (is_deprecated or is_unsupported):
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
warning_cls = UnsupportedWarning if is_unsupported else DeprecatedWarning
|
|
111
|
+
warning = warning_cls(feature, deprecated_in, removed_in, details)
|
|
112
|
+
warnings.warn(warning, stacklevel=stacklevel)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def warn_cleanup(
|
|
116
|
+
workaround: str,
|
|
117
|
+
*,
|
|
118
|
+
cleanup_by: str | date,
|
|
119
|
+
current_version: str | None = None,
|
|
120
|
+
details: str = "",
|
|
121
|
+
stacklevel: int = 2,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Emit a warning for temporary workarounds that need cleanup by a deadline.
|
|
124
|
+
|
|
125
|
+
Use this helper for temporary code that addresses upstream issues, compatibility
|
|
126
|
+
shims, or other workarounds that should be removed once external conditions
|
|
127
|
+
change (e.g., when a library adds support for a feature, or when an API
|
|
128
|
+
stabilizes). The deprecation check workflow will fail when the cleanup deadline
|
|
129
|
+
is reached, ensuring the workaround is removed before the specified version or
|
|
130
|
+
date.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
workaround: Description of the temporary workaround
|
|
134
|
+
cleanup_by: Version string or date when this workaround must be removed
|
|
135
|
+
current_version: Override the detected package version (for testing)
|
|
136
|
+
details: Additional context about why cleanup is needed
|
|
137
|
+
stacklevel: Stack level for warning emission
|
|
138
|
+
"""
|
|
139
|
+
current_version = current_version or _current_version()
|
|
140
|
+
|
|
141
|
+
should_cleanup = False
|
|
142
|
+
if isinstance(cleanup_by, date):
|
|
143
|
+
should_cleanup = date.today() >= cleanup_by
|
|
144
|
+
else:
|
|
145
|
+
try:
|
|
146
|
+
current = pkg_version.parse(current_version)
|
|
147
|
+
target = pkg_version.parse(str(cleanup_by))
|
|
148
|
+
should_cleanup = current >= target
|
|
149
|
+
except pkg_version.InvalidVersion:
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
if should_cleanup:
|
|
153
|
+
message = (
|
|
154
|
+
f"Cleanup required: {workaround}. "
|
|
155
|
+
f"This workaround was scheduled for removal by {cleanup_by}."
|
|
156
|
+
)
|
|
157
|
+
if details:
|
|
158
|
+
message += f" {details}"
|
|
159
|
+
warnings.warn(message, UserWarning, stacklevel=stacklevel)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
__all__ = [
|
|
163
|
+
"deprecated",
|
|
164
|
+
"warn_deprecated",
|
|
165
|
+
"warn_cleanup",
|
|
166
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Utility functions for GitHub integrations."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Zero-width joiner character (U+200D)
|
|
7
|
+
# We use ZWJ instead of ZWSP (U+200B) because:
|
|
8
|
+
# - ZWJ is semantically more appropriate (joins characters without adding space)
|
|
9
|
+
# - ZWJ has better support in modern renderers
|
|
10
|
+
# - ZWJ is invisible and doesn't affect text rendering or selection
|
|
11
|
+
ZWJ = "\u200d"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sanitize_openhands_mentions(text: str) -> str:
|
|
15
|
+
"""Sanitize @OpenHands mentions in text to prevent self-mention loops.
|
|
16
|
+
|
|
17
|
+
This function inserts a zero-width joiner (ZWJ) after the @ symbol in
|
|
18
|
+
@OpenHands mentions, making them non-clickable in GitHub comments while
|
|
19
|
+
preserving readability. The original case of the mention is preserved.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: The text to sanitize
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Text with sanitized @OpenHands mentions (e.g., "@OpenHands" -> "@OpenHands")
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> sanitize_openhands_mentions("Thanks @OpenHands for the help!")
|
|
29
|
+
'Thanks @\\u200dOpenHands for the help!'
|
|
30
|
+
>>> sanitize_openhands_mentions("Check @openhands and @OPENHANDS")
|
|
31
|
+
'Check @\\u200dopenhands and @\\u200dOPENHANDS'
|
|
32
|
+
>>> sanitize_openhands_mentions("No mention here")
|
|
33
|
+
'No mention here'
|
|
34
|
+
"""
|
|
35
|
+
# Pattern to match @OpenHands mentions at word boundaries
|
|
36
|
+
# Uses re.IGNORECASE so we don't need [Oo]pen[Hh]ands
|
|
37
|
+
# Capture group preserves the original case
|
|
38
|
+
pattern = r"@(OpenHands)\b"
|
|
39
|
+
|
|
40
|
+
# Replace @ with @ + ZWJ while preserving the original case
|
|
41
|
+
# The \1 backreference preserves the matched case
|
|
42
|
+
sanitized = re.sub(pattern, f"@{ZWJ}\\1", text, flags=re.IGNORECASE)
|
|
43
|
+
|
|
44
|
+
return sanitized
|
|
@@ -0,0 +1,48 @@
|
|
|
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.")
|