Skip to content

Commit b489142

Browse files
Add auto-formatting with ruff format to CI
- Split ruff pre-commit hook into separate format and check steps - Add auto-commit functionality to CI for formatting changes - Ensures consistent code formatting across all PRs - Auto-formatted existing code with new ruff format settings Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 936df5c commit b489142

File tree

12 files changed

+112
-157
lines changed

12 files changed

+112
-157
lines changed

.github/workflows/precommit.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ name: Pre-commit checks
44
on:
55
push:
66
branches: ["**"] # all branches
7+
pull_request:
8+
branches: ["**"]
9+
10+
permissions:
11+
contents: write
12+
pull-requests: write
713

814
jobs:
915
pre-commit:
@@ -12,6 +18,9 @@ jobs:
1218
steps:
1319
- name: Checkout code
1420
uses: actions/checkout@v4
21+
with:
22+
token: ${{ secrets.GITHUB_TOKEN }}
23+
fetch-depth: 0
1524

1625
- name: Set up Python
1726
uses: actions/setup-python@v5
@@ -28,3 +37,10 @@ jobs:
2837
uses: pre-commit/action@v3.0.1
2938
with:
3039
extra_args: --all-files
40+
41+
- name: Auto-commit formatting changes
42+
if: failure()
43+
uses: stefanzweifel/git-auto-commit-action@v5
44+
with:
45+
commit_message: "Auto-format code with ruff\n\nCo-authored-by: openhands <openhands@all-hands.dev>"
46+
file_pattern: "*.py"

.openhands/setup.sh

100644100755
File mode changed.

.pre-commit-config.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
repos:
22
- repo: local
33
hooks:
4-
- id: ruff
5-
name: Ruff (format + lint)
4+
- id: ruff-format
5+
name: Ruff format
6+
entry: uv
7+
args: [run, ruff, format]
8+
language: system
9+
types: [python]
10+
pass_filenames: false
11+
always_run: true
12+
- id: ruff-check
13+
name: Ruff lint
614
entry: uv
715
args: [run, ruff, check, --fix, --exit-non-zero-on-fix]
816
language: system

