Repository navigation
Improve automation run observability and LLM preflight #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,6 +73,7 @@ | |
| import os | ||
| import sys | ||
| import time | ||
| import urllib.request | ||
|
|
||
| # Detect execution mode based on AGENT_SERVER_URL presence | ||
| agent_server_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/") | ||
|
|
@@ -145,6 +146,44 @@ def _conversation_supports_user_id() -> bool: | |
| return False | ||
|
|
||
|
|
||
| def _conversation_run_timeout() -> float: | ||
| raw_timeout = os.environ.get("AUTOMATION_RUN_TIMEOUT") | ||
| if not raw_timeout: | ||
| return 3600.0 | ||
| try: | ||
| outer_timeout = float(raw_timeout) | ||
| except ValueError: | ||
| return 3600.0 | ||
| if outer_timeout <= 90: | ||
| return max(30.0, outer_timeout - 10.0) | ||
| return outer_timeout - 60.0 | ||
|
|
||
|
|
||
| def _notify_conversation_started(conversation_id: str) -> None: | ||
| callback_url = os.environ.get("AUTOMATION_CALLBACK_URL") | ||
| if not callback_url: | ||
| return | ||
| if not callback_url.endswith("/complete"): | ||
| print(" conversation update skipped: callback URL has unexpected shape") | ||
| return | ||
| update_url = callback_url[: -len("/complete")] + "/conversation" | ||
| data = json.dumps({"conversation_id": str(conversation_id)}).encode() | ||
| request = urllib.request.Request( | ||
| update_url, | ||
| data=data, | ||
| method="POST", | ||
| headers={ | ||
| "Authorization": f"Bearer {api_key}", | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The conversation-update POST authenticates with |
||
| "Content-Type": "application/json", | ||
| }, | ||
| ) | ||
| try: | ||
| with urllib.request.urlopen(request, timeout=10) as response: | ||
| print(f" conversation update sent: {response.status}") | ||
| except Exception as e: | ||
| print(f" conversation update failed: {type(e).__name__}: {e}", file=sys.stderr) | ||
|
|
||
|
|
||
| # Workspace base directory (for RemoteWorkspace working_dir). | ||
| # Default is /workspace because preset scripts run inside containers where this path | ||
| # is guaranteed to exist. For native local mode (outside containers), the dispatcher | ||
|
|
@@ -282,7 +321,6 @@ def _conversation_supports_user_id() -> bool: | |
| print(f" profile: {model_profile or 'DEFAULT'}") | ||
| print(f" model: {llm.model}") | ||
| print(f" api_key present: {bool(llm.api_key)}") | ||
|
|
||
| # Get secrets via workspace | ||
| print("\n=== GET_SECRETS ===") | ||
| secrets = {} | ||
|
|
@@ -358,10 +396,12 @@ def event_callback(event) -> None: | |
| conversation.update_secrets({"AUTOMATION_SESSION_URL": session_url}) | ||
| print(f" session URL: {session_url}") | ||
|
|
||
| _notify_conversation_started(str(conversation.id)) | ||
|
|
||
| try: | ||
| print(f" sending prompt: {USER_PROMPT[:80]}...") | ||
| conversation.send_message(USER_PROMPT) | ||
| conversation.run() | ||
| conversation.run(timeout=_conversation_run_timeout()) | ||
|
|
||
| # Wait for the stream to settle | ||
| while time.time() - last_event_time["ts"] < 2.0: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| AutomationRunResponse, | ||
| CreateAutomationRequest, | ||
| RunCompleteRequest, | ||
| RunConversationRequest, | ||
| UpdateAutomationRequest, | ||
| ) | ||
| from openhands.automation.storage import FileStore, get_file_store | ||
|
|
@@ -318,6 +319,50 @@ async def list_automation_runs( | |
| ) | ||
|
|
||
|
|
||
| # --- Run conversation metadata --- | ||
|
|
||
| @router.post("/runs/{run_id}/conversation") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| async def update_run_conversation( | ||
| run_id: uuid.UUID, | ||
| body: RunConversationRequest, | ||
| user: AuthenticatedUser = Depends(_require_manage_automations), | ||
| session: AsyncSession = Depends(get_session), | ||
| ) -> AutomationRunResponse: | ||
| """Associate a live conversation with an automation run. | ||
|
|
||
| Preset scripts call this immediately after creating the RemoteConversation so | ||
| the activity log can link to running or watchdog-failed runs even if the | ||
| process is later killed before the terminal completion callback fires. | ||
| """ | ||
| result = await session.execute( | ||
| select(AutomationRun) | ||
| .where(AutomationRun.id == run_id) | ||
| .options(selectinload(AutomationRun.automation)) | ||
| ) | ||
| run = result.scalars().first() | ||
| if run is None: | ||
| raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Run not found") | ||
|
|
||
| automation = run.automation | ||
| if automation.user_id != user.user_id or automation.org_id != user.org_id: | ||
| raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Not your automation") | ||
|
|
||
| if run.status not in { | ||
| AutomationRunStatus.PENDING, | ||
| AutomationRunStatus.RUNNING, | ||
| }: | ||
| raise HTTPException( | ||
| status.HTTP_409_CONFLICT, | ||
| detail=f"Run is {run.status.value}, expected PENDING or RUNNING", | ||
| ) | ||
|
|
||
| run.conversation_id = body.conversation_id | ||
| await session.flush() | ||
| await session.refresh(run) | ||
| logger.info("Run %s conversation_id=%s", run_id, body.conversation_id) | ||
| return AutomationRunResponse.model_validate(run) | ||
|
|
||
|
|
||
| # --- Run completion callback --- | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same credential bug as the prompt preset:
api_keyis empty in local/self-hosted mode (onlyAGENT_SERVER_URL,SESSION_API_KEY,WORKSPACE_BASE, andAUTOMATION_CALLBACK_API_KEYare injected), so this postsAuthorization: Bearerand gets a 401 that is swallowed by theexcept Exception. PreferAUTOMATION_CALLBACK_API_KEY(withapi_keyas fallback) so the conversation is actually linked in local deployments.