openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- openhands-1.0.1.dist-info/METADATA +52 -0
- openhands-1.0.1.dist-info/RECORD +31 -0
- {openhands-0.0.0.dist-info → openhands-1.0.1.dist-info}/WHEEL +1 -2
- openhands-1.0.1.dist-info/entry_points.txt +2 -0
- openhands_cli/__init__.py +8 -0
- openhands_cli/agent_chat.py +186 -0
- openhands_cli/argparsers/main_parser.py +56 -0
- openhands_cli/argparsers/serve_parser.py +31 -0
- openhands_cli/gui_launcher.py +220 -0
- openhands_cli/listeners/__init__.py +4 -0
- openhands_cli/listeners/loading_listener.py +63 -0
- openhands_cli/listeners/pause_listener.py +83 -0
- openhands_cli/llm_utils.py +57 -0
- openhands_cli/locations.py +13 -0
- openhands_cli/pt_style.py +30 -0
- openhands_cli/runner.py +178 -0
- openhands_cli/setup.py +116 -0
- openhands_cli/simple_main.py +59 -0
- openhands_cli/tui/__init__.py +5 -0
- openhands_cli/tui/settings/mcp_screen.py +217 -0
- openhands_cli/tui/settings/settings_screen.py +202 -0
- openhands_cli/tui/settings/store.py +93 -0
- openhands_cli/tui/status.py +109 -0
- openhands_cli/tui/tui.py +100 -0
- openhands_cli/tui/utils.py +14 -0
- openhands_cli/user_actions/__init__.py +17 -0
- openhands_cli/user_actions/agent_action.py +95 -0
- openhands_cli/user_actions/exit_session.py +18 -0
- openhands_cli/user_actions/settings_action.py +171 -0
- openhands_cli/user_actions/types.py +18 -0
- openhands_cli/user_actions/utils.py +199 -0
- openhands/__init__.py +0 -1
- openhands/sdk/__init__.py +0 -45
- openhands/sdk/agent/__init__.py +0 -8
- openhands/sdk/agent/agent/__init__.py +0 -6
- openhands/sdk/agent/agent/agent.py +0 -349
- openhands/sdk/agent/base.py +0 -103
- openhands/sdk/context/__init__.py +0 -28
- openhands/sdk/context/agent_context.py +0 -153
- openhands/sdk/context/condenser/__init__.py +0 -5
- openhands/sdk/context/condenser/condenser.py +0 -73
- openhands/sdk/context/condenser/no_op_condenser.py +0 -13
- openhands/sdk/context/manager.py +0 -5
- openhands/sdk/context/microagents/__init__.py +0 -26
- openhands/sdk/context/microagents/exceptions.py +0 -11
- openhands/sdk/context/microagents/microagent.py +0 -345
- openhands/sdk/context/microagents/types.py +0 -70
- openhands/sdk/context/utils/__init__.py +0 -8
- openhands/sdk/context/utils/prompt.py +0 -52
- openhands/sdk/context/view.py +0 -116
- openhands/sdk/conversation/__init__.py +0 -12
- openhands/sdk/conversation/conversation.py +0 -207
- openhands/sdk/conversation/state.py +0 -50
- openhands/sdk/conversation/types.py +0 -6
- openhands/sdk/conversation/visualizer.py +0 -300
- openhands/sdk/event/__init__.py +0 -27
- openhands/sdk/event/base.py +0 -148
- openhands/sdk/event/condenser.py +0 -49
- openhands/sdk/event/llm_convertible.py +0 -265
- openhands/sdk/event/types.py +0 -5
- openhands/sdk/event/user_action.py +0 -12
- openhands/sdk/event/utils.py +0 -30
- openhands/sdk/llm/__init__.py +0 -19
- openhands/sdk/llm/exceptions.py +0 -108
- openhands/sdk/llm/llm.py +0 -867
- openhands/sdk/llm/llm_registry.py +0 -116
- openhands/sdk/llm/message.py +0 -216
- openhands/sdk/llm/metadata.py +0 -34
- openhands/sdk/llm/utils/fn_call_converter.py +0 -1049
- openhands/sdk/llm/utils/metrics.py +0 -311
- openhands/sdk/llm/utils/model_features.py +0 -153
- openhands/sdk/llm/utils/retry_mixin.py +0 -122
- openhands/sdk/llm/utils/telemetry.py +0 -252
- openhands/sdk/logger.py +0 -167
- openhands/sdk/mcp/__init__.py +0 -20
- openhands/sdk/mcp/client.py +0 -113
- openhands/sdk/mcp/definition.py +0 -69
- openhands/sdk/mcp/tool.py +0 -104
- openhands/sdk/mcp/utils.py +0 -59
- openhands/sdk/tests/llm/test_llm.py +0 -447
- openhands/sdk/tests/llm/test_llm_fncall_converter.py +0 -691
- openhands/sdk/tests/llm/test_model_features.py +0 -221
- openhands/sdk/tool/__init__.py +0 -30
- openhands/sdk/tool/builtins/__init__.py +0 -34
- openhands/sdk/tool/builtins/finish.py +0 -57
- openhands/sdk/tool/builtins/think.py +0 -60
- openhands/sdk/tool/schema.py +0 -236
- openhands/sdk/tool/security_prompt.py +0 -5
- openhands/sdk/tool/tool.py +0 -142
- openhands/sdk/utils/__init__.py +0 -14
- openhands/sdk/utils/discriminated_union.py +0 -210
- openhands/sdk/utils/json.py +0 -48
- openhands/sdk/utils/truncate.py +0 -44
- openhands/tools/__init__.py +0 -44
- openhands/tools/execute_bash/__init__.py +0 -30
- openhands/tools/execute_bash/constants.py +0 -31
- openhands/tools/execute_bash/definition.py +0 -166
- openhands/tools/execute_bash/impl.py +0 -38
- openhands/tools/execute_bash/metadata.py +0 -101
- openhands/tools/execute_bash/terminal/__init__.py +0 -22
- openhands/tools/execute_bash/terminal/factory.py +0 -113
- openhands/tools/execute_bash/terminal/interface.py +0 -189
- openhands/tools/execute_bash/terminal/subprocess_terminal.py +0 -412
- openhands/tools/execute_bash/terminal/terminal_session.py +0 -492
- openhands/tools/execute_bash/terminal/tmux_terminal.py +0 -160
- openhands/tools/execute_bash/utils/command.py +0 -150
- openhands/tools/str_replace_editor/__init__.py +0 -17
- openhands/tools/str_replace_editor/definition.py +0 -158
- openhands/tools/str_replace_editor/editor.py +0 -683
- openhands/tools/str_replace_editor/exceptions.py +0 -41
- openhands/tools/str_replace_editor/impl.py +0 -66
- openhands/tools/str_replace_editor/utils/__init__.py +0 -0
- openhands/tools/str_replace_editor/utils/config.py +0 -2
- openhands/tools/str_replace_editor/utils/constants.py +0 -9
- openhands/tools/str_replace_editor/utils/encoding.py +0 -135
- openhands/tools/str_replace_editor/utils/file_cache.py +0 -154
- openhands/tools/str_replace_editor/utils/history.py +0 -122
- openhands/tools/str_replace_editor/utils/shell.py +0 -72
- openhands/tools/task_tracker/__init__.py +0 -16
- openhands/tools/task_tracker/definition.py +0 -336
- openhands/tools/utils/__init__.py +0 -1
- openhands-0.0.0.dist-info/METADATA +0 -3
- openhands-0.0.0.dist-info/RECORD +0 -94
- openhands-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from logging import getLogger
|
|
3
|
-
|
|
4
|
-
from openhands.sdk.context.view import View
|
|
5
|
-
from openhands.sdk.event.condenser import Condensation
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
logger = getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Condenser(ABC):
|
|
12
|
-
"""Abstract condenser interface.
|
|
13
|
-
|
|
14
|
-
Condensers take a list of `Event` objects and reduce them into a potentially smaller
|
|
15
|
-
list.
|
|
16
|
-
|
|
17
|
-
Agents can use condensers to reduce the amount of events they need to consider when
|
|
18
|
-
deciding which action to take. To use a condenser, agents can call the
|
|
19
|
-
`condensed_history` method on the current `State` being considered and use the
|
|
20
|
-
results instead of the full history.
|
|
21
|
-
|
|
22
|
-
If the condenser returns a `Condensation` instead of a `View`, the agent should
|
|
23
|
-
return `Condensation.action` instead of producing its own action. On the next agent
|
|
24
|
-
step the condenser will use that condensation event to produce a new `View`.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
@abstractmethod
|
|
28
|
-
def condense(self, view: View) -> View | Condensation:
|
|
29
|
-
"""Condense a sequence of events into a potentially smaller list.
|
|
30
|
-
|
|
31
|
-
New condenser strategies should override this method to implement their own
|
|
32
|
-
condensation logic. Call `self.add_metadata` in the implementation to record any
|
|
33
|
-
relevant per-condensation diagnostic information.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
view: A view of the history containing all events that should be condensed.
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
View | Condensation: A condensed view of the events or an event indicating
|
|
40
|
-
the history has been condensed.
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class RollingCondenser(Condenser, ABC):
|
|
45
|
-
"""Base class for a specialized condenser strategy that applies condensation to a
|
|
46
|
-
rolling history.
|
|
47
|
-
|
|
48
|
-
The rolling history is generated by `View.from_events`, which analyzes all events in
|
|
49
|
-
the history and produces a `View` object representing what will be sent to the LLM.
|
|
50
|
-
|
|
51
|
-
If `should_condense` says so, the condenser is then responsible for generating a
|
|
52
|
-
`Condensation` object from the `View` object. This will be added to the event
|
|
53
|
-
history which should -- when given to `get_view` -- produce the condensed `View` to
|
|
54
|
-
be passed to the LLM.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
@abstractmethod
|
|
58
|
-
def should_condense(self, view: View) -> bool:
|
|
59
|
-
"""Determine if a view should be condensed."""
|
|
60
|
-
|
|
61
|
-
@abstractmethod
|
|
62
|
-
def get_condensation(self, view: View) -> Condensation:
|
|
63
|
-
"""Get the condensation from a view."""
|
|
64
|
-
|
|
65
|
-
def condense(self, view: View) -> View | Condensation:
|
|
66
|
-
# If we trigger the condenser-specific condensation threshold, compute and
|
|
67
|
-
# return the condensation.
|
|
68
|
-
if self.should_condense(view):
|
|
69
|
-
return self.get_condensation(view)
|
|
70
|
-
|
|
71
|
-
# Otherwise we're safe to just return the view.
|
|
72
|
-
else:
|
|
73
|
-
return view
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from openhands.sdk.context.condenser.condenser import Condenser
|
|
2
|
-
from openhands.sdk.context.view import View
|
|
3
|
-
from openhands.sdk.event.condenser import Condensation
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class NoOpCondenser(Condenser):
|
|
7
|
-
"""Simple condenser that returns a view un-manipulated.
|
|
8
|
-
|
|
9
|
-
Primarily intended for testing purposes.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
def condense(self, view: View) -> View | Condensation:
|
|
13
|
-
return view
|
openhands/sdk/context/manager.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from openhands.sdk.context.microagents.exceptions import MicroagentValidationError
|
|
2
|
-
from openhands.sdk.context.microagents.microagent import (
|
|
3
|
-
BaseMicroagent,
|
|
4
|
-
KnowledgeMicroagent,
|
|
5
|
-
RepoMicroagent,
|
|
6
|
-
TaskMicroagent,
|
|
7
|
-
load_microagents_from_dir,
|
|
8
|
-
)
|
|
9
|
-
from openhands.sdk.context.microagents.types import (
|
|
10
|
-
MicroagentKnowledge,
|
|
11
|
-
MicroagentMetadata,
|
|
12
|
-
MicroagentType,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
__all__ = [
|
|
17
|
-
"BaseMicroagent",
|
|
18
|
-
"KnowledgeMicroagent",
|
|
19
|
-
"RepoMicroagent",
|
|
20
|
-
"TaskMicroagent",
|
|
21
|
-
"MicroagentMetadata",
|
|
22
|
-
"MicroagentType",
|
|
23
|
-
"MicroagentKnowledge",
|
|
24
|
-
"load_microagents_from_dir",
|
|
25
|
-
"MicroagentValidationError",
|
|
26
|
-
]
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
class MicroagentError(Exception):
|
|
2
|
-
"""Base exception for all microagent errors."""
|
|
3
|
-
|
|
4
|
-
pass
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class MicroagentValidationError(MicroagentError):
|
|
8
|
-
"""Raised when there's a validation error in microagent metadata."""
|
|
9
|
-
|
|
10
|
-
def __init__(self, message: str = "Microagent validation failed") -> None:
|
|
11
|
-
super().__init__(message)
|
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
import io
|
|
2
|
-
import re
|
|
3
|
-
from itertools import chain
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any, ClassVar, Union, cast
|
|
6
|
-
|
|
7
|
-
import frontmatter
|
|
8
|
-
from fastmcp.mcp_config import MCPConfig
|
|
9
|
-
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
10
|
-
|
|
11
|
-
from openhands.sdk.context.microagents.exceptions import MicroagentValidationError
|
|
12
|
-
from openhands.sdk.context.microagents.types import (
|
|
13
|
-
InputMetadata,
|
|
14
|
-
MicroagentType,
|
|
15
|
-
)
|
|
16
|
-
from openhands.sdk.logger import get_logger
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
logger = get_logger(__name__)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class BaseMicroagent(BaseModel):
|
|
23
|
-
"""Base class for all microagents."""
|
|
24
|
-
|
|
25
|
-
name: str
|
|
26
|
-
content: str
|
|
27
|
-
source: str | None = Field(
|
|
28
|
-
default=None,
|
|
29
|
-
description=(
|
|
30
|
-
"The source path or identifier of the microagent. "
|
|
31
|
-
"When it is None, it is treated as a programmatically defined microagent."
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
type: MicroagentType = Field(..., description="The type of the microagent")
|
|
35
|
-
|
|
36
|
-
PATH_TO_THIRD_PARTY_MICROAGENT_NAME: ClassVar[dict[str, str]] = {
|
|
37
|
-
".cursorrules": "cursorrules",
|
|
38
|
-
"agents.md": "agents",
|
|
39
|
-
"agent.md": "agents",
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def _handle_third_party(
|
|
44
|
-
cls, path: Path, file_content: str
|
|
45
|
-
) -> Union["RepoMicroagent", None]:
|
|
46
|
-
# Determine the agent name based on file type
|
|
47
|
-
microagent_name = cls.PATH_TO_THIRD_PARTY_MICROAGENT_NAME.get(path.name.lower())
|
|
48
|
-
|
|
49
|
-
# Create RepoMicroagent if we recognized the file type
|
|
50
|
-
if microagent_name is not None:
|
|
51
|
-
return RepoMicroagent(
|
|
52
|
-
name=microagent_name,
|
|
53
|
-
content=file_content,
|
|
54
|
-
source=str(path),
|
|
55
|
-
type=MicroagentType.REPO_KNOWLEDGE,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
@classmethod
|
|
61
|
-
def load(
|
|
62
|
-
cls,
|
|
63
|
-
path: Union[str, Path],
|
|
64
|
-
microagent_dir: Path | None = None,
|
|
65
|
-
file_content: str | None = None,
|
|
66
|
-
) -> "BaseMicroagent":
|
|
67
|
-
"""Load a microagent from a markdown file with frontmatter.
|
|
68
|
-
|
|
69
|
-
The agent's name is derived from its path relative to the microagent_dir.
|
|
70
|
-
"""
|
|
71
|
-
path = Path(path) if isinstance(path, str) else path
|
|
72
|
-
|
|
73
|
-
# Calculate derived name from relative path if microagent_dir is provided
|
|
74
|
-
microagent_name = None
|
|
75
|
-
if microagent_dir is not None:
|
|
76
|
-
# Special handling for files which are not in microagent_dir
|
|
77
|
-
microagent_name = cls.PATH_TO_THIRD_PARTY_MICROAGENT_NAME.get(
|
|
78
|
-
path.name.lower()
|
|
79
|
-
) or str(path.relative_to(microagent_dir).with_suffix(""))
|
|
80
|
-
else:
|
|
81
|
-
microagent_name = path.stem
|
|
82
|
-
|
|
83
|
-
# Only load directly from path if file_content is not provided
|
|
84
|
-
if file_content is None:
|
|
85
|
-
with open(path) as f:
|
|
86
|
-
file_content = f.read()
|
|
87
|
-
|
|
88
|
-
# Legacy repo instructions are stored in .openhands_instructions
|
|
89
|
-
if path.name == ".openhands_instructions":
|
|
90
|
-
return RepoMicroagent(
|
|
91
|
-
name="repo_legacy",
|
|
92
|
-
content=file_content,
|
|
93
|
-
source=str(path),
|
|
94
|
-
type=MicroagentType.REPO_KNOWLEDGE,
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
# Handle third-party agent instruction files
|
|
98
|
-
third_party_agent = cls._handle_third_party(path, file_content)
|
|
99
|
-
if third_party_agent is not None:
|
|
100
|
-
return third_party_agent
|
|
101
|
-
|
|
102
|
-
file_io = io.StringIO(file_content)
|
|
103
|
-
loaded = frontmatter.load(file_io)
|
|
104
|
-
content = loaded.content
|
|
105
|
-
|
|
106
|
-
# Handle case where there's no frontmatter or empty frontmatter
|
|
107
|
-
metadata_dict = loaded.metadata or {}
|
|
108
|
-
|
|
109
|
-
# Use name from frontmatter if provided, otherwise use derived name
|
|
110
|
-
agent_name = str(metadata_dict.get("name", microagent_name))
|
|
111
|
-
|
|
112
|
-
# Validate type field if provided in frontmatter
|
|
113
|
-
if "type" in metadata_dict:
|
|
114
|
-
type_value = metadata_dict["type"]
|
|
115
|
-
valid_types = [t.value for t in MicroagentType]
|
|
116
|
-
if type_value not in valid_types:
|
|
117
|
-
valid_types_str = ", ".join(f'"{t}"' for t in valid_types)
|
|
118
|
-
raise MicroagentValidationError(
|
|
119
|
-
f'Invalid "type" value: "{type_value}". '
|
|
120
|
-
f"Valid types are: {valid_types_str}"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
# Infer the agent type:
|
|
124
|
-
# 1. If inputs exist -> TASK
|
|
125
|
-
# 2. If triggers exist -> KNOWLEDGE
|
|
126
|
-
# 3. Else (no triggers) -> REPO (always active)
|
|
127
|
-
triggers = metadata_dict.get("triggers", [])
|
|
128
|
-
if not isinstance(triggers, list):
|
|
129
|
-
raise MicroagentValidationError("Triggers must be a list of strings")
|
|
130
|
-
if "inputs" in metadata_dict:
|
|
131
|
-
# Add a trigger for the agent name if not already present
|
|
132
|
-
trigger = f"/{agent_name}"
|
|
133
|
-
if trigger not in triggers:
|
|
134
|
-
triggers.append(trigger)
|
|
135
|
-
return TaskMicroagent(
|
|
136
|
-
name=agent_name,
|
|
137
|
-
content=content,
|
|
138
|
-
source=str(path),
|
|
139
|
-
triggers=triggers,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
elif metadata_dict.get("triggers", None):
|
|
143
|
-
return KnowledgeMicroagent(
|
|
144
|
-
name=agent_name,
|
|
145
|
-
content=content,
|
|
146
|
-
source=str(path),
|
|
147
|
-
triggers=triggers,
|
|
148
|
-
)
|
|
149
|
-
else:
|
|
150
|
-
# No triggers, default to REPO
|
|
151
|
-
mcp_tools_raw = metadata_dict.get("mcp_tools")
|
|
152
|
-
# Type cast to satisfy type checker - validation happens in RepoMicroagent
|
|
153
|
-
mcp_tools = cast(MCPConfig | dict[str, Any] | None, mcp_tools_raw)
|
|
154
|
-
return RepoMicroagent(
|
|
155
|
-
name=agent_name, content=content, source=str(path), mcp_tools=mcp_tools
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
class KnowledgeMicroagent(BaseMicroagent):
|
|
160
|
-
"""Knowledge micro-agents provide specialized expertise that's triggered by keywords
|
|
161
|
-
in conversations.
|
|
162
|
-
|
|
163
|
-
They help with:
|
|
164
|
-
- Language best practices
|
|
165
|
-
- Framework guidelines
|
|
166
|
-
- Common patterns
|
|
167
|
-
- Tool usage
|
|
168
|
-
"""
|
|
169
|
-
|
|
170
|
-
type: MicroagentType = MicroagentType.KNOWLEDGE
|
|
171
|
-
triggers: list[str] = Field(
|
|
172
|
-
default_factory=list, description="List of triggers for the microagent"
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
def __init__(self, **data):
|
|
176
|
-
super().__init__(**data)
|
|
177
|
-
|
|
178
|
-
def match_trigger(self, message: str) -> str | None:
|
|
179
|
-
"""Match a trigger in the message.
|
|
180
|
-
|
|
181
|
-
It returns the first trigger that matches the message.
|
|
182
|
-
"""
|
|
183
|
-
message = message.lower()
|
|
184
|
-
for trigger in self.triggers:
|
|
185
|
-
if trigger.lower() in message:
|
|
186
|
-
return trigger
|
|
187
|
-
|
|
188
|
-
return None
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
class RepoMicroagent(BaseMicroagent):
|
|
192
|
-
"""Microagent specialized for repository-specific knowledge and guidelines.
|
|
193
|
-
|
|
194
|
-
RepoMicroagents are loaded from `.openhands/microagents/repo.md` files within
|
|
195
|
-
repositories and contain private, repository-specific instructions that are
|
|
196
|
-
automatically loaded when
|
|
197
|
-
working with that repository. They are ideal for:
|
|
198
|
-
- Repository-specific guidelines
|
|
199
|
-
- Team practices and conventions
|
|
200
|
-
- Project-specific workflows
|
|
201
|
-
- Custom documentation references
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
type: MicroagentType = MicroagentType.REPO_KNOWLEDGE
|
|
205
|
-
mcp_tools: MCPConfig | dict | None = Field(
|
|
206
|
-
default=None,
|
|
207
|
-
description="MCP tools configuration for the microagent",
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
# Field-level validation for mcp_tools
|
|
211
|
-
@field_validator("mcp_tools")
|
|
212
|
-
@classmethod
|
|
213
|
-
def _validate_mcp_tools(cls, v: MCPConfig | dict | None, info):
|
|
214
|
-
if v is None:
|
|
215
|
-
return v
|
|
216
|
-
if isinstance(v, dict):
|
|
217
|
-
try:
|
|
218
|
-
v = MCPConfig.model_validate(v)
|
|
219
|
-
except Exception as e:
|
|
220
|
-
raise MicroagentValidationError(
|
|
221
|
-
f"Invalid MCPConfig dictionary: {e}"
|
|
222
|
-
) from e
|
|
223
|
-
return v
|
|
224
|
-
|
|
225
|
-
@model_validator(mode="after")
|
|
226
|
-
def _enforce_repo_type(self):
|
|
227
|
-
if self.type != MicroagentType.REPO_KNOWLEDGE:
|
|
228
|
-
raise MicroagentValidationError(
|
|
229
|
-
f"RepoMicroagent initialized with incorrect type: {self.type}"
|
|
230
|
-
)
|
|
231
|
-
return self
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class TaskMicroagent(KnowledgeMicroagent):
|
|
235
|
-
"""TaskMicroagent is a special type of KnowledgeMicroagent that requires user input.
|
|
236
|
-
|
|
237
|
-
These microagents are triggered by a special format: "/{agent_name}"
|
|
238
|
-
and will prompt the user for any required inputs before proceeding.
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
type: MicroagentType = MicroagentType.TASK
|
|
242
|
-
inputs: list[InputMetadata] = Field(
|
|
243
|
-
default_factory=list,
|
|
244
|
-
description=(
|
|
245
|
-
"Input metadata for the microagent. Only exists for task microagents"
|
|
246
|
-
),
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
def __init__(self, **data):
|
|
250
|
-
super().__init__(**data)
|
|
251
|
-
self._append_missing_variables_prompt()
|
|
252
|
-
|
|
253
|
-
def _append_missing_variables_prompt(self) -> None:
|
|
254
|
-
"""Append a prompt to ask for missing variables."""
|
|
255
|
-
# Check if the content contains any variables or has inputs defined
|
|
256
|
-
if not self.requires_user_input() and not self.inputs:
|
|
257
|
-
return
|
|
258
|
-
|
|
259
|
-
prompt = (
|
|
260
|
-
"\n\nIf the user didn't provide any of these variables, ask the user to "
|
|
261
|
-
"provide them first before the agent can proceed with the task."
|
|
262
|
-
)
|
|
263
|
-
self.content += prompt
|
|
264
|
-
|
|
265
|
-
def extract_variables(self, content: str) -> list[str]:
|
|
266
|
-
"""Extract variables from the content.
|
|
267
|
-
|
|
268
|
-
Variables are in the format ${variable_name}.
|
|
269
|
-
"""
|
|
270
|
-
pattern = r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}"
|
|
271
|
-
matches = re.findall(pattern, content)
|
|
272
|
-
return matches
|
|
273
|
-
|
|
274
|
-
def requires_user_input(self) -> bool:
|
|
275
|
-
"""Check if this microagent requires user input.
|
|
276
|
-
|
|
277
|
-
Returns True if the content contains variables in the format ${variable_name}.
|
|
278
|
-
"""
|
|
279
|
-
# Check if the content contains any variables
|
|
280
|
-
variables = self.extract_variables(self.content)
|
|
281
|
-
logger.debug(f"This microagent requires user input: {variables}")
|
|
282
|
-
return len(variables) > 0
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def load_microagents_from_dir(
|
|
286
|
-
microagent_dir: str | Path,
|
|
287
|
-
) -> tuple[dict[str, RepoMicroagent], dict[str, KnowledgeMicroagent]]:
|
|
288
|
-
"""Load all microagents from the given directory.
|
|
289
|
-
|
|
290
|
-
Note, legacy repo instructions will not be loaded here.
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
microagent_dir: Path to the microagents directory (e.g. .openhands/microagents)
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
Tuple of (repo_agents, knowledge_agents) dictionaries
|
|
297
|
-
"""
|
|
298
|
-
if isinstance(microagent_dir, str):
|
|
299
|
-
microagent_dir = Path(microagent_dir)
|
|
300
|
-
|
|
301
|
-
repo_agents = {}
|
|
302
|
-
knowledge_agents = {}
|
|
303
|
-
|
|
304
|
-
# Load all agents from microagents directory
|
|
305
|
-
logger.debug(f"Loading agents from {microagent_dir}")
|
|
306
|
-
|
|
307
|
-
# Always check for .cursorrules and AGENTS.md files in repo root, regardless of whether microagents_dir exists # noqa: E501
|
|
308
|
-
special_files = []
|
|
309
|
-
repo_root = microagent_dir.parent.parent
|
|
310
|
-
|
|
311
|
-
# Check for third party rules: .cursorrules, AGENTS.md, etc
|
|
312
|
-
for filename in BaseMicroagent.PATH_TO_THIRD_PARTY_MICROAGENT_NAME.keys():
|
|
313
|
-
for variant in [filename, filename.lower(), filename.upper()]:
|
|
314
|
-
if (repo_root / variant).exists():
|
|
315
|
-
special_files.append(repo_root / variant)
|
|
316
|
-
break # Only add the first one found to avoid duplicates
|
|
317
|
-
|
|
318
|
-
# Collect .md files from microagents directory if it exists
|
|
319
|
-
md_files = []
|
|
320
|
-
if microagent_dir.exists():
|
|
321
|
-
md_files = [f for f in microagent_dir.rglob("*.md") if f.name != "README.md"]
|
|
322
|
-
|
|
323
|
-
# Process all files in one loop
|
|
324
|
-
for file in chain(special_files, md_files):
|
|
325
|
-
try:
|
|
326
|
-
agent = BaseMicroagent.load(file, microagent_dir)
|
|
327
|
-
if isinstance(agent, RepoMicroagent):
|
|
328
|
-
repo_agents[agent.name] = agent
|
|
329
|
-
elif isinstance(agent, KnowledgeMicroagent):
|
|
330
|
-
# Both KnowledgeMicroagent and TaskMicroagent go into knowledge_agents
|
|
331
|
-
knowledge_agents[agent.name] = agent
|
|
332
|
-
except MicroagentValidationError as e:
|
|
333
|
-
# For validation errors, include the original exception
|
|
334
|
-
error_msg = f"Error loading microagent from {file}: {str(e)}"
|
|
335
|
-
raise MicroagentValidationError(error_msg) from e
|
|
336
|
-
except Exception as e:
|
|
337
|
-
# For other errors, wrap in a ValueError with detailed message
|
|
338
|
-
error_msg = f"Error loading microagent from {file}: {str(e)}"
|
|
339
|
-
raise ValueError(error_msg) from e
|
|
340
|
-
|
|
341
|
-
logger.debug(
|
|
342
|
-
f"Loaded {len(repo_agents) + len(knowledge_agents)} microagents: "
|
|
343
|
-
f"{[*repo_agents.keys(), *knowledge_agents.keys()]}"
|
|
344
|
-
)
|
|
345
|
-
return repo_agents, knowledge_agents
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
from datetime import datetime, timezone
|
|
2
|
-
from enum import Enum
|
|
3
|
-
|
|
4
|
-
from fastmcp.mcp_config import MCPConfig
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class MicroagentType(str, Enum):
|
|
9
|
-
"""Type of microagent."""
|
|
10
|
-
|
|
11
|
-
KNOWLEDGE = "knowledge" # Optional microagent, triggered by keywords
|
|
12
|
-
REPO_KNOWLEDGE = "repo" # Always active microagent
|
|
13
|
-
TASK = "task" # Special type for task microagents that require user input
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class InputMetadata(BaseModel):
|
|
17
|
-
"""Metadata for task microagent inputs."""
|
|
18
|
-
|
|
19
|
-
name: str = Field(..., description="Name of the input parameter")
|
|
20
|
-
description: str = Field(..., description="Description of the input parameter")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class MicroagentMetadata(BaseModel):
|
|
24
|
-
"""Metadata for all microagents."""
|
|
25
|
-
|
|
26
|
-
name: str = Field("default", description="Unique name of the microagent")
|
|
27
|
-
type: MicroagentType = Field(default=MicroagentType.REPO_KNOWLEDGE)
|
|
28
|
-
triggers: list[str] = [] # optional, only exists for knowledge microagents
|
|
29
|
-
inputs: list[InputMetadata] = [] # optional, only exists for task microagents
|
|
30
|
-
mcp_tools: MCPConfig | None = (
|
|
31
|
-
None # optional, for microagents that provide additional MCP tools
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class MicroagentKnowledge(BaseModel):
|
|
36
|
-
"""Represents knowledge from a triggered microagent."""
|
|
37
|
-
|
|
38
|
-
name: str = Field(description="The name of the microagent that was triggered")
|
|
39
|
-
trigger: str = Field(description="The word that triggered this microagent")
|
|
40
|
-
content: str = Field(description="The actual content/knowledge from the microagent")
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class MicroagentResponse(BaseModel):
|
|
44
|
-
"""Response model for microagents endpoint.
|
|
45
|
-
|
|
46
|
-
Note: This model only includes basic metadata that can be determined
|
|
47
|
-
without parsing microagent content. Use the separate content API
|
|
48
|
-
to get detailed microagent information.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
name: str = Field(description="The name of the microagent")
|
|
52
|
-
path: str = Field(description="The path or identifier of the microagent")
|
|
53
|
-
created_at: datetime = Field(
|
|
54
|
-
default_factory=lambda: datetime.now(timezone.utc),
|
|
55
|
-
description="Timestamp when the microagent was created",
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class MicroagentContentResponse(BaseModel):
|
|
60
|
-
"""Response model for individual microagent content endpoint."""
|
|
61
|
-
|
|
62
|
-
content: str = Field(description="The full content of the microagent")
|
|
63
|
-
path: str = Field(description="The path or identifier of the microagent")
|
|
64
|
-
triggers: list[str] = Field(
|
|
65
|
-
description="List of triggers associated with the microagent"
|
|
66
|
-
)
|
|
67
|
-
git_provider: str | None = Field(
|
|
68
|
-
None,
|
|
69
|
-
description="Git provider if the microagent is sourced from a Git repository",
|
|
70
|
-
)
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# prompt_utils.py
|
|
2
|
-
import os
|
|
3
|
-
import re
|
|
4
|
-
import sys
|
|
5
|
-
from functools import lru_cache
|
|
6
|
-
|
|
7
|
-
from jinja2 import Environment, FileSystemBytecodeCache, FileSystemLoader, Template
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def refine(text: str) -> str:
|
|
11
|
-
if sys.platform == "win32":
|
|
12
|
-
text = re.sub(
|
|
13
|
-
r"\bexecute_bash\b", "execute_powershell", text, flags=re.IGNORECASE
|
|
14
|
-
)
|
|
15
|
-
text = re.sub(
|
|
16
|
-
r"(?<!execute_)(?<!_)\bbash\b", "powershell", text, flags=re.IGNORECASE
|
|
17
|
-
)
|
|
18
|
-
return text
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@lru_cache(maxsize=64)
|
|
22
|
-
def _get_env(prompt_dir: str) -> Environment:
|
|
23
|
-
if not prompt_dir:
|
|
24
|
-
raise ValueError("prompt_dir is required")
|
|
25
|
-
# BytecodeCache avoids reparsing templates across processes
|
|
26
|
-
cache_folder = os.path.join(prompt_dir, ".jinja_cache")
|
|
27
|
-
os.makedirs(cache_folder, exist_ok=True)
|
|
28
|
-
bcc = FileSystemBytecodeCache(directory=cache_folder)
|
|
29
|
-
env = Environment(
|
|
30
|
-
loader=FileSystemLoader(prompt_dir),
|
|
31
|
-
bytecode_cache=bcc,
|
|
32
|
-
autoescape=False,
|
|
33
|
-
)
|
|
34
|
-
# Optional: expose refine as a filter so templates can use {{ text|refine }}
|
|
35
|
-
env.filters["refine"] = refine
|
|
36
|
-
return env
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@lru_cache(maxsize=256)
|
|
40
|
-
def _get_template(prompt_dir: str, template_name: str) -> Template:
|
|
41
|
-
env = _get_env(prompt_dir)
|
|
42
|
-
try:
|
|
43
|
-
return env.get_template(template_name)
|
|
44
|
-
except Exception:
|
|
45
|
-
raise FileNotFoundError(
|
|
46
|
-
f"Prompt file {os.path.join(prompt_dir, template_name)} not found"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def render_template(prompt_dir: str, template_name: str, **ctx) -> str:
|
|
51
|
-
tpl = _get_template(prompt_dir, template_name)
|
|
52
|
-
return refine(tpl.render(**ctx).strip())
|