openhands/core/agent/codeact_agent/codeact_agent.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,7 @@ def __init__(
3535
assert tool not in tools, f"{tool} is automatically included and should not be provided."
3636
super().__init__(llm=llm, tools=tools + BUILT_IN_TOOLS, env_context=env_context)
3737

38-
self.system_message: TextContent = TextContent(
39-
text=render_system_message(
40-
prompt_dir=self.prompt_dir,
41-
system_prompt_filename=system_prompt_filename,
42-
cli_mode=cli_mode
43-
))
38+
self.system_message: TextContent = TextContent(text=render_system_message(prompt_dir=self.prompt_dir, system_prompt_filename=system_prompt_filename, cli_mode=cli_mode))
4439

4540
self.max_iterations: int = 10
4641

openhands/core/context/__init__.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,4 @@
1717
from .utils import render_additional_info, render_initial_user_message, render_microagent_info, render_system_message
1818

1919

20-
__all__ = [
21-
"EnvContext",
22-
"RepositoryInfo",
23-
"RuntimeInfo",
24-
"ConversationInstructions",
25-
"MessageContext",
26-
"BaseMicroagent",
27-
"KnowledgeMicroagent",
28-
"RepoMicroagent",
29-
"MicroagentMetadata",
30-
"MicroagentType",
31-
"MicroagentKnowledge",
32-
"load_microagents_from_dir",
33-
"render_system_message",
34-
"render_initial_user_message",
35-
"render_additional_info",
36-
"render_microagent_info"
37-
]
20+
__all__ = ["EnvContext", "RepositoryInfo", "RuntimeInfo", "ConversationInstructions", "MessageContext", "BaseMicroagent", "KnowledgeMicroagent", "RepoMicroagent", "MicroagentMetadata", "MicroagentType", "MicroagentKnowledge", "load_microagents_from_dir", "render_system_message", "render_initial_user_message", "render_additional_info", "render_microagent_info"]

openhands/core/context/env_context.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
from pydantic import BaseModel, Field
32

43
from openhands.core.context.utils.prompt import render_microagent_info

openhands/core/context/microagents/microagent.py

Lines changed: 31 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ class BaseMicroagent(BaseModel):
2626
type: MicroagentType
2727

2828
PATH_TO_THIRD_PARTY_MICROAGENT_NAME: ClassVar[dict[str, str]] = {
29-
'.cursorrules': 'cursorrules',
30-
'agents.md': 'agents',
31-
'agent.md': 'agents',
29+
".cursorrules": "cursorrules",
30+
"agents.md": "agents",
31+
"agent.md": "agents",
3232
}
3333

3434
@classmethod
35-
def _handle_third_party(
36-
cls, path: Path, file_content: str
37-
) -> Union['RepoMicroagent', None]:
35+
def _handle_third_party(cls, path: Path, file_content: str) -> Union["RepoMicroagent", None]:
3836
# Determine the agent name based on file type
3937
microagent_name = cls.PATH_TO_THIRD_PARTY_MICROAGENT_NAME.get(path.name.lower())
4038

@@ -56,7 +54,7 @@ def load(
5654
path: Union[str, Path],
5755
microagent_dir: Path | None = None,
5856
file_content: str | None = None,
59-
) -> 'BaseMicroagent':
57+
) -> "BaseMicroagent":
6058
"""Load a microagent from a markdown file with frontmatter.
6159
6260
The agent's name is derived from its path relative to the microagent_dir.
@@ -68,21 +66,19 @@ def load(
6866
derived_name = None
6967
if microagent_dir is not None:
7068
# Special handling for files which are not in microagent_dir
71-
derived_name = cls.PATH_TO_THIRD_PARTY_MICROAGENT_NAME.get(
72-
path.name.lower()
73-
) or str(path.relative_to(microagent_dir).with_suffix(''))
69+
derived_name = cls.PATH_TO_THIRD_PARTY_MICROAGENT_NAME.get(path.name.lower()) or str(path.relative_to(microagent_dir).with_suffix(""))
7470

7571
# Only load directly from path if file_content is not provided
7672
if file_content is None:
7773
with open(path) as f:
7874
file_content = f.read()
7975

8076
# Legacy repo instructions are stored in .openhands_instructions
81-
if path.name == '.openhands_instructions':
77+
if path.name == ".openhands_instructions":
8278
return RepoMicroagent(
83-
name='repo_legacy',
79+
name="repo_legacy",
8480
content=file_content,
85-
metadata=MicroagentMetadata(name='repo_legacy'),
81+
metadata=MicroagentMetadata(name="repo_legacy"),
8682
source=str(path),
8783
type=MicroagentType.REPO_KNOWLEDGE,
8884
)
@@ -100,31 +96,24 @@ def load(
10096
metadata_dict = loaded.metadata or {}
10197

10298
# Ensure version is always a string (YAML may parse numeric versions as integers)
103-
if 'version' in metadata_dict and not isinstance(metadata_dict['version'], str):
104-
metadata_dict['version'] = str(metadata_dict['version'])
99+
if "version" in metadata_dict and not isinstance(metadata_dict["version"], str):
100+
metadata_dict["version"] = str(metadata_dict["version"])
105101

106102
try:
107103
metadata = MicroagentMetadata.model_validate(metadata_dict)
108104

109105
# Validate MCP tools configuration if present
110106
if metadata.mcp_tools:
111107
if metadata.mcp_tools.sse_servers:
112-
logger.warning(
113-
f'Microagent {metadata.name} has SSE servers. Only stdio servers are currently supported.'
114-
)
108+
logger.warning(f"Microagent {metadata.name} has SSE servers. Only stdio servers are currently supported.")
115109

116110
if not metadata.mcp_tools.stdio_servers:
117-
raise MicroagentValidationError(
118-
f'Microagent {metadata.name} has MCP tools configuration but no stdio servers. '
119-
'Only stdio servers are currently supported.'
120-
)
111+
raise MicroagentValidationError(f"Microagent {metadata.name} has MCP tools configuration but no stdio servers. Only stdio servers are currently supported.")
121112
except Exception as e:
122113
# Provide more detailed error message for validation errors
123-
error_msg = f'Error validating microagent metadata in {path.name}: {str(e)}'
124-
if 'type' in metadata_dict and metadata_dict['type'] not in [
125-
t.value for t in MicroagentType
126-
]:
127-
valid_types = ', '.join([f'"{t.value}"' for t in MicroagentType])
114+
error_msg = f"Error validating microagent metadata in {path.name}: {str(e)}"
115+
if "type" in metadata_dict and metadata_dict["type"] not in [t.value for t in MicroagentType]:
116+
valid_types = ", ".join([f'"{t.value}"' for t in MicroagentType])
128117
error_msg += f'. Invalid "type" value: "{metadata_dict["type"]}". Valid types are: {valid_types}'
129118
raise MicroagentValidationError(error_msg) from e
130119

@@ -143,7 +132,7 @@ def load(
143132
if metadata.inputs:
144133
inferred_type = MicroagentType.TASK
145134
# Add a trigger for the agent name if not already present
146-
trigger = f'/{metadata.name}'
135+
trigger = f"/{metadata.name}"
147136
if not metadata.triggers or trigger not in metadata.triggers:
148137
if not metadata.triggers:
149138
metadata.triggers = [trigger]
@@ -158,7 +147,7 @@ def load(
158147

159148
if inferred_type not in subclass_map:
160149
# This should theoretically not happen with the logic above
161-
raise ValueError(f'Could not determine microagent type for: {path}')
150+
raise ValueError(f"Could not determine microagent type for: {path}")
162151

163152
# Use derived_name if available (from relative path), otherwise fallback to metadata.name
164153
agent_name = derived_name if derived_name is not None else metadata.name
@@ -186,7 +175,7 @@ class KnowledgeMicroagent(BaseMicroagent):
186175
def __init__(self, **data):
187176
super().__init__(**data)
188177
if self.type not in [MicroagentType.KNOWLEDGE, MicroagentType.TASK]:
189-
raise ValueError('KnowledgeMicroagent must have type KNOWLEDGE or TASK')
178+
raise ValueError("KnowledgeMicroagent must have type KNOWLEDGE or TASK")
190179

191180
def match_trigger(self, message: str) -> str | None:
192181
"""Match a trigger in the message.
@@ -220,9 +209,7 @@ class RepoMicroagent(BaseMicroagent):
220209
def __init__(self, **data):
221210
super().__init__(**data)
222211
if self.type != MicroagentType.REPO_KNOWLEDGE:
223-
raise ValueError(
224-
f'RepoMicroagent initialized with incorrect type: {self.type}'
225-
)
212+
raise ValueError(f"RepoMicroagent initialized with incorrect type: {self.type}")
226213

