openhands 0.0.0__py3-none-any.whl → 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openhands might be problematic. Click here for more details.

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