mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
25 Commits
fix-docs-i
...
allow-mess
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a2e7d6270 | ||
|
|
f5c58adaf7 | ||
|
|
c6cb025afe | ||
|
|
b0030d3a2b | ||
|
|
d76477099c | ||
|
|
3e3b2aaa5c | ||
|
|
1f8aa93843 | ||
|
|
34920ea04e | ||
|
|
f5aeb47a72 | ||
|
|
c830177207 | ||
|
|
e4ccd4057d | ||
|
|
cb8214676e | ||
|
|
fdfb7308b8 | ||
|
|
4785de91b0 | ||
|
|
effd2b7d06 | ||
|
|
608dd8f2c2 | ||
|
|
6d0c03509e | ||
|
|
3e1070bbe9 | ||
|
|
2045350720 | ||
|
|
5f83d4cf9a | ||
|
|
d5a996a9e1 | ||
|
|
b0d38bbeb8 | ||
|
|
ed50b3ee8f | ||
|
|
4e5ed36213 | ||
|
|
9060452af6 |
6
.github/workflows/openhands-resolver.yml
vendored
6
.github/workflows/openhands-resolver.yml
vendored
@@ -74,13 +74,13 @@ jobs:
|
||||
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
|
||||
)
|
||||
)
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: useblacksmith/setup-python@v6
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
contains(github.event.review.body, '@openhands-agent-exp')
|
||||
)
|
||||
)
|
||||
uses: useblacksmith/cache@v5
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
|
||||
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }}
|
||||
|
||||
@@ -402,5 +402,26 @@
|
||||
"theme.unlistedContent.message": {
|
||||
"message": "Cette page n'est pas répertoriée. Les moteurs de recherche ne l'indexeront pas, et seuls les utilisateurs ayant un lien direct peuvent y accéder.",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web, call APIs, and yes-even copy code snippets from StackOverflow.": {
|
||||
"message": "Utilisez l'IA pour gérer les tâches répétitives de votre backlog. Nos agents disposent des mêmes outils qu'un développeur humain : ils peuvent modifier du code, exécuter des commandes, naviguer sur le web, appeler des API et même copier des extraits de code depuis StackOverflow."
|
||||
},
|
||||
"Get started with OpenHands.": {
|
||||
"message": "Commencer avec OpenHands"
|
||||
},
|
||||
"Most Popular Links": {
|
||||
"message": "Liens Populaires"
|
||||
},
|
||||
"Customizing OpenHands to a repository": {
|
||||
"message": "Personnaliser OpenHands pour un dépôt"
|
||||
},
|
||||
"Integrating OpenHands with Github": {
|
||||
"message": "Intégrer OpenHands avec Github"
|
||||
},
|
||||
"Recommended models to use": {
|
||||
"message": "Modèles recommandés"
|
||||
},
|
||||
"Connecting OpenHands to your filesystem": {
|
||||
"message": "Connecter OpenHands à votre système de fichiers"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,5 +402,26 @@
|
||||
"theme.unlistedContent.message": {
|
||||
"message": "此页面未列出。搜索引擎不会对其索引,只有拥有直接链接的用户才能访问。",
|
||||
"description": "The unlisted content banner message"
|
||||
},
|
||||
"Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web, call APIs, and yes-even copy code snippets from StackOverflow.": {
|
||||
"message": "使用AI处理您积压的工作。我们的代理拥有与人类开发者相同的工具:它们可以修改代码、运行命令、浏览网页、调用API,甚至从StackOverflow复制代码片段。"
|
||||
},
|
||||
"Get started with OpenHands.": {
|
||||
"message": "开始使用OpenHands"
|
||||
},
|
||||
"Most Popular Links": {
|
||||
"message": "热门链接"
|
||||
},
|
||||
"Customizing OpenHands to a repository": {
|
||||
"message": "为仓库定制OpenHands"
|
||||
},
|
||||
"Integrating OpenHands with Github": {
|
||||
"message": "将OpenHands与Github集成"
|
||||
},
|
||||
"Recommended models to use": {
|
||||
"message": "推荐使用的模型"
|
||||
},
|
||||
"Connecting OpenHands to your filesystem": {
|
||||
"message": "将OpenHands连接到您的文件系统"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,19 @@ export function HomepageHeader() {
|
||||
padding: '0rem 0rem 1rem'
|
||||
}}>
|
||||
<p style={{ margin: '0' }}>
|
||||
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web,
|
||||
call APIs, and yes-even copy code snippets from StackOverflow.
|
||||
<Translate>
|
||||
Use AI to tackle the toil in your backlog. Our agents have all the same tools as a human developer: they can modify code, run commands, browse the web,
|
||||
call APIs, and yes-even copy code snippets from StackOverflow.
|
||||
</Translate>
|
||||
<br/>
|
||||
<Link to="https://docs.all-hands.dev/modules/usage/installation"
|
||||
<Link to="/modules/usage/installation"
|
||||
style={{
|
||||
textDecoration: 'underline',
|
||||
display: 'inline-block',
|
||||
marginTop: '0.5rem'
|
||||
}}
|
||||
>
|
||||
Get started with OpenHands.
|
||||
<Translate>Get started with OpenHands.</Translate>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import Layout from '@theme/Layout';
|
||||
import { HomepageHeader } from '../components/HomepageHeader/HomepageHeader';
|
||||
import { translate } from '@docusaurus/Translate';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import Link from '@docusaurus/Link';
|
||||
import { Demo } from "../components/Demo/Demo";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
@@ -21,12 +23,28 @@ export default function Home(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'center', padding: '0.5rem 2rem 1.5rem' }}>
|
||||
<h2>Most Popular Links</h2>
|
||||
<h2><Translate>Most Popular Links</Translate></h2>
|
||||
<ul style={{ listStyleType: 'none'}}>
|
||||
<li><a href="/modules/usage/prompting/microagents-repo">Customizing OpenHands to a repository</a></li>
|
||||
<li><a href="/modules/usage/how-to/github-action">Integrating OpenHands with Github</a></li>
|
||||
<li><a href="/modules/usage/llms#model-recommendations">Recommended models to use</a></li>
|
||||
<li><a href="/modules/usage/runtimes#connecting-to-your-filesystem">Connecting OpenHands to your filesystem</a></li>
|
||||
<li>
|
||||
<Link to="/modules/usage/prompting/microagents-repo">
|
||||
<Translate>Customizing OpenHands to a repository</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/modules/usage/how-to/github-action">
|
||||
<Translate>Integrating OpenHands with Github</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/modules/usage/llms#model-recommendations">
|
||||
<Translate>Recommended models to use</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/modules/usage/runtimes#connecting-to-your-filesystem">
|
||||
<Translate>Connecting OpenHands to your filesystem</Translate>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function ActionSuggestions({
|
||||
suggestion={{
|
||||
label: "Push to Branch",
|
||||
value:
|
||||
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request.",
|
||||
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("push_to_branch_button_clicked");
|
||||
@@ -51,7 +51,7 @@ export function ActionSuggestions({
|
||||
suggestion={{
|
||||
label: "Push & Create PR",
|
||||
value:
|
||||
"Please push the changes to GitHub and open a pull request.",
|
||||
"Please push the changes to GitHub and open a pull request. Please use the exact SAME branch name as the one you are currently on.",
|
||||
}}
|
||||
onClick={(value) => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
|
||||
@@ -13,7 +13,10 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
import { FeedbackModal } from "../feedback/feedback-modal";
|
||||
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
||||
import { TypingIndicator } from "./typing-indicator";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { Messages } from "./messages";
|
||||
import { ChatSuggestions } from "./chat-suggestions";
|
||||
import { ActionSuggestions } from "./action-suggestions";
|
||||
@@ -34,7 +37,7 @@ function getEntryPoint(
|
||||
}
|
||||
|
||||
export function ChatInterface() {
|
||||
const { send, isLoadingMessages } = useWsClient();
|
||||
const { send, isLoadingMessages, status, pendingMessages } = useWsClient();
|
||||
const dispatch = useDispatch();
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
|
||||
@@ -54,6 +57,9 @@ export function ChatInterface() {
|
||||
const params = useParams();
|
||||
const { mutate: getTrajectory } = useGetTrajectory();
|
||||
|
||||
const isClientDisconnected = status === WsClientProviderStatus.DISCONNECTED;
|
||||
const hasPendingMessages = pendingMessages.length > 0;
|
||||
|
||||
const handleSendMessage = async (content: string, files: File[]) => {
|
||||
if (messages.length === 0) {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
@@ -76,7 +82,15 @@ export function ChatInterface() {
|
||||
const timestamp = new Date().toISOString();
|
||||
const pending = true;
|
||||
dispatch(addUserMessage({ content, imageUrls, timestamp, pending }));
|
||||
send(createChatMessage(content, imageUrls, timestamp));
|
||||
|
||||
// Create and send the chat message
|
||||
const chatMessage = createChatMessage(content, imageUrls, timestamp);
|
||||
send(chatMessage);
|
||||
|
||||
// Send the agent state change event immediately
|
||||
// The backend will handle the ordering and queueing
|
||||
send(generateAgentStateChangeEvent(AgentState.RUNNING));
|
||||
|
||||
setMessageToSend(null);
|
||||
};
|
||||
|
||||
@@ -131,8 +145,20 @@ export function ChatInterface() {
|
||||
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
|
||||
>
|
||||
{isLoadingMessages && (
|
||||
<div className="flex justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<LoadingSpinner size="small" />
|
||||
{isClientDisconnected && (
|
||||
<div className="text-sm text-neutral-400">
|
||||
Waiting for client to become ready...
|
||||
{hasPendingMessages && (
|
||||
<div className="text-xs text-neutral-500 mt-1">
|
||||
{pendingMessages.length} message
|
||||
{pendingMessages.length !== 1 ? "s" : ""} will be sent when
|
||||
connected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -179,7 +205,7 @@ export function ChatInterface() {
|
||||
onSubmit={handleSendMessage}
|
||||
onStop={handleStop}
|
||||
isDisabled={
|
||||
curAgentState === AgentState.LOADING ||
|
||||
// Allow input even when loading, but not during confirmation
|
||||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
|
||||
}
|
||||
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
|
||||
|
||||
@@ -22,10 +22,11 @@ export function AgentStatusBar() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curStatusMessage } = useSelector((state: RootState) => state.status);
|
||||
const { status } = useWsClient();
|
||||
const { status, pendingMessages } = useWsClient();
|
||||
const { notify } = useNotification();
|
||||
|
||||
const [statusMessage, setStatusMessage] = React.useState<string>("");
|
||||
const hasPendingMessages = pendingMessages.length > 0;
|
||||
|
||||
const updateStatusMessage = () => {
|
||||
let message = curStatusMessage.message || "";
|
||||
@@ -71,7 +72,13 @@ export function AgentStatusBar() {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||
setStatusMessage("Connecting...");
|
||||
if (hasPendingMessages) {
|
||||
setStatusMessage(
|
||||
`Connecting... (${pendingMessages.length} pending message${pendingMessages.length !== 1 ? "s" : ""})`,
|
||||
);
|
||||
} else {
|
||||
setStatusMessage("Connecting...");
|
||||
}
|
||||
} else {
|
||||
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
|
||||
if (notificationStates.includes(curAgentState)) {
|
||||
@@ -87,7 +94,7 @@ export function AgentStatusBar() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [curAgentState, notify, t]);
|
||||
}, [curAgentState, status, pendingMessages.length, notify, t]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
|
||||
@@ -49,12 +49,14 @@ interface UseWsClient {
|
||||
isLoadingMessages: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
send: (event: Record<string, unknown>) => void;
|
||||
pendingMessages: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
status: WsClientProviderStatus.DISCONNECTED,
|
||||
isLoadingMessages: true,
|
||||
events: [],
|
||||
pendingMessages: [],
|
||||
send: () => {
|
||||
throw new Error("not connected");
|
||||
},
|
||||
@@ -109,26 +111,43 @@ export function WsClientProvider({
|
||||
WsClientProviderStatus.DISCONNECTED,
|
||||
);
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const [pendingMessages, setPendingMessages] = React.useState<
|
||||
Record<string, unknown>[]
|
||||
>([]);
|
||||
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
|
||||
|
||||
const messageRateHandler = useRate({ threshold: 250 });
|
||||
|
||||
// Private function to queue messages for later sending
|
||||
const queueMessage = (event: Record<string, unknown>) => {
|
||||
EventLogger.info(`Queueing message: ${JSON.stringify(event)}`);
|
||||
setPendingMessages((prev) => [...prev, event]);
|
||||
};
|
||||
|
||||
function send(event: Record<string, unknown>) {
|
||||
if (!sioRef.current) {
|
||||
EventLogger.error("WebSocket is not connected.");
|
||||
EventLogger.info("WebSocket is not connected, queueing message");
|
||||
queueMessage(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the message to the backend
|
||||
EventLogger.info(`Sending message: ${JSON.stringify(event)}`);
|
||||
sioRef.current.emit("oh_action", event);
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
setStatus(WsClientProviderStatus.CONNECTED);
|
||||
EventLogger.info(
|
||||
`WebSocket connected. Pending messages: ${pendingMessages.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
function handleMessage(event: Record<string, unknown>) {
|
||||
if (isOpenHandsEvent(event) && isMessageAction(event)) {
|
||||
messageRateHandler.record(new Date().getTime());
|
||||
}
|
||||
|
||||
setEvents((prevEvents) => [...prevEvents, event]);
|
||||
if (!Number.isNaN(parseInt(event.id as string, 10))) {
|
||||
lastEventRef.current = event;
|
||||
@@ -145,14 +164,39 @@ export function WsClientProvider({
|
||||
}
|
||||
sio.io.opts.query = sio.io.opts.query || {};
|
||||
sio.io.opts.query.latest_event_id = lastEventRef.current?.id;
|
||||
EventLogger.info(
|
||||
`WebSocket disconnected. Latest event ID: ${lastEventRef.current?.id}`,
|
||||
);
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
}
|
||||
|
||||
function handleError(data: unknown) {
|
||||
setStatus(WsClientProviderStatus.DISCONNECTED);
|
||||
EventLogger.error(`WebSocket connection error: ${JSON.stringify(data)}`);
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
}
|
||||
|
||||
// Process any pending messages when the WebSocket connects
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
status === WsClientProviderStatus.CONNECTED &&
|
||||
pendingMessages.length > 0 &&
|
||||
sioRef.current
|
||||
) {
|
||||
// We're connected and have pending messages
|
||||
EventLogger.info(
|
||||
`Connected! Sending ${pendingMessages.length} queued messages`,
|
||||
);
|
||||
|
||||
pendingMessages.forEach((event) => {
|
||||
sioRef.current?.emit("oh_action", event);
|
||||
});
|
||||
|
||||
setPendingMessages([]);
|
||||
EventLogger.info("All queued messages sent, queue cleared");
|
||||
}
|
||||
}, [status, pendingMessages.length]);
|
||||
|
||||
React.useEffect(() => {
|
||||
lastEventRef.current = null;
|
||||
}, [conversationId]);
|
||||
@@ -210,9 +254,10 @@ export function WsClientProvider({
|
||||
status,
|
||||
isLoadingMessages: messageRateHandler.isUnderThreshold,
|
||||
events,
|
||||
pendingMessages,
|
||||
send,
|
||||
}),
|
||||
[status, messageRateHandler.isUnderThreshold, events],
|
||||
[status, messageRateHandler.isUnderThreshold, events, pendingMessages],
|
||||
);
|
||||
|
||||
return <WsClientContext value={value}>{children}</WsClientContext>;
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
/**
|
||||
* A utility class for logging events. This class will only log events in development mode.
|
||||
* A utility class for logging events. This class will log events in development mode
|
||||
* and can be forced to log in any environment by setting FORCE_LOGGING to true.
|
||||
*/
|
||||
class EventLogger {
|
||||
static isDevMode = process.env.NODE_ENV === "development";
|
||||
|
||||
static FORCE_LOGGING = false; // Set to false for production, true only for debugging
|
||||
|
||||
static shouldLog() {
|
||||
return this.isDevMode || this.FORCE_LOGGING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and log a message event
|
||||
* @param event The raw event object
|
||||
*/
|
||||
static message(event: MessageEvent) {
|
||||
if (this.isDevMode) {
|
||||
console.warn(JSON.stringify(JSON.parse(event.data.toString()), null, 2));
|
||||
if (this.shouldLog()) {
|
||||
console.warn(
|
||||
"[OpenHands]",
|
||||
JSON.stringify(JSON.parse(event.data.toString()), null, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +32,8 @@ class EventLogger {
|
||||
* @param name The name of the event
|
||||
*/
|
||||
static event(event: Event, name?: string) {
|
||||
if (this.isDevMode) {
|
||||
console.warn(name || "EVENT", event);
|
||||
if (this.shouldLog()) {
|
||||
console.warn("[OpenHands]", name || "EVENT", event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +42,18 @@ class EventLogger {
|
||||
* @param warning The warning message
|
||||
*/
|
||||
static warning(warning: string) {
|
||||
if (this.isDevMode) {
|
||||
console.warn(warning);
|
||||
if (this.shouldLog()) {
|
||||
console.warn("[OpenHands]", warning);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an info message
|
||||
* @param info The info message
|
||||
*/
|
||||
static info(info: string) {
|
||||
if (this.shouldLog()) {
|
||||
console.info("[OpenHands]", info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +62,8 @@ class EventLogger {
|
||||
* @param error The error message
|
||||
*/
|
||||
static error(error: string) {
|
||||
if (this.isDevMode) {
|
||||
console.error(error);
|
||||
if (this.shouldLog()) {
|
||||
console.error("[OpenHands]", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,7 @@ function loadFeatureFlag(
|
||||
}
|
||||
}
|
||||
|
||||
export const BILLING_SETTINGS = () => loadFeatureFlag("BILLING_SETTINGS");
|
||||
export const HIDE_LLM_SETTINGS = () => loadFeatureFlag("HIDE_LLM_SETTINGS");
|
||||
export const BILLING_SETTINGS = () =>
|
||||
true || loadFeatureFlag("BILLING_SETTINGS");
|
||||
export const HIDE_LLM_SETTINGS = () =>
|
||||
true || loadFeatureFlag("HIDE_LLM_SETTINGS");
|
||||
|
||||
@@ -19,7 +19,7 @@ each of which has a corresponding port:
|
||||
When starting a web server, use the corresponding ports. You should also
|
||||
set any options to allow iframes and CORS requests, and allow the server to
|
||||
be accessed from any host (e.g. 0.0.0.0).
|
||||
For example, if you are using vite.config.js, you should set server.host to 0.0.0.0, server.port to the port assigned to you, and allowedHosts to ['*'].
|
||||
For example, if you are using vite.config.js, you should set server.host to 0.0.0.0, server.port to the port assigned to you, and allowedHosts to the host assigned to you.
|
||||
{% endif %}
|
||||
{% if runtime_info.additional_agent_instructions %}
|
||||
{{ runtime_info.additional_agent_instructions }}
|
||||
|
||||
@@ -111,12 +111,13 @@ class State:
|
||||
get_conversation_agent_state_filename(sid, user_id), encoded
|
||||
)
|
||||
|
||||
# see if state is in old directory. If yes, delete it.
|
||||
filename = get_conversation_agent_state_filename(sid)
|
||||
try:
|
||||
file_store.delete(filename)
|
||||
except Exception:
|
||||
pass
|
||||
# see if state is in the old directory on saas/remote use cases and delete it.
|
||||
if user_id:
|
||||
filename = get_conversation_agent_state_filename(sid)
|
||||
try:
|
||||
file_store.delete(filename)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to save state to session: {e}')
|
||||
raise e
|
||||
@@ -125,6 +126,11 @@ class State:
|
||||
def restore_from_session(
|
||||
sid: str, file_store: FileStore, user_id: str | None = None
|
||||
) -> 'State':
|
||||
"""
|
||||
Restores the state from the previously saved session.
|
||||
"""
|
||||
|
||||
state: State
|
||||
try:
|
||||
encoded = file_store.read(
|
||||
get_conversation_agent_state_filename(sid, user_id)
|
||||
@@ -132,12 +138,17 @@ class State:
|
||||
pickled = base64.b64decode(encoded)
|
||||
state = pickle.loads(pickled)
|
||||
except FileNotFoundError:
|
||||
# if user_id is provided, we are in a saas/remote use case
|
||||
# and we need to check if the state is in the old directory.
|
||||
if user_id:
|
||||
# see if state is in old directory. If yes, load it.
|
||||
filename = get_conversation_agent_state_filename(sid)
|
||||
encoded = file_store.read(filename)
|
||||
pickled = base64.b64decode(encoded)
|
||||
state = pickle.loads(pickled)
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f'Could not restore state from session file for sid: {sid}'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f'Could not restore state from session: {e}')
|
||||
raise e
|
||||
|
||||
@@ -406,7 +406,7 @@ class EventStream:
|
||||
# Text search in event content if query provided
|
||||
if query:
|
||||
event_dict = event_to_dict(event)
|
||||
event_str = str(event_dict).lower()
|
||||
event_str = json.dumps(event_dict).lower()
|
||||
if query.lower() not in event_str:
|
||||
return True
|
||||
|
||||
|
||||
@@ -121,13 +121,13 @@ Note: OpenHands works best with powerful models like Anthropic's Claude or OpenA
|
||||
The resolver can automatically attempt to fix a single issue in your repository using the following command:
|
||||
|
||||
```bash
|
||||
python -m openhands.resolver.resolve_issue --repo [OWNER]/[REPO] --issue-number [NUMBER]
|
||||
python -m openhands.resolver.resolve_issue --selected-repo [OWNER]/[REPO] --issue-number [NUMBER]
|
||||
```
|
||||
|
||||
For instance, if you want to resolve issue #100 in this repo, you would run:
|
||||
|
||||
```bash
|
||||
python -m openhands.resolver.resolve_issue --repo all-hands-ai/openhands --issue-number 100
|
||||
python -m openhands.resolver.resolve_issue --selected-repo all-hands-ai/openhands --issue-number 100
|
||||
```
|
||||
|
||||
The output will be written to the `output/` directory.
|
||||
@@ -135,19 +135,19 @@ The output will be written to the `output/` directory.
|
||||
If you've installed the package from source using poetry, you can use:
|
||||
|
||||
```bash
|
||||
poetry run python openhands/resolver/resolve_issue.py --repo all-hands-ai/openhands --issue-number 100
|
||||
poetry run python openhands/resolver/resolve_issue.py --selected-repo all-hands-ai/openhands --issue-number 100
|
||||
```
|
||||
|
||||
For resolving multiple issues at once (e.g., in a batch process), you can use the `resolve_all_issues` command:
|
||||
|
||||
```bash
|
||||
python -m openhands.resolver.resolve_all_issues --repo [OWNER]/[REPO] --issue-numbers [NUMBERS]
|
||||
python -m openhands.resolver.resolve_all_issues --selected-repo [OWNER]/[REPO] --issue-numbers [NUMBERS]
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
python -m openhands.resolver.resolve_all_issues --repo all-hands-ai/openhands --issue-numbers 100,101,102
|
||||
python -m openhands.resolver.resolve_all_issues --selected-repo all-hands-ai/openhands --issue-numbers 100,101,102
|
||||
```
|
||||
|
||||
## Responding to PR Comments
|
||||
|
||||
@@ -234,7 +234,7 @@ def main() -> None:
|
||||
description='Resolve multiple issues from Github or Gitlab.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--repo',
|
||||
'--selected-repo',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Github or Gitlab repository to resolve issues in form of `owner/repo`.',
|
||||
@@ -333,7 +333,7 @@ def main() -> None:
|
||||
f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
|
||||
)
|
||||
|
||||
owner, repo = my_args.repo.split('/')
|
||||
owner, repo = my_args.selected_repo.split('/')
|
||||
token = my_args.token or os.getenv('GITHUB_TOKEN') or os.getenv('GITLAB_TOKEN')
|
||||
username = my_args.username if my_args.username else os.getenv('GIT_USERNAME')
|
||||
if not username:
|
||||
@@ -342,7 +342,7 @@ def main() -> None:
|
||||
if not token:
|
||||
raise ValueError('Token is required.')
|
||||
|
||||
platform = identify_token(token)
|
||||
platform = identify_token(token, my_args.selected_repo)
|
||||
if platform == Platform.INVALID:
|
||||
raise ValueError('Token is invalid.')
|
||||
|
||||
|
||||
@@ -539,7 +539,7 @@ def main() -> None:
|
||||
|
||||
parser = argparse.ArgumentParser(description='Resolve a single issue.')
|
||||
parser.add_argument(
|
||||
'--repo',
|
||||
'--selected-repo',
|
||||
type=str,
|
||||
required=True,
|
||||
help='repository to resolve issues in form of `owner/repo`.',
|
||||
@@ -638,9 +638,9 @@ def main() -> None:
|
||||
f'ghcr.io/all-hands-ai/runtime:{openhands.__version__}-nikolaik'
|
||||
)
|
||||
|
||||
parts = my_args.repo.rsplit('/', 1)
|
||||
parts = my_args.selected_repo.rsplit('/', 1)
|
||||
if len(parts) < 2:
|
||||
raise ValueError('Invalid repo name')
|
||||
raise ValueError('Invalid repository format. Expected owner/repo')
|
||||
owner, repo = parts
|
||||
|
||||
token = my_args.token or os.getenv('GITHUB_TOKEN') or os.getenv('GITLAB_TOKEN')
|
||||
@@ -651,7 +651,7 @@ def main() -> None:
|
||||
if not token:
|
||||
raise ValueError('Token is required.')
|
||||
|
||||
platform = identify_token(token, repo)
|
||||
platform = identify_token(token, my_args.selected_repo)
|
||||
if platform == Platform.INVALID:
|
||||
raise ValueError('Token is invalid.')
|
||||
|
||||
|
||||
@@ -22,13 +22,13 @@ class Platform(Enum):
|
||||
GITLAB = 2
|
||||
|
||||
|
||||
def identify_token(token: str, repo: str | None = None) -> Platform:
|
||||
def identify_token(token: str, selected_repo: str | None = None) -> Platform:
|
||||
"""
|
||||
Identifies whether a token belongs to GitHub or GitLab.
|
||||
|
||||
Parameters:
|
||||
token (str): The personal access token to check.
|
||||
repo (str): Repository in format "owner/repo" for GitHub Actions token validation.
|
||||
selected_repo (str): Repository in format "owner/repo" for GitHub Actions token validation.
|
||||
|
||||
Returns:
|
||||
Platform: "GitHub" if the token is valid for GitHub,
|
||||
@@ -36,8 +36,8 @@ def identify_token(token: str, repo: str | None = None) -> Platform:
|
||||
"Invalid" if the token is not recognized by either.
|
||||
"""
|
||||
# Try GitHub Actions token format (Bearer) with repo endpoint if repo is provided
|
||||
if repo:
|
||||
github_repo_url = f'https://api.github.com/repos/{repo}'
|
||||
if selected_repo:
|
||||
github_repo_url = f'https://api.github.com/repos/{selected_repo}'
|
||||
github_bearer_headers = {
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
@@ -50,7 +50,7 @@ def identify_token(token: str, repo: str | None = None) -> Platform:
|
||||
if github_repo_response.status_code == 200:
|
||||
return Platform.GITHUB
|
||||
except requests.RequestException as e:
|
||||
print(f'Error connecting to GitHub API (repo check): {e}')
|
||||
print(f'Error connecting to GitHub API (selected_repo check): {e}')
|
||||
|
||||
# Try GitHub PAT format (token)
|
||||
github_url = 'https://api.github.com/user'
|
||||
|
||||
@@ -330,6 +330,15 @@ class StandaloneConversationManager(ConversationManager):
|
||||
|
||||
session = self._local_agent_loops_by_sid.get(sid)
|
||||
if session:
|
||||
# Check if the session is ready to process actions
|
||||
if not session.is_ready():
|
||||
logger.info(
|
||||
f'Session not ready, queueing action: {data}',
|
||||
extra={'session_id': sid},
|
||||
)
|
||||
session.queue_action(data)
|
||||
return
|
||||
|
||||
await session.dispatch(data)
|
||||
return
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import asyncio
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from dataclasses import field
|
||||
from logging import LoggerAdapter
|
||||
from typing import List
|
||||
|
||||
import socketio
|
||||
|
||||
@@ -12,6 +14,7 @@ from openhands.core.config.condenser_config import (
|
||||
)
|
||||
from openhands.core.const.guide_url import TROUBLESHOOTING_URL
|
||||
from openhands.core.logger import OpenHandsLoggerAdapter
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events.action import MessageAction, NullAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
@@ -43,6 +46,9 @@ class Session:
|
||||
file_store: FileStore
|
||||
user_id: str | None
|
||||
logger: LoggerAdapter
|
||||
_pending_actions: List[dict] = []
|
||||
_is_ready: bool = False
|
||||
_ready_event: asyncio.Event = field(default_factory=asyncio.Event)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -70,6 +76,16 @@ class Session:
|
||||
self.config = deepcopy(config)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
self.user_id = user_id
|
||||
self._pending_actions = []
|
||||
self._is_ready = False
|
||||
self._ready_event = asyncio.Event()
|
||||
|
||||
# Subscribe to agent state changes to detect when the agent is ready
|
||||
self.agent_session.event_stream.subscribe(
|
||||
EventStreamSubscriber.SERVER,
|
||||
self._on_agent_state_change,
|
||||
f'{self.sid}_state_change',
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
if self.sio:
|
||||
@@ -86,6 +102,11 @@ class Session:
|
||||
async def initialize_agent(
|
||||
self, settings: Settings, initial_message: MessageAction | None
|
||||
):
|
||||
# Reset the ready state when initializing a new agent
|
||||
self._is_ready = False
|
||||
self._ready_event.clear()
|
||||
|
||||
# Set the agent state to LOADING
|
||||
self.agent_session.event_stream.add_event(
|
||||
AgentStateChangedObservation('', AgentState.LOADING),
|
||||
EventSource.ENVIRONMENT,
|
||||
@@ -279,3 +300,55 @@ class Session:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._send_status_message(msg_type, id, message), self.loop
|
||||
)
|
||||
|
||||
def is_ready(self) -> bool:
|
||||
"""Check if the session is ready to process actions."""
|
||||
return self._is_ready
|
||||
|
||||
def queue_action(self, action_data: dict):
|
||||
"""Queue an action to be processed when the session is ready."""
|
||||
logger.info(f'Queueing action for session {self.sid}: {action_data}')
|
||||
self._pending_actions.append(action_data)
|
||||
|
||||
# Start a task to process the queue when the session becomes ready
|
||||
asyncio.run_coroutine_threadsafe(self._process_queue_when_ready(), self.loop)
|
||||
|
||||
async def _process_queue_when_ready(self):
|
||||
"""Process the queue of actions when the session becomes ready."""
|
||||
if not self._ready_event.is_set():
|
||||
try:
|
||||
# Wait for the session to become ready
|
||||
await asyncio.wait_for(self._ready_event.wait(), timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(
|
||||
f'Timeout waiting for session {self.sid} to become ready'
|
||||
)
|
||||
return
|
||||
|
||||
# Process all pending actions
|
||||
if self._pending_actions:
|
||||
logger.info(
|
||||
f'Processing {len(self._pending_actions)} queued actions for session {self.sid}'
|
||||
)
|
||||
|
||||
# Process all pending actions
|
||||
for action_data in self._pending_actions:
|
||||
logger.info(f'Processing queued action: {action_data}')
|
||||
await self.dispatch(action_data)
|
||||
|
||||
# Clear the queue
|
||||
self._pending_actions = []
|
||||
|
||||
def _on_agent_state_change(self, event: Event):
|
||||
"""Handle agent state change events to detect when the agent is ready."""
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
# Check if the agent state indicates it's ready
|
||||
if event.agent_state in ['idle', 'ready']:
|
||||
logger.info(f'Agent for session {self.sid} is now ready')
|
||||
self._is_ready = True
|
||||
self._ready_event.set()
|
||||
|
||||
# Process any pending actions
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._process_queue_when_ready(), self.loop
|
||||
)
|
||||
|
||||
@@ -99,6 +99,7 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -127,6 +128,7 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
Reference in New Issue
Block a user