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,111 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from openhands.sdk.event.base import Event
|
|
4
|
+
from openhands.sdk.event.llm_convertible import ActionEvent
|
|
5
|
+
from openhands.sdk.logger import get_logger
|
|
6
|
+
from openhands.sdk.security.risk import SecurityRisk
|
|
7
|
+
from openhands.sdk.utils.models import (
|
|
8
|
+
DiscriminatedUnionMixin,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SecurityAnalyzerBase(DiscriminatedUnionMixin, ABC):
|
|
16
|
+
"""Abstract base class for security analyzers.
|
|
17
|
+
|
|
18
|
+
Security analyzers evaluate the risk of actions before they are executed
|
|
19
|
+
and can influence the conversation flow based on security policies.
|
|
20
|
+
|
|
21
|
+
This is adapted from OpenHands SecurityAnalyzer but designed to work
|
|
22
|
+
with the agent-sdk's conversation-based architecture.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def security_risk(self, action: ActionEvent) -> SecurityRisk:
|
|
27
|
+
"""Evaluate the security risk of an ActionEvent.
|
|
28
|
+
|
|
29
|
+
This is the core method that analyzes an ActionEvent and returns its risk level.
|
|
30
|
+
Implementations should examine the action's content, context, and potential
|
|
31
|
+
impact to determine the appropriate risk level.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
action: The ActionEvent to analyze for security risks
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
ActionSecurityRisk enum indicating the risk level
|
|
38
|
+
"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def analyze_event(self, event: Event) -> SecurityRisk | None:
|
|
42
|
+
"""Analyze an event for security risks.
|
|
43
|
+
|
|
44
|
+
This is a convenience method that checks if the event is an action
|
|
45
|
+
and calls security_risk() if it is. Non-action events return None.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
event: The event to analyze
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
ActionSecurityRisk if event is an action, None otherwise
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(event, ActionEvent):
|
|
54
|
+
return self.security_risk(event)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def should_require_confirmation(
|
|
58
|
+
self, risk: SecurityRisk, confirmation_mode: bool = False
|
|
59
|
+
) -> bool:
|
|
60
|
+
"""Determine if an action should require user confirmation.
|
|
61
|
+
|
|
62
|
+
This implements the default confirmation logic based on risk level
|
|
63
|
+
and confirmation mode settings.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
risk: The security risk level of the action
|
|
67
|
+
confirmation_mode: Whether confirmation mode is enabled
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True if confirmation is required, False otherwise
|
|
71
|
+
"""
|
|
72
|
+
if risk == SecurityRisk.HIGH:
|
|
73
|
+
# HIGH risk actions always require confirmation
|
|
74
|
+
return True
|
|
75
|
+
elif risk == SecurityRisk.UNKNOWN and not confirmation_mode:
|
|
76
|
+
# UNKNOWN risk requires confirmation if no security analyzer is configured
|
|
77
|
+
return True
|
|
78
|
+
elif confirmation_mode:
|
|
79
|
+
# In confirmation mode, all actions require confirmation
|
|
80
|
+
return True
|
|
81
|
+
else:
|
|
82
|
+
# LOW and MEDIUM risk actions don't require confirmation by default
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
def analyze_pending_actions(
|
|
86
|
+
self, pending_actions: list[ActionEvent]
|
|
87
|
+
) -> list[tuple[ActionEvent, SecurityRisk]]:
|
|
88
|
+
"""Analyze all pending actions in a conversation.
|
|
89
|
+
|
|
90
|
+
This method gets all unmatched actions from the conversation state
|
|
91
|
+
and analyzes each one for security risks.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
conversation: The conversation to analyze
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of tuples containing (action, risk_level) for each pending action
|
|
98
|
+
"""
|
|
99
|
+
analyzed_actions = []
|
|
100
|
+
|
|
101
|
+
for action_event in pending_actions:
|
|
102
|
+
try:
|
|
103
|
+
risk = self.security_risk(action_event)
|
|
104
|
+
analyzed_actions.append((action_event, risk))
|
|
105
|
+
logger.debug(f"Action {action_event} analyzed with risk level: {risk}")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Error analyzing action {action_event}: {e}")
|
|
108
|
+
# Default to HIGH risk on analysis error for safety
|
|
109
|
+
analyzed_actions.append((action_event, SecurityRisk.HIGH))
|
|
110
|
+
|
|
111
|
+
return analyzed_actions
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from openhands.sdk.security.risk import SecurityRisk
|
|
6
|
+
from openhands.sdk.utils.models import DiscriminatedUnionMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfirmationPolicyBase(DiscriminatedUnionMixin, ABC):
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def should_confirm(self, risk: SecurityRisk = SecurityRisk.UNKNOWN) -> bool:
|
|
12
|
+
"""Determine if an action with the given risk level requires confirmation.
|
|
13
|
+
|
|
14
|
+
This method defines the core logic for determining whether user confirmation
|
|
15
|
+
is required before executing an action based on its security risk level.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
risk: The security risk level of the action to be evaluated.
|
|
19
|
+
Defaults to SecurityRisk.UNKNOWN if not specified.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
True if the action requires user confirmation before execution,
|
|
23
|
+
False if the action can proceed without confirmation.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AlwaysConfirm(ConfirmationPolicyBase):
|
|
28
|
+
def should_confirm(
|
|
29
|
+
self,
|
|
30
|
+
risk: SecurityRisk = SecurityRisk.UNKNOWN, # noqa: ARG002
|
|
31
|
+
) -> bool:
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NeverConfirm(ConfirmationPolicyBase):
|
|
36
|
+
def should_confirm(
|
|
37
|
+
self,
|
|
38
|
+
risk: SecurityRisk = SecurityRisk.UNKNOWN, # noqa: ARG002
|
|
39
|
+
) -> bool:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConfirmRisky(ConfirmationPolicyBase):
|
|
44
|
+
threshold: SecurityRisk = SecurityRisk.HIGH
|
|
45
|
+
confirm_unknown: bool = True
|
|
46
|
+
|
|
47
|
+
@field_validator("threshold")
|
|
48
|
+
def validate_threshold(cls, v: SecurityRisk) -> SecurityRisk:
|
|
49
|
+
if v == SecurityRisk.UNKNOWN:
|
|
50
|
+
raise ValueError("Threshold cannot be UNKNOWN")
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
def should_confirm(self, risk: SecurityRisk = SecurityRisk.UNKNOWN) -> bool:
|
|
54
|
+
if risk == SecurityRisk.UNKNOWN:
|
|
55
|
+
return self.confirm_unknown
|
|
56
|
+
|
|
57
|
+
# This comparison is reflexive by default, so if the threshold is HIGH we will
|
|
58
|
+
# still require confirmation for HIGH risk actions. And since the threshold is
|
|
59
|
+
# guaranteed to never be UNKNOWN (by the validator), we're guaranteed to get a
|
|
60
|
+
# boolean here.
|
|
61
|
+
return risk.is_riskier(self.threshold)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from openhands.sdk.event import ActionEvent
|
|
2
|
+
from openhands.sdk.logger import get_logger
|
|
3
|
+
from openhands.sdk.security.analyzer import SecurityAnalyzerBase
|
|
4
|
+
from openhands.sdk.security.risk import SecurityRisk
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LLMSecurityAnalyzer(SecurityAnalyzerBase):
|
|
11
|
+
"""LLM-based security analyzer.
|
|
12
|
+
|
|
13
|
+
This analyzer respects the security_risk attribute that can be set by the LLM
|
|
14
|
+
when generating actions, similar to OpenHands' LLMRiskAnalyzer.
|
|
15
|
+
|
|
16
|
+
It provides a lightweight security analysis approach that leverages the LLM's
|
|
17
|
+
understanding of action context and potential risks.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def security_risk(self, action: ActionEvent) -> SecurityRisk:
|
|
21
|
+
"""Evaluate security risk based on LLM-provided assessment.
|
|
22
|
+
|
|
23
|
+
This method checks if the action has a security_risk attribute set by the LLM
|
|
24
|
+
and returns it. The LLM may not always provide this attribute but it defaults to
|
|
25
|
+
UNKNOWN if not explicitly set.
|
|
26
|
+
"""
|
|
27
|
+
logger.debug(f"Analyzing security risk: {action} -- {action.security_risk}")
|
|
28
|
+
|
|
29
|
+
return action.security_risk
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecurityRisk(str, Enum):
|
|
9
|
+
"""Security risk levels for actions.
|
|
10
|
+
|
|
11
|
+
Based on OpenHands security risk levels but adapted for agent-sdk.
|
|
12
|
+
Integer values allow for easy comparison and ordering.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
UNKNOWN = "UNKNOWN"
|
|
16
|
+
LOW = "LOW"
|
|
17
|
+
MEDIUM = "MEDIUM"
|
|
18
|
+
HIGH = "HIGH"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def description(self) -> str:
|
|
22
|
+
"""Get a human-readable description of the risk level."""
|
|
23
|
+
descriptions = {
|
|
24
|
+
SecurityRisk.LOW: (
|
|
25
|
+
"Low risk - Safe operation with minimal security impact"
|
|
26
|
+
),
|
|
27
|
+
SecurityRisk.MEDIUM: (
|
|
28
|
+
"Medium risk - Moderate security impact, review recommended"
|
|
29
|
+
),
|
|
30
|
+
SecurityRisk.HIGH: (
|
|
31
|
+
"High risk - Significant security impact, confirmation required"
|
|
32
|
+
),
|
|
33
|
+
SecurityRisk.UNKNOWN: ("Unknown risk - Risk level could not be determined"),
|
|
34
|
+
}
|
|
35
|
+
return descriptions.get(self, "Unknown risk level")
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
return self.name
|
|
39
|
+
|
|
40
|
+
def get_color(self) -> str:
|
|
41
|
+
"""Get the color for displaying this risk level in Rich text."""
|
|
42
|
+
color_map = {
|
|
43
|
+
SecurityRisk.LOW: "green",
|
|
44
|
+
SecurityRisk.MEDIUM: "yellow",
|
|
45
|
+
SecurityRisk.HIGH: "red",
|
|
46
|
+
SecurityRisk.UNKNOWN: "white",
|
|
47
|
+
}
|
|
48
|
+
return color_map.get(self, "white")
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def visualize(self) -> Text:
|
|
52
|
+
"""Return Rich Text representation of this risk level."""
|
|
53
|
+
content = Text()
|
|
54
|
+
content.append(
|
|
55
|
+
"Predicted Security Risk: ",
|
|
56
|
+
style="bold",
|
|
57
|
+
)
|
|
58
|
+
content.append(
|
|
59
|
+
f"{self.value}\n\n",
|
|
60
|
+
style=f"bold {self.get_color()}",
|
|
61
|
+
)
|
|
62
|
+
return content
|
|
63
|
+
|
|
64
|
+
def is_riskier(self, other: SecurityRisk, reflexive: bool = True) -> bool:
|
|
65
|
+
"""Check if this risk level is riskier than another.
|
|
66
|
+
|
|
67
|
+
Risk levels follow the natural ordering: LOW is less risky than MEDIUM, which is
|
|
68
|
+
less risky than HIGH. UNKNOWN is not comparable to any other level.
|
|
69
|
+
|
|
70
|
+
To make this act like a standard well-ordered domain, we reflexively consider
|
|
71
|
+
risk levels to be riskier than themselves. That is:
|
|
72
|
+
|
|
73
|
+
for risk_level in list(SecurityRisk):
|
|
74
|
+
assert risk_level.is_riskier(risk_level)
|
|
75
|
+
|
|
76
|
+
# More concretely:
|
|
77
|
+
assert SecurityRisk.HIGH.is_riskier(SecurityRisk.HIGH)
|
|
78
|
+
assert SecurityRisk.MEDIUM.is_riskier(SecurityRisk.MEDIUM)
|
|
79
|
+
assert SecurityRisk.LOW.is_riskier(SecurityRisk.LOW)
|
|
80
|
+
|
|
81
|
+
This can be disabled by setting the `reflexive` parameter to False.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
other (SecurityRisk): The other risk level to compare against.
|
|
85
|
+
reflexive (bool): Whether the relationship is reflexive.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If either risk level is UNKNOWN.
|
|
89
|
+
"""
|
|
90
|
+
if self.value == SecurityRisk.UNKNOWN or other.value == SecurityRisk.UNKNOWN:
|
|
91
|
+
raise ValueError("Cannot compare unknown risk levels.")
|
|
92
|
+
|
|
93
|
+
# Map risk levels to a well-ordered domain for comparison. No need to map
|
|
94
|
+
# UNKNOWN since we'll already have raised an error by now if either is UNKNOWN.
|
|
95
|
+
risk_order = {
|
|
96
|
+
SecurityRisk.LOW: 1,
|
|
97
|
+
SecurityRisk.MEDIUM: 2,
|
|
98
|
+
SecurityRisk.HIGH: 3,
|
|
99
|
+
}
|
|
100
|
+
return risk_order[self] > risk_order[other] or (reflexive and self == other)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from openhands.sdk.tool.builtins import BUILT_IN_TOOLS, FinishTool, ThinkTool
|
|
2
|
+
from openhands.sdk.tool.registry import (
|
|
3
|
+
list_registered_tools,
|
|
4
|
+
register_tool,
|
|
5
|
+
resolve_tool,
|
|
6
|
+
)
|
|
7
|
+
from openhands.sdk.tool.schema import (
|
|
8
|
+
Action,
|
|
9
|
+
Observation,
|
|
10
|
+
)
|
|
11
|
+
from openhands.sdk.tool.spec import Tool
|
|
12
|
+
from openhands.sdk.tool.tool import (
|
|
13
|
+
ExecutableTool,
|
|
14
|
+
ToolAnnotations,
|
|
15
|
+
ToolDefinition,
|
|
16
|
+
ToolExecutor,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Tool",
|
|
22
|
+
"ToolDefinition",
|
|
23
|
+
"ToolAnnotations",
|
|
24
|
+
"ToolExecutor",
|
|
25
|
+
"ExecutableTool",
|
|
26
|
+
"Action",
|
|
27
|
+
"Observation",
|
|
28
|
+
"FinishTool",
|
|
29
|
+
"ThinkTool",
|
|
30
|
+
"BUILT_IN_TOOLS",
|
|
31
|
+
"register_tool",
|
|
32
|
+
"resolve_tool",
|
|
33
|
+
"list_registered_tools",
|
|
34
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, Self
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from openhands.sdk.tool.tool import (
|
|
8
|
+
Action,
|
|
9
|
+
Observation,
|
|
10
|
+
ToolAnnotations,
|
|
11
|
+
ToolDefinition,
|
|
12
|
+
ToolExecutor,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from openhands.sdk.conversation.base import BaseConversation
|
|
18
|
+
from openhands.sdk.conversation.state import ConversationState
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FinishAction(Action):
|
|
22
|
+
message: str = Field(description="Final message to send to the user.")
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def visualize(self) -> Text:
|
|
26
|
+
"""Return Rich Text representation of this action."""
|
|
27
|
+
content = Text()
|
|
28
|
+
content.append("Finish with message:\n", style="bold blue")
|
|
29
|
+
content.append(self.message)
|
|
30
|
+
return content
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FinishObservation(Observation):
|
|
34
|
+
"""
|
|
35
|
+
Observation returned after finishing a task.
|
|
36
|
+
The FinishAction itself contains the message sent to the user so no
|
|
37
|
+
extra fields are needed here.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def visualize(self) -> Text:
|
|
42
|
+
"""Return an empty Text representation since the message is in the action."""
|
|
43
|
+
return Text()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
TOOL_DESCRIPTION = """Signals the completion of the current task or conversation.
|
|
47
|
+
|
|
48
|
+
Use this tool when:
|
|
49
|
+
- You have successfully completed the user's requested task
|
|
50
|
+
- You cannot proceed further due to technical limitations or missing information
|
|
51
|
+
|
|
52
|
+
The message should include:
|
|
53
|
+
- A clear summary of actions taken and their results
|
|
54
|
+
- Any next steps for the user
|
|
55
|
+
- Explanation if you're unable to complete the task
|
|
56
|
+
- Any follow-up questions if more information is needed
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FinishExecutor(ToolExecutor):
|
|
61
|
+
def __call__(
|
|
62
|
+
self,
|
|
63
|
+
action: FinishAction,
|
|
64
|
+
conversation: "BaseConversation | None" = None, # noqa: ARG002
|
|
65
|
+
) -> FinishObservation:
|
|
66
|
+
return FinishObservation.from_text(text=action.message)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FinishTool(ToolDefinition[FinishAction, FinishObservation]):
|
|
70
|
+
"""Tool for signaling the completion of a task or conversation."""
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def create(
|
|
74
|
+
cls,
|
|
75
|
+
conv_state: "ConversationState | None" = None, # noqa: ARG003
|
|
76
|
+
**params,
|
|
77
|
+
) -> Sequence[Self]:
|
|
78
|
+
"""Create FinishTool instance.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
conv_state: Optional conversation state (not used by FinishTool).
|
|
82
|
+
**params: Additional parameters (none supported).
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
A sequence containing a single FinishTool instance.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ValueError: If any parameters are provided.
|
|
89
|
+
"""
|
|
90
|
+
if params:
|
|
91
|
+
raise ValueError("FinishTool doesn't accept parameters")
|
|
92
|
+
return [
|
|
93
|
+
cls(
|
|
94
|
+
action_type=FinishAction,
|
|
95
|
+
observation_type=FinishObservation,
|
|
96
|
+
description=TOOL_DESCRIPTION,
|
|
97
|
+
executor=FinishExecutor(),
|
|
98
|
+
annotations=ToolAnnotations(
|
|
99
|
+
title="finish",
|
|
100
|
+
readOnlyHint=True,
|
|
101
|
+
destructiveHint=False,
|
|
102
|
+
idempotentHint=True,
|
|
103
|
+
openWorldHint=False,
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import TYPE_CHECKING, Self
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
from openhands.sdk.tool.tool import (
|
|
8
|
+
Action,
|
|
9
|
+
Observation,
|
|
10
|
+
ToolAnnotations,
|
|
11
|
+
ToolDefinition,
|
|
12
|
+
ToolExecutor,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from openhands.sdk.conversation.base import BaseConversation
|
|
18
|
+
from openhands.sdk.conversation.state import ConversationState
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ThinkAction(Action):
|
|
22
|
+
"""Action for logging a thought without making any changes."""
|
|
23
|
+
|
|
24
|
+
thought: str = Field(description="The thought to log.")
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def visualize(self) -> Text:
|
|
28
|
+
"""Return Rich Text representation with thinking styling."""
|
|
29
|
+
content = Text()
|
|
30
|
+
|
|
31
|
+
# Add thinking icon and header
|
|
32
|
+
content.append("🤔 ", style="yellow")
|
|
33
|
+
content.append("Thinking: ", style="bold yellow")
|
|
34
|
+
|
|
35
|
+
# Add the thought content with proper formatting
|
|
36
|
+
if self.thought:
|
|
37
|
+
# Split into lines for better formatting
|
|
38
|
+
lines = self.thought.split("\n")
|
|
39
|
+
for i, line in enumerate(lines):
|
|
40
|
+
if i > 0:
|
|
41
|
+
content.append("\n")
|
|
42
|
+
content.append(line.strip(), style="italic white")
|
|
43
|
+
|
|
44
|
+
return content
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ThinkObservation(Observation):
|
|
48
|
+
"""
|
|
49
|
+
Observation returned after logging a thought.
|
|
50
|
+
The ThinkAction itself contains the thought logged so no extra
|
|
51
|
+
fields are needed here.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def visualize(self) -> Text:
|
|
56
|
+
"""Return an empty Text representation since the thought is in the action."""
|
|
57
|
+
return Text()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
Common use cases:
|
|
63
|
+
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.
|
|
64
|
+
2. After receiving test results, use this tool to brainstorm ways to fix failing tests.
|
|
65
|
+
3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs.
|
|
66
|
+
4. When designing a new feature, use this tool to think through architecture decisions and implementation details.
|
|
67
|
+
5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses.
|
|
68
|
+
|
|
69
|
+
The tool simply logs your thought process for better transparency and does not execute any code or make changes.""" # noqa: E501
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ThinkExecutor(ToolExecutor):
|
|
73
|
+
def __call__(
|
|
74
|
+
self,
|
|
75
|
+
_: ThinkAction,
|
|
76
|
+
conversation: "BaseConversation | None" = None, # noqa: ARG002
|
|
77
|
+
) -> ThinkObservation:
|
|
78
|
+
return ThinkObservation.from_text(text="Your thought has been logged.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ThinkTool(ToolDefinition[ThinkAction, ThinkObservation]):
|
|
82
|
+
"""Tool for logging thoughts without making changes."""
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def create(
|
|
86
|
+
cls,
|
|
87
|
+
conv_state: "ConversationState | None" = None, # noqa: ARG003
|
|
88
|
+
**params,
|
|
89
|
+
) -> Sequence[Self]:
|
|
90
|
+
"""Create ThinkTool instance.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
conv_state: Optional conversation state (not used by ThinkTool).
|
|
94
|
+
**params: Additional parameters (none supported).
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A sequence containing a single ThinkTool instance.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
ValueError: If any parameters are provided.
|
|
101
|
+
"""
|
|
102
|
+
if params:
|
|
103
|
+
raise ValueError("ThinkTool doesn't accept parameters")
|
|
104
|
+
return [
|
|
105
|
+
cls(
|
|
106
|
+
description=THINK_DESCRIPTION,
|
|
107
|
+
action_type=ThinkAction,
|
|
108
|
+
observation_type=ThinkObservation,
|
|
109
|
+
executor=ThinkExecutor(),
|
|
110
|
+
annotations=ToolAnnotations(
|
|
111
|
+
readOnlyHint=True,
|
|
112
|
+
destructiveHint=False,
|
|
113
|
+
idempotentHint=True,
|
|
114
|
+
openWorldHint=False,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
]
|