227214

228215
class TaskMicroagent(KnowledgeMicroagent):
@@ -235,9 +222,7 @@ class TaskMicroagent(KnowledgeMicroagent):
235222
def __init__(self, **data):
236223
super().__init__(**data)
237224
if self.type != MicroagentType.TASK:
238-
raise ValueError(
239-
f'TaskMicroagent initialized with incorrect type: {self.type}'
240-
)
225+
raise ValueError(f"TaskMicroagent initialized with incorrect type: {self.type}")
241226

242227
# Append a prompt to ask for missing variables
243228
self._append_missing_variables_prompt()
@@ -256,7 +241,7 @@ def extract_variables(self, content: str) -> list[str]:
256241
257242
Variables are in the format ${variable_name}.
258243
"""
259-
pattern = r'\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
244+
pattern = r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}"
260245
matches = re.findall(pattern, content)
261246
return matches
262247

@@ -267,7 +252,7 @@ def requires_user_input(self) -> bool:
267252
"""
268253
# Check if the content contains any variables
269254
variables = self.extract_variables(self.content)
270-
logger.debug(f'This microagent requires user input: {variables}')
255+
logger.debug(f"This microagent requires user input: {variables}")
271256
return len(variables) > 0
272257

273258
@property
@@ -296,18 +281,18 @@ def load_microagents_from_dir(
296281
knowledge_agents = {}
297282

298283
# Load all agents from microagents directory
299-
logger.debug(f'Loading agents from {microagent_dir}')
284+
logger.debug(f"Loading agents from {microagent_dir}")
300285

301286
# Always check for .cursorrules and AGENTS.md files in repo root, regardless of whether microagents_dir exists
302287
special_files = []
303288
repo_root = microagent_dir.parent.parent
304289

305290
# Check for .cursorrules
306-
if (repo_root / '.cursorrules').exists():
307-
special_files.append(repo_root / '.cursorrules')
291+
if (repo_root / ".cursorrules").exists():
292+
special_files.append(repo_root / ".cursorrules")
308293

309294
# Check for AGENTS.md (case-insensitive)
310-
for agents_filename in ['AGENTS.md', 'agents.md', 'AGENT.md', 'agent.md']:
295+
for agents_filename in ["AGENTS.md", "agents.md", "AGENT.md", "agent.md"]:
311296
agents_path = repo_root / agents_filename
312297
if agents_path.exists():
313298
special_files.append(agents_path)
@@ -316,7 +301,7 @@ def load_microagents_from_dir(
316301
# Collect .md files from microagents directory if it exists
317302
md_files = []
318303
if microagent_dir.exists():
319-
md_files = [f for f in microagent_dir.rglob('*.md') if f.name != 'README.md']
304+
md_files = [f for f in microagent_dir.rglob("*.md") if f.name != "README.md"]
320305

321306
# Process all files in one loop
322307
for file in chain(special_files, md_files):
@@ -329,15 +314,12 @@ def load_microagents_from_dir(
329314
knowledge_agents[agent.name] = agent
330315
except MicroagentValidationError as e:
331316
# For validation errors, include the original exception
332-
error_msg = f'Error loading microagent from {file}: {str(e)}'
317+
error_msg = f"Error loading microagent from {file}: {str(e)}"
333318
raise MicroagentValidationError(error_msg) from e
334319
except Exception as e:
335320
# For other errors, wrap in a ValueError with detailed message
336-
error_msg = f'Error loading microagent from {file}: {str(e)}'
321+
error_msg = f"Error loading microagent from {file}: {str(e)}"
337322
raise ValueError(error_msg) from e
338323

339-
logger.debug(
340-
f'Loaded {len(repo_agents) + len(knowledge_agents)} microagents: '
341-
f'{[*repo_agents.keys(), *knowledge_agents.keys()]}'
342-
)
324+
logger.debug(f"Loaded {len(repo_agents) + len(knowledge_agents)} microagents: {[*repo_agents.keys(), *knowledge_agents.keys()]}")
343325
return repo_agents, knowledge_agents

openhands/core/context/utils/prompt.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def refine(text: str) -> str:
1313
text = re.sub(r"(?<!execute_)(?<!_)\bbash\b", "powershell", text, flags=re.IGNORECASE)
1414
return text
1515

16+
1617
@lru_cache(maxsize=64)
1718
def _get_env(prompt_dir: str) -> Environment:
1819
if not prompt_dir:
@@ -30,6 +31,7 @@ def _get_env(prompt_dir: str) -> Environment:
3031
env.filters["refine"] = refine
3132
return env
3233

34+
3335
@lru_cache(maxsize=256)
3436
def _get_template(prompt_dir: str, template_name: str) -> Template:
3537
env = _get_env(prompt_dir)
@@ -38,19 +40,24 @@ def _get_template(prompt_dir: str, template_name: str) -> Template:
3840
except Exception:
3941
raise FileNotFoundError(f"Prompt file {os.path.join(prompt_dir, template_name)} not found")
4042

43+
4144
def render_template(prompt_dir: str, template_name: str, **ctx) -> str:
4245
tpl = _get_template(prompt_dir, template_name)
4346
return refine(tpl.render(**ctx).strip())
4447

48+
4549
# Convenience wrappers keeping old names/semantics
4650
def render_system_message(prompt_dir: str, system_prompt_filename: str = "system_prompt.j2", **ctx) -> str:
4751
return render_template(prompt_dir, system_prompt_filename, **ctx)
4852

53+
4954
def render_initial_user_message(prompt_dir: str, **ctx) -> str:
5055
return render_template(prompt_dir, "user_prompt.j2", **ctx)
5156

57+
5258
def render_additional_info(prompt_dir: str, **ctx) -> str:
5359
return render_template(prompt_dir, "additional_info.j2", **ctx)
5460

61+
5562
def render_microagent_info(prompt_dir: str, **ctx) -> str:
5663
return render_template(prompt_dir, "microagent_info.j2", **ctx)

openhands/core/event/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
N_CHAR_PREVIEW = 500
1717

18+
1819
class EventBase(BaseModel, ABC):
1920
"""Base class for all events."""
2021

@@ -56,7 +57,7 @@ def __str__(self) -> str:
5657
content_preview = " ".join(text_parts)
5758
# Truncate long content for display
5859
if len(content_preview) > N_CHAR_PREVIEW:
59-
content_preview = content_preview[:N_CHAR_PREVIEW-3] + "..."
60+
content_preview = content_preview[: N_CHAR_PREVIEW - 3] + "..."
6061
return f"{base_str}\n {llm_message.role}: {content_preview}"
6162
else:
6263
return f"{base_str}\n {llm_message.role}: [no text content]"

openhands/core/event/llm_convertible.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __str__(self) -> str:
102102
if text_parts:
103103
content_preview = " ".join(text_parts)
104104
if len(content_preview) > N_CHAR_PREVIEW:
105-
content_preview = content_preview[:N_CHAR_PREVIEW-3] + "..."
105+
content_preview = content_preview[: N_CHAR_PREVIEW - 3] + "..."
106106
microagent_info = f" [Microagents: {', '.join(self.activated_microagents)}]" if self.activated_microagents else ""
107107
return f"{base_str}\n {self.llm_message.role}: {content_preview}{microagent_info}"
108108
else:

0 commit comments

Comments
 (0)