Skip to content

Commit

Permalink
Merge branch 'main' into ALL-963/saas-billing
Browse files Browse the repository at this point in the history
  • Loading branch information
tofarr committed Feb 12, 2025
2 parents c37fda5 + ba599c7 commit f9a38a3
Show file tree
Hide file tree
Showing 24 changed files with 1,322 additions and 276 deletions.
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
"@playwright/test": "^1.50.1",
"@react-router/dev": "^7.1.5",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.66.0",
"@tanstack/eslint-plugin-query": "^5.66.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function GitHubRepositorySelector({

const allRepositories: GitHubRepository[] = [
...publicRepositories.filter(
(repo) => !publicRepositories.find((r) => r.id === repo.id),
(repo) => !userRepositories.find((r) => r.id === repo.id),
),
...userRepositories,
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router";
import { I18nKey } from "#/i18n/declaration";
import NewProjectIcon from "#/icons/new-project.svg?react";
import { TooltipButton } from "./tooltip-button";
Expand All @@ -10,12 +9,7 @@ interface ExitProjectButtonProps {

export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
const { t } = useTranslation();
const location = useLocation();
const startNewProject = t(I18nKey.PROJECT$START_NEW);

// Only show the button in the conversations page
if (!location.pathname.startsWith("/conversations")) return null;

return (
<TooltipButton
tooltip={startNewProject}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { LoadingSpinner } from "../../loading-spinner";
import { ModalBackdrop } from "../modal-backdrop";
import { SettingsForm } from "./settings-form";
import { Settings } from "#/types/settings";
import { DEFAULT_SETTINGS } from "#/services/settings";

interface SettingsModalProps {
settings: Settings;
settings?: Settings;
onClose: () => void;
}

Expand Down Expand Up @@ -38,7 +39,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
)}
{aiConfigOptions.data && (
<SettingsForm
settings={settings}
settings={settings || DEFAULT_SETTINGS}
models={aiConfigOptions.data?.models}
agents={aiConfigOptions.data?.agents}
securityAnalyzers={aiConfigOptions.data?.securityAnalyzers}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/hooks/query/use-settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import posthog from "posthog-js";
import { DEFAULT_SETTINGS } from "#/services/settings";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
import { useConfig } from "#/hooks/query/use-config";
Expand Down Expand Up @@ -31,10 +30,11 @@ export const useSettings = () => {
const query = useQuery({
queryKey: ["settings"],
queryFn: getSettingsQueryFn,
initialData: DEFAULT_SETTINGS,
staleTime: 0,
retry: false,
enabled: config?.APP_MODE !== "saas" || githubTokenIsSet,
// Only retry if the error is not a 404 because we
// would want to show the modal immediately if the
// settings are not found
retry: (_, error) => error.status !== 404,
meta: {
disableToast: true,
},
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/types/core/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export interface FileReadAction extends OpenHandsActionEvent<"read"> {
args: {
path: string;
thought: string;
translated_ipython_code: string | null;
security_risk: ActionSecurityRisk | null;
impl_source?: string;
view_range?: number[] | null;
};
}

Expand All @@ -100,7 +102,18 @@ export interface FileEditAction extends OpenHandsActionEvent<"edit"> {
source: "agent";
args: {
path: string;
translated_ipython_code: string;
command?: string;
file_text?: string | null;
view_range?: number[] | null;
old_str?: string | null;
new_str?: string | null;
insert_line?: number | null;
content?: string;
start?: number;
end?: number;
thought: string;
security_risk: ActionSecurityRisk | null;
impl_source?: string;
};
}

Expand Down
28 changes: 14 additions & 14 deletions openhands/agenthub/codeact_agent/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
FunctionCallNotExistsError,
FunctionCallValidationError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
Action,
AgentDelegateAction,
Expand Down Expand Up @@ -541,26 +540,27 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
raise FunctionCallValidationError(
f'Missing required argument "path" in tool call {tool_call.function.name}'
)
path = arguments['path']
command = arguments['command']
other_kwargs = {
k: v for k, v in arguments.items() if k not in ['command', 'path']
}

# We implement this in agent_skills, which can be used via Jupyter
# convert tool_call.function.arguments to kwargs that can be passed to file_editor
code = f'print(file_editor(**{arguments}))'
logger.debug(
f'TOOL CALL: str_replace_editor -> file_editor with code: {code}'
)

if arguments['command'] == 'view':
if command == 'view':
action = FileReadAction(
path=arguments['path'],
translated_ipython_code=code,
path=path,
impl_source=FileReadSource.OH_ACI,
view_range=other_kwargs.get('view_range', None),
)
else:
if 'view_range' in other_kwargs:
# Remove view_range from other_kwargs since it is not needed for FileEditAction
other_kwargs.pop('view_range')
action = FileEditAction(
path=arguments['path'],
content='', # dummy value -- we don't need it
translated_ipython_code=code,
path=path,
command=command,
impl_source=FileEditSource.OH_ACI,
**other_kwargs,
)
elif tool_call.function.name == 'browser':
if 'code' not in arguments:
Expand Down
16 changes: 14 additions & 2 deletions openhands/core/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ class NoColorFormatter(logging.Formatter):

def format(self, record: logging.LogRecord) -> str:
# Create a deep copy of the record to avoid modifying the original
new_record: logging.LogRecord = copy.deepcopy(record)
new_record = _fix_record(record)

# Strip ANSI color codes from the message
new_record.msg = strip_ansi(new_record.msg)

Expand Down Expand Up @@ -130,7 +131,18 @@ def format(self, record):
return f'{msg}'
else:
return record.msg
return super().format(record)

new_record = _fix_record(record)
return super().format(new_record)


def _fix_record(record: logging.LogRecord):
new_record = copy.copy(record)
# The formatter expects non boolean values, and will raise an exception if there is a boolean - so we fix these
if new_record.exc_info is True and not new_record.exc_text: # type: ignore
new_record.exc_info = sys.exc_info() # type: ignore
new_record.stack_info = None # type: ignore
return new_record


file_formatter = NoColorFormatter(
Expand Down
76 changes: 63 additions & 13 deletions openhands/events/action/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class FileReadAction(Action):
runnable: ClassVar[bool] = True
security_risk: ActionSecurityRisk | None = None
impl_source: FileReadSource = FileReadSource.DEFAULT
translated_ipython_code: str = '' # translated openhands-aci IPython code
view_range: list[int] | None = None # ONLY used in OH_ACI mode

@property
def message(self) -> str:
Expand Down Expand Up @@ -60,29 +60,79 @@ def __repr__(self) -> str:

@dataclass
class FileEditAction(Action):
"""Edits a file by provided a draft at a given path.
Can be set to edit specific lines using start and end (1-index, inclusive) if the file is too long.
Default lines 1:-1 (whole file).
If start is set to -1, the FileEditAction will simply append the content to the file.
"""Edits a file using various commands including view, create, str_replace, insert, and undo_edit.
This class supports two main modes of operation:
1. LLM-based editing (impl_source = FileEditSource.LLM_BASED_EDIT)
2. ACI-based editing (impl_source = FileEditSource.OH_ACI)
Attributes:
path (str): The path to the file being edited. Works for both LLM-based and OH_ACI editing.
OH_ACI only arguments:
command (str): The editing command to be performed (view, create, str_replace, insert, undo_edit, write).
file_text (str): The content of the file to be created (used with 'create' command in OH_ACI mode).
old_str (str): The string to be replaced (used with 'str_replace' command in OH_ACI mode).
new_str (str): The string to replace old_str (used with 'str_replace' and 'insert' commands in OH_ACI mode).
insert_line (int): The line number after which to insert new_str (used with 'insert' command in OH_ACI mode).
LLM-based editing arguments:
content (str): The content to be written or edited in the file (used in LLM-based editing and 'write' command).
start (int): The starting line for editing (1-indexed, inclusive). Default is 1.
end (int): The ending line for editing (1-indexed, inclusive). Default is -1 (end of file).
thought (str): The reasoning behind the edit action.
action (str): The type of action being performed (always ActionType.EDIT).
runnable (bool): Indicates if the action can be executed (always True).
security_risk (ActionSecurityRisk | None): Indicates any security risks associated with the action.
impl_source (FileEditSource): The source of the implementation (LLM_BASED_EDIT or OH_ACI).
Usage:
- For LLM-based editing: Use path, content, start, and end attributes.
- For ACI-based editing: Use path, command, and the appropriate attributes for the specific command.
Note:
- If start is set to -1 in LLM-based editing, the content will be appended to the file.
- The 'write' command behaves similarly to LLM-based editing, using content, start, and end attributes.
"""

path: str
content: str

# OH_ACI arguments
command: str = ''
file_text: str | None = None
old_str: str | None = None
new_str: str | None = None
insert_line: int | None = None

# LLM-based editing arguments
content: str = ''
start: int = 1
end: int = -1

# Shared arguments
thought: str = ''
action: str = ActionType.EDIT
runnable: ClassVar[bool] = True
security_risk: ActionSecurityRisk | None = None
impl_source: FileEditSource = FileEditSource.LLM_BASED_EDIT
translated_ipython_code: str = ''
impl_source: FileEditSource = FileEditSource.OH_ACI

def __repr__(self) -> str:
ret = '**FileEditAction**\n'
ret += f'Thought: {self.thought}\n'
ret += f'Range: [L{self.start}:L{self.end}]\n'
ret += f'Path: [{self.path}]\n'
ret += f'Content:\n```\n{self.content}\n```\n'
ret += f'Thought: {self.thought}\n'

if self.impl_source == FileEditSource.LLM_BASED_EDIT:
ret += f'Range: [L{self.start}:L{self.end}]\n'
ret += f'Content:\n```\n{self.content}\n```\n'
else: # OH_ACI mode
ret += f'Command: {self.command}\n'
if self.command == 'create':
ret += f'Created File with Text:\n```\n{self.file_text}\n```\n'
elif self.command == 'str_replace':
ret += f'Old String: ```\n{self.old_str}\n```\n'
ret += f'New String: ```\n{self.new_str}\n```\n'
elif self.command == 'insert':
ret += f'Insert Line: {self.insert_line}\n'
ret += f'New String: ```\n{self.new_str}\n```\n'
elif self.command == 'undo_edit':
ret += 'Undo Edit\n'
# We ignore "view" command because it will be mapped to a FileReadAction
return ret
17 changes: 11 additions & 6 deletions openhands/events/observation/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,18 @@ class FileEditObservation(Observation):
The observation includes both the old and new content of the file, and can
generate a diff visualization showing the changes. The diff is computed lazily
and cached to improve performance.
The .content property can either be:
- Git diff in LLM-based editing mode
- the rendered message sent to the LLM in OH_ACI mode (e.g., "The file /path/to/file.txt is created with the provided content.")
"""

path: str
prev_exist: bool
old_content: str
new_content: str
path: str = ''
prev_exist: bool = False
old_content: str | None = None
new_content: str | None = None
observation: str = ObservationType.EDIT
impl_source: FileEditSource = FileEditSource.LLM_BASED_EDIT
formatted_output_and_error: str = ''
_diff_cache: str | None = None # Cache for the diff visualization

@property
Expand All @@ -75,6 +78,8 @@ def get_edit_groups(self, n_context_lines: int = 2) -> list[dict[str, list[str]]
Returns:
A list of edit groups, where each group contains before/after edits.
"""
if self.old_content is None or self.new_content is None:
return []
old_lines = self.old_content.split('\n')
new_lines = self.new_content.split('\n')
# Borrowed from difflib.unified_diff to directly parse into structured format
Expand Down Expand Up @@ -173,7 +178,7 @@ def visualize_diff(
def __str__(self) -> str:
"""Get a string representation of the file edit observation."""
if self.impl_source == FileEditSource.OH_ACI:
return self.formatted_output_and_error
return self.content

if not self.prev_exist:
assert (
Expand Down
Loading

0 comments on commit f9a38a3

Please sign in to comment.