Compare commits

..

171 Commits

Author SHA1 Message Date
Alona King
1dfbe9390c cleanup: remove unnecessary AGENT_READY event emissions
- Remove artificial agent_state_changed events with invalid 'READY' state
- Frontend now properly waits for real AWAITING_USER_INPUT state
- These events weren't recognized by frontend anyway (READY not a valid AgentState)
- Simplifies code and removes confusion about agent readiness timing
2025-09-26 13:18:20 -04:00
Alona King
b442637aad feat: disable chat input until agent reaches AWAITING_USER_INPUT state
- Add tracking for when agent first reaches AWAITING_USER_INPUT
- Reset tracking when conversation status is STARTING
- Block input until agent shows 'Waiting for task' for first time
- Prevents messages being sent during setup.sh execution
- Works for new conversations, resume, and restart scenarios
- Added debug logging to track input state changes
2025-09-26 13:18:20 -04:00
Alona King
61e966f877 Revert "fix: wait for agent ready state before enabling chat"
This reverts commit dc3cbefe6f2f356b4105894c7657ba6ce3402812.
2025-09-26 13:18:20 -04:00
Alona King
846a9eed7d Revert "fix: respect Redis starting flag for all runtime states"
This reverts commit 62902bd8000bffde9613dc34b31524ff3ce8f477.
2025-09-26 13:18:20 -04:00
Alona King
9741207cea Revert "fix: add WebSocket to HTTP URL conversion and detailed logging in agent ready polling"
This reverts commit 8c314ade3b7eb2dfb60b3befe5fd2f49db9f1268.
2025-09-26 13:18:19 -04:00
Alona King
cb7293b5e0 fix: add WebSocket to HTTP URL conversion and detailed logging in agent ready polling
- Convert wss:// URLs to https:// for HTTP API calls
- Add detailed logging to diagnose polling failures
- Log the actual URL being used for events endpoint
- Log response status codes when requests fail
- Log when no ready state is found in events
2025-09-26 13:18:19 -04:00
Alona King
caf3852f59 fix: respect Redis starting flag for all runtime states
The previous logic only changed status to STARTING if runtime was STOPPED.
For resumed conversations with RUNNING runtime, the Redis flag was ignored,
causing frontend to show RUNNING even while agent was still initializing.

Now we always return STARTING if Redis flag exists, regardless of runtime status.
2025-09-26 13:18:19 -04:00
Alona King
e1e53ca5b6 fix: wait for agent ready state before enabling chat
- Add _wait_for_agent_truly_ready method that polls for agent ready states
- Check for AWAITING_USER_INPUT, RUNNING, or FINISHED states
- Move Redis flag deletion to AFTER agent is ready (not in finally block)
- Prevents messages being sent before setup.sh completes
- Works for both new conversations and resume
- 60-second timeout prevents infinite wait
- Error handling ensures Redis cleanup
2025-09-26 13:18:19 -04:00
Alona King
c000f15b0c Revert "fix: delay AGENT_READY event until agent reaches AWAITING_USER_INPUT state"
This reverts commit 6cfc666019cb076133caee5bb041a71f9079ee57.
2025-09-26 13:18:19 -04:00
Alona King
59553e943b fix: delay AGENT_READY event until agent reaches AWAITING_USER_INPUT state
- Modified _wait_for_conversation_ready to check agent state
- Wait for agent to be in AWAITING_USER_INPUT before emitting AGENT_READY
- Prevents frontend from showing 'Running' before agent can process messages
- Fixes issue where messages sent during setup.sh are lost
- Ensures agent is truly ready before enabling user input
2025-09-26 13:18:19 -04:00
Alona King
eb7e800166 fix: prevent WebSocket connection attempts with null session_api_key
- Add check to skip WebSocket connection if session_api_key is not available
- Prevents 'session_api_key=null' in WebSocket URL that causes auth failures
- Relies on existing polling to retry when session_api_key becomes available
- Fixes persistent 'Disconnected' status and connection errors
2025-09-26 13:18:19 -04:00
Alona King
bcdd410105 feat: add message and agent state logging to frontend
- Log all messages received (user and agent) with content preview
- Log agent state changes for debugging state transitions
- Log user message confirmations
- Helps debug why initial messages might not get responses
2025-09-26 13:18:19 -04:00
Alona King
419eae1878 fix: suppress WebSocket error banner during STARTING state
- Add connection error counter to track retry attempts
- Only show error banner after 3+ failures AND not in STARTING state
- Reset counter on successful connection
- Different handling for connection vs other errors
- Prevents transient connection issues from showing red banner
2025-09-26 13:18:19 -04:00
Alona King
04b3b8a035 feat: add comprehensive WebSocket debugging logs
- Log all connection parameters including URL and session_api_key presence
- Add detailed error information for connect_error events
- Include conversation and runtime status in all WebSocket events
- Add emoji indicators for better log readability
2025-09-26 13:18:19 -04:00
Alona King
f98a00def9 fix: prioritize conversation status over WebSocket status in UI
- Show 'Initializing agent...' during STARTING instead of 'Connecting...'
- Check conversation status before WebSocket status in getStatusCode
- Fixes UI confusion where WebSocket connection state overrides actual conversation state
- Users now see accurate status progression: Initializing -> Running
2025-09-26 13:18:19 -04:00
Alona King
7a6a759ec6 fix: allow WebSocket connection during STARTING status
The frontend was creating a deadlock:
- It set WebSocket to CONNECTING when conversation was STARTING
- But didn't actually establish connection until status was RUNNING
- This left the UI stuck at 'Connecting...' forever

The fix allows WebSocket to connect during STARTING status so it can:
- Receive real-time status updates
- Get the AGENT_STATE_CHANGED(READY) event
- Transition properly to RUNNING status

This is a minimal change - just removing the early return for STARTING
status and allowing it to proceed with WebSocket connection.
2025-09-26 13:18:19 -04:00
Alona King
673c455007 feat: add comprehensive frontend debugging logs for WebSocket connection
Added detailed logging to diagnose why frontend stays in 'Connecting...' state:
- Log WebSocket connection lifecycle and decision-making
- Log conversation status polling and transitions
- Log status code determination that shows UI messages
- Track when WebSocket actually connects vs when it stays in CONNECTING

Key findings from code review:
- Frontend sets WebSocket to CONNECTING when conversation is STARTING
- But it doesn't actually establish connection until status is RUNNING
- This causes the UI to show 'Connecting...' indefinitely if backend
  keeps returning STARTING status
2025-09-26 13:18:19 -04:00
Alona King
d5a77719fa fix: use existing session_api_key for resume instead of new one
When resuming a paused conversation, we should use the existing
session_api_key from the stored runtime, not the new one from the
reconnected runtime. This ensures proper authentication with the
existing conversation.
2025-09-26 13:18:19 -04:00
Alona King
a77d016a28 fix: correct misleading hardcoded log values for attach_to_existing
The logs were showing hardcoded 'False' values instead of the actual
attach_to_existing variable value, making debugging difficult.
2025-09-26 13:18:19 -04:00
Alona King
67f6881ab0 fix: properly handle running status in maybe_start_agent_loop
The previous implementation had a bug where it would return immediately
when seeing a 'running' status, preventing fresh conversations from
starting properly. This caused the agent to never receive messages.

Changes:
- Remove the early return for running status that was breaking fresh starts
- Properly distinguish between:
  - Fresh start: schedule work for stopped/empty runtimes
  - Resume: schedule work for paused runtimes
  - Already running: return STARTING if Redis says starting, else RUNNING
- Simplify the logic to be clearer about each state transition

This ensures that fresh conversations can start properly while still
handling resume cases correctly.
2025-09-26 13:18:19 -04:00
Alona King
072bb0e29e initial commit 2025-09-26 13:18:19 -04:00
Alona King
aee4f6bcc7 revert: remove git_provider_tokens functional change to keep pure logging only 2025-09-26 13:18:19 -04:00
Alona King
04ca22ee1d feat: add critical logging to diagnose frontend stuck state
- Add WEBSOCKET_DEBUG logging to track events endpoint readiness
- Add FRONTEND_DEBUG logging to track status returned to frontend
- Log what POST /start returns to frontend
- Log what GET /conversations returns during polling
- Track if websocket events endpoint becomes ready
2025-09-26 13:18:19 -04:00
Alona King
c321c4fc19 fix: apply enterprise linting fixes
- Remove f-string placeholders where not needed
- Apply ruff formatting
2025-09-26 13:18:19 -04:00
Alona King
345cd03ec2 feat: add comprehensive debugging logs for runtime initialization issues
- Add RESUME_DEBUG, ATTACH_DEBUG, CRITICAL_DEBUG, BUG_ALERT logging to identify status mapping bug
- Add TOKEN_SOURCE_DEBUG to track whether using cookie vs DB offline tokens
- Add STATE_TRANSITION logging for runtime state changes (paused->resuming->running)
- Add INIT_PATH_DEBUG and ENV_SETUP_DEBUG to track initialization differences
- Identify critical bug: maybe_start_agent_loop incorrectly maps 'paused' status, causing new runtime creation instead of resume
- Shows attach_to_existing is hardcoded to False when should be dynamic for resume scenarios
2025-09-26 13:18:19 -04:00
Alona King
045a78e2d7 Add comprehensive agent session initialization logging
- Add detailed logging throughout agent session start process
- Log runtime connection, repo cloning, controller creation steps
- Add error handling around git operations and setup scripts
- This will help identify exactly where 'Initializing Agent' hangs occur
- All changes are logging-only, no functional behavior changes
2025-09-26 13:18:19 -04:00
Alona King
90db91ca84 fix: apply enterprise linting
Fixed ruff and ruff-format issues in enterprise directory:
- Applied automatic formatting fixes to saas_nested_conversation_manager.py
- All enterprise linting checks now pass

Enterprise linting must be run from /enterprise directory as specified in CI.
2025-09-26 13:18:19 -04:00
Alona King
08d1d78525 fix: add defensive logging to prevent 500s on None values
Fixed unsafe string slicing that could cause exceptions:
- token_manager.py: offline_token[:20] -> offline_token[:20] if offline_token else 'None'
- api_key_store.py: api_key[:10] -> api_key[:10] if api_key else 'None'

All logging is now defensive and uses:
- logger.info() instead of print statements
- Conditional checks: value if value else 'None'
- Safe dictionary access: runtime.get('key', 'default')
- Protected string slicing: token[:N] if token else 'None'

No more risk of ASGI exceptions from logging operations.
2025-09-26 13:18:19 -04:00
Alona King
cbe02a405b Remove functional is_resume logic, keeping only logging
Removed functional changes that modified behavior:
- is_resume parameter from _start_agent_loop() and _create_runtime()
- attach_to_existing=is_resume logic (reverted to attach_to_existing=False)
- Status manipulation for resume detection
- .pre-commit-config.yaml symlink

Preserved valuable debugging additions:
- All [TOKEN_DEBUG] logging statements
- Token comparison logic in saas_user_auth.py (purely diagnostic)
- Runtime status logging
- Provider token debugging
- Session API key logging
- Defensive programming fixes in LegacyConversationManager

The branch now contains only logging enhancements on top of main branch
logic, suitable for investigation without changing runtime behavior.
2025-09-26 13:18:19 -04:00
Alona King
e1874a9b80 feat: add enhanced logging to distinguish OAuth2 proxy CSRF failures from token expiry
- Add detection logic to identify CSRF validation failures vs actual token expiry
- Log OAuth2 proxy CSRF cookie details when redirects occur
- Add clear messaging about the type of authentication failure
- Help diagnose why page refresh doesn't fix the issue after pod restarts

This logging will help understand the OAuth2 proxy CSRF token validation issue
that occurs when pods are destroyed/recreated during pause/resume operations.
2025-09-26 13:18:19 -04:00
Alona King
43a3f38cf8 Add comprehensive OAuth2 proxy session debugging logs
- Log close_session/pause operations to track when runtime stops
- Log OAuth2 proxy CSRF cookies in 302 redirects
- Log session API key usage in token refresh attempts
- Log provider tokens state before runtime creation
- Log keycloak_auth cookie presence and size
- Track full request details for token refresh API calls
2025-09-26 13:18:19 -04:00
Alona King
be14d3275f Fix formatting from ruff-format 2025-09-26 13:18:19 -04:00
Alona King
8fea63bdef Add comprehensive logging for session API key generation and validation
- Log when session API keys are created in ApiKeyStore
- Log when session API keys are validated
- Log when keys are retrieved from runtime headers
- Log when keys are used in API requests
- Track key previews for debugging session validation issues
2025-09-26 13:18:19 -04:00
Alona King
3fd0311eac feat: add definitive token comparison logging
- Compare full tokens, not just prefixes
- Log TOKENS_ARE_SAME or TOKENS_ARE_DIFFERENT
- Show token lengths for additional verification
- Will definitively prove if cookie and DB tokens match
2025-09-26 13:18:19 -04:00
Alona King
6f8fc1142b fix: make logging more defensive to prevent ASGI exceptions
- Wrap settings.provider_tokens access in try/catch
- Prevents AttributeError if settings is None or malformed
- Should fix 500 errors on POST /api/conversations
2025-09-26 13:18:19 -04:00
Alona King
9c9a3f1d5c feat: add logging to identify cookie vs offline token usage issue
- Cookie contains regular refresh token (2-hour expiry)
- Database contains offline token (30-day expiry)
- Code uses cookie token instead of DB offline token
- This explains why tokens expire after 2 hours despite 30-day offline setting
2025-09-26 13:18:19 -04:00
Alona King
33253292e5 fix: replace print statements with proper logging to resolve ASGI exceptions
- Fixed all print() statements to use logger.info() for consistency
- This was causing 'Exception in ASGI application' errors and 500 status codes
- Print statements interfered with ASGI's output stream handling
2025-09-26 13:18:19 -04:00
Alona King
8757476ff4 fix: make LegacyConversationManager more defensive against non-dict runtimes
- Add isinstance(dict) check to handle mocked objects in tests
- Ensure command is a string before substring check
- Fixes test failures with AsyncMock objects
- Fixes mypy unreachable code warning
2025-09-26 13:18:19 -04:00
Alona King
676632987d fix: update tests for LegacyConversationManager changes
- Empty command now returns False (use new manager)
- Missing command key now returns False instead of raising KeyError
- This matches the fix for handling paused runtimes without command field
2025-09-26 13:18:19 -04:00
Alona King
4cd33de9b3 feat: add focused logging for token refresh debugging
- Add detailed logging to /api/refresh-tokens endpoint
- Log session key validation to understand 403 vs 302 issues
- Add Keycloak refresh logging to see where 302 originates
- Track offline token usage and refresh attempts
- Remove DockerNestedConversationManager changes (not used in production)

This focused logging will help identify:
1. Why 5-minute stop/resume fails (session validation issue)
2. Where exactly the 302 redirect comes from
3. Whether it's a Keycloak timeout or session problem
2025-09-26 13:18:19 -04:00
Alona King
a85e6e7202 feat: add detailed token debugging to understand expiration issues
- Add comprehensive logging in ProviderHandler to detect 302 redirects
- Log token type (OAuth/PAT/Fine-grained) and prefix in RemoteRuntime
- Track token expiration times in hours/days for clarity
- Handle 302 redirects gracefully without trying to parse as JSON
- Add token length and prefix logging for type identification

This will help diagnose whether the issue is:
1. Keycloak offline token expiration (2 hours)
2. GitHub OAuth token expiration
3. Both token systems expiring

The logs will show exactly what type of GitHub token we're using
and how long it's supposed to last.
2025-09-26 13:18:19 -04:00
Alona King
a42e7e6283 fix: handle missing command field in LegacyConversationManager
- Fix KeyError when runtime['command'] doesn't exist for paused runtimes
- Add safe handling with runtime.get('command', '')
- Paused runtimes now correctly use SaasNestedConversationManager
- Add detailed logging to track which manager is being used
- Fix routing decision for token refresh on resume

This ensures paused conversations use the new SaasNestedConversationManager
which has the token refresh logic, not the legacy ClusteredConversationManager.
2025-09-26 13:18:19 -04:00
Alona King
7f413b27ee fix: add detailed token refresh clarity logging
- Show current token status with expiry info before refresh
- Compare old vs new tokens to detect actual changes
- Clear indicators (/⚠️) if tokens were refreshed or unchanged
- Fix type errors treating ProviderToken as dict
- Properly handle ProviderType enum keys with hasattr/getattr

This provides clarity on "No fresh tokens returned" issue to understand
if tokens are actually expired or still valid during resume operations.
2025-09-26 13:18:19 -04:00
Alona King
8bb1df6d01 fix: apply ruff lint fix for f-string formatting 2025-09-26 13:18:19 -04:00
Alona King
92d194286b fix: return STARTING status instead of STOPPED when resuming
The frontend was stuck in 'Initializing agent' because we returned
STOPPED status even though we were actually starting the resume.

This caused the frontend to not know the conversation was resuming.
Now we properly return STARTING status so the frontend knows to wait
for the conversation to become active.

The backend was working (runtime resumed, tokens refreshed) but the
frontend didn't know because of the wrong status.
2025-09-26 13:18:19 -04:00
Alona King
ed52f1c740 fix: apply ruff-format changes for enterprise linting
The enterprise directory has its own dev_config and CI runs
pre-commit from within the enterprise directory. Applied
formatting changes to pass CI.
2025-09-26 13:18:19 -04:00
Alona King
d078baa918 fix: use correct provider_tokens attribute name
Fixed mypy error - ProviderHandler has 'provider_tokens' not
'git_provider_tokens'. Also note that enterprise/ is excluded
from local pre-commit hooks which is why CI caught this.
2025-09-26 13:18:19 -04:00
Alona King
a21db6b863 feat: add comprehensive logging to SaasNestedConversationManager
Added extensive TOKEN_DEBUG logging to trace every decision point:
- Entry/exit of maybe_start_agent_loop
- Runtime fetch status and details
- Resume detection logic (paused runtime check)
- Provider token handling
- RemoteRuntime creation with attach_to_existing flag
- Status parsing and mapping
- Redis operations

This will help us understand:
1. Which conversation manager is actually being used
2. Whether runtime is properly detected as paused
3. If is_resume flag is being passed correctly
4. Whether tokens are being provided
5. The complete flow from REST API to runtime creation
2025-09-26 13:18:19 -04:00
Alona King
7a07520654 fix: enable token refresh on resume for SaasNestedConversationManager
Found the actual conversation manager used in staging! SaasNestedConversationManager
also hardcodes attach_to_existing=False. This fix:

1. Detects paused runtime (resume scenario)
2. Passes is_resume=True through the chain
3. Sets attach_to_existing=True for resume operations
4. Adds logging to track the flow

This should finally trigger token refresh when resuming conversations
in the enterprise/SaaS environment.
2025-09-26 13:18:19 -04:00
Alona King
60d52b91e6 debug: add logging to identify actual conversation manager class in use
The logs show maybe_start_agent_loop returns STOPPED but none of our
instrumented logging appears. This will tell us which conversation
manager implementation is actually being used in staging.
2025-09-26 13:18:19 -04:00
Alona King
44078776c3 fix: handle missing Docker containers on resume with attach_to_existing=True
When resuming a conversation after the Docker container has been removed:
1. Detect container missing and attempt to recreate it
2. Pass is_resume=True to _create_runtime to set attach_to_existing=True
3. This ensures token refresh runs when the runtime is recreated
4. Return STARTING status if container is still being created

This addresses both the 'Initializing agent...' stuck issue and ensures
that git provider tokens are refreshed on resume.
2025-09-26 13:18:19 -04:00
Alona King
85ee18ca0b fix: add safer logging to catch Docker container missing issue
- Add entry logging at start of maybe_start_agent_loop
- Wrap Docker container lookup in try/catch
- Log when container is missing and return STOPPED status
- This explains why maybe_start_agent_loop returns STOPPED on resume
- Container gets removed on stop, causing _get_nested_url to fail
2025-09-26 13:18:19 -04:00
Alona King
da4c3a4ec0 chore: add comprehensive logging to trace conversation flow
- Add logging to _start_agent_loop in both managers
- Add logging to get_running_agent_loops to see what sessions exist
- Add logging to is_agent_loop_running to see decision making
- Will show why maybe_start_agent_loop decides to start new vs use existing
- Shows container names in Docker manager to debug session detection
2025-09-26 13:18:19 -04:00
Alona King
84f5dd0332 chore: add logging to Docker conversation manager and REST API
- Add logging to DockerNestedConversationManager.maybe_start_agent_loop
- Add logging to REST API /start endpoint in manage_conversations.py
- Will show if Docker manager is being used (vs Standalone)
- Will clearly show RESUME vs NEW conversation starts
- Helps trace why attach_to_existing is always False
2025-09-26 13:18:19 -04:00
Alona King
8e397de517 chore: add comprehensive entry point logging to trace conversation flow
- Add logging to SocketIO connect handler (entry point)
- Add logging to conversation_manager.join_conversation
- Add logging to maybe_start_agent_loop with resume detection
- Will help identify if resumes bypass agent_session.py entirely
- Should show which path processes resuming conversations
2025-09-26 13:18:19 -04:00
Alona King
fc2a9158b0 chore: add comprehensive diagnostic logging to trace conversation flow
- Add logging to identify WebSocket vs REST API paths
- Log when ServerConversation is initialized (REST API)
- Log when AgentSession.start() is called (WebSocket)
- Check and log if runtime already exists for session
- Show when attach_to_existing should be True but is False
- Help identify why tokens aren't being passed through
2025-09-26 13:18:19 -04:00
Alona King
437cbc57c0 chore: add comprehensive logging to trace attach_to_existing decision
- Log when AgentSession.start() is called with all parameters
- Log before and after _create_runtime() calls
- Log runtime class type and whether it's RemoteRuntime
- Add DECISION POINT log where attach_to_existing should be determined
- No behavior changes, only logging to understand the flow
2025-09-26 13:18:19 -04:00
Alona King
bce166f7a1 Revert "fix: dynamically set attach_to_existing based on runtime existence"
This reverts commit fd5f134c847fd761b665319bec0a08e66e76942f.
2025-09-26 13:18:19 -04:00
Alona King
13fe3589dd fix: dynamically set attach_to_existing based on runtime existence
- Add _check_runtime_exists() method to determine if a runtime already exists for the session
- Set attach_to_existing=True when resuming existing runtime (status=running/paused)
- Set attach_to_existing=False when creating new runtime
- Add comprehensive [TOKEN_DEBUG] logging to trace the decision process
- Fixes issue where token refresh wasn't triggered on resume because attach_to_existing was hardcoded to False
2025-09-26 13:18:19 -04:00
Alona King
8a0355ede5 Add debug logging to trace attach_to_existing and token flow
Found root cause: attach_to_existing is hardcoded to False in
agent_session._create_runtime(), even when resuming existing conversations.

This causes:
- Token refresh to be skipped (setup_initial_env returns early)
- Provider tokens might not be passed correctly
- Our token refresh fix doesn't run

Added comprehensive logging to trace:
- RemoteRuntime initialization parameters
- attach_to_existing value throughout the flow
- Provider token presence and values
2025-09-26 13:18:19 -04:00
Alona King
44a42a2ee2 Add Bitbucket support to git URL token refresh
Ensures all three supported providers (GitHub, GitLab, Bitbucket) have
their git remote URLs updated with fresh tokens after resume.

Handles both Bitbucket token formats:
- username:app_password format
- access token with x-token-auth prefix
2025-09-26 13:18:19 -04:00
Alona King
6a87f1857d Fix: Use event loop properly for async token refresh
Creates a new event loop to avoid conflicts with existing loops
and properly closes it after use.
2025-09-26 13:18:19 -04:00
Alona King
6c4af040bd Fix token refresh on runtime resume after expiration
Problem: When resuming a paused runtime after 2+ hours, provider tokens
(GitHub, GitLab, etc.) have expired but are never refreshed, causing
git authentication failures.

Solution:
1. Add _refresh_provider_tokens_on_resume() method that always runs on resume
2. Fetch fresh tokens with 3-attempt retry logic and exponential backoff
3. Update git remote URLs with fresh tokens after successful refresh
4. Add comprehensive logging throughout to trace token refresh flow

The fix ensures that even when attach_to_existing=True (which skips
setup_initial_env), we still refresh expired tokens. This is critical
for long-running conversations that get paused and resumed hours later.
2025-09-26 13:18:19 -04:00
Alona King
ba4c593049 Add debug logging for token refresh investigation on resume
This commit adds initial debug logging to help investigate the issue
where GitHub tokens (and other provider tokens) expire and are not
renewed when resuming paused runtimes after 2+ hours.

Issue: When resuming conversations after extended periods, git operations
fail due to expired tokens that are never refreshed.
2025-09-26 13:18:19 -04:00
sp.wack
fb6f688049 refactor(frontend): convert settings to vertical sidebar layout (#10971)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-26 13:57:37 +00:00
sp.wack
ef12adc107 fix(backend): Add validation for LLM settings to prevent non-pro user bypass (#11113) 2025-09-26 16:10:09 +04:00
Hiep Le
8a7a5cce5e refactor(frontend): remove store.ts file (#11119)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-09-26 10:03:18 +07:00
mamoodi
b883fe37e6 fix MCP links (#11137) 2025-09-25 23:32:19 +00:00
mamoodi
182b7adcab Clean up MCP docs and move it to settings (#11133) 2025-09-25 18:28:29 -04:00
mamoodi
63829d0f45 Fix help links for OpenHands Provider (#11128)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-25 19:04:48 +00:00
jpelletier1
830a9e027f docs: Add Pro Subscription documentation page (#11115)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-25 13:07:30 -04:00
mamoodi
120a5d6ebd Add new settings folder for better organization (#11118) 2025-09-25 12:06:10 -04:00
mamoodi
6b1d1869f3 Run pypi workflow on tags (#11112) 2025-09-25 14:03:01 +00:00
Hiep Le
e376c2bfd1 refactor(frontend): migration of agent-slice.ts to zustand (#11102) 2025-09-25 12:44:21 +07:00
mamoodi
f8f74858da Update parts of the OpenHands Cloud docs (#11107) 2025-09-24 16:27:52 -04:00
Ray Myers
848a884b04 chore - Track Python test coverage (#11072) 2025-09-24 15:27:34 -05:00
Hiep Le
88a58a1748 refactor(frontend): migration of jupyter-slice.ts to zustand (#11019) 2025-09-25 00:56:55 +07:00
Hiep Le
f59ea69b70 refactor(frontend): migration of microagent-management-slice.ts to zustand (#11033) 2025-09-24 23:58:17 +07:00
dependabot[bot]
8f004a1f6d chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.86.0 to 5.90.1 in /frontend in the eslint group (#11101)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 20:53:20 +04:00
sp.wack
15b4690ebf feat(frontend): Animate conversation panels (#11099) 2025-09-24 16:41:19 +00:00
Hiep Le
df1c5bbf85 refactor(frontend): migration of event-message-slice.ts to zustand (#11080) 2025-09-24 22:53:45 +07:00
Hiep Le
8adbb76bd7 refactor(frontend): migration of browser-slice.ts to zustand (#11081) 2025-09-24 22:52:48 +07:00
Hiep Le
0095672439 feat(frontend): keyboard shortcuts for file copy and paste (#11096) 2025-09-24 22:52:03 +07:00
Hiep Le
6a5d09660d fix(frontend): upgrade banner covers conversation panel in settings (#11094) 2025-09-24 21:40:16 +07:00
Hiep Le
a94906e15c refactor(frontend): migration of security-analyzer-slice.ts to zustand (#11082) 2025-09-24 21:32:25 +07:00
Hiep Le
12dc256b5a refactor(frontend): git actions should be enabled at all times (#11063) 2025-09-24 21:31:51 +07:00
Hiep Le
11edf33b97 refactor(frontend): remove file-state-slice.ts file (#11061) 2025-09-24 21:31:11 +07:00
Hiep Le
fce66e94e7 refactor(frontend): event message (#11001) 2025-09-24 21:30:55 +07:00
Hiep Le
5457392eae refactor(frontend): migration of conversation-slice.ts to zustand (#11032) 2025-09-24 21:30:37 +07:00
Ray Myers
1e7024b60a fix - Set claude sonnet output limit (#11098) 2025-09-24 13:31:20 +00:00
Hiep Le
3977d4fdd7 fix(frontend): insufficient spacing between last message and chat input (#11055) 2025-09-24 20:13:21 +07:00
Tejas Goyal
16004426a2 feat: Add configurable timeouts for MCP tool invocations (Good first issues: #10684) (#11029)
Co-authored-by: Tejas Goyal <tejas@Tejass-MacBook-Pro.local>
2025-09-24 16:43:54 +04:00
dependabot[bot]
73eb53a379 chore(deps): bump the version-all group across 1 directory with 21 updates (#11078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-24 16:24:27 +04:00
BenYao21
d3d70fcc60 issue #9388, this will fix the issue (#10450)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-09-22 16:56:53 -04:00
Xinyi He
7906eab6b1 Add inference generation of SWE-Perf Benchmark (#10246)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 20:35:30 +00:00
juanmichelini
547e1049f1 Multi swe gym (#10605)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 15:56:26 -04:00
mamoodi
818cc60b52 New label for not going stale (#11069) 2025-09-22 11:53:47 -04:00
Robert Brennan
431d2c1f43 security: upgrade setuptools to >=78.1.1 to address CVE-2025-47273 and CVE-2024-6345 (#11038)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: enyst <engel.nyst@gmail.com>
2025-09-22 04:05:45 +00:00
Engel Nyst
07f23641a3 build(deps): pin litellm to avoid build failure (#11054)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-22 03:54:37 +02:00
Hiep Le
de84af5586 feat(frontend): display lock icon when confirmation mode is enabled (#11030) 2025-09-20 10:55:19 +07:00
Hiep Le
b7765ba3f7 refactor(frontend): fix typecheck (#11037) 2025-09-19 13:43:00 -04:00
Hiep Le
b89f2e51e4 refactor(frontend): migration of metrics-slice.ts to zustand (#11018) 2025-09-19 23:52:21 +07:00
mamoodi
e09f93aa75 Release 0.57.0 (#10981)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-09-19 12:40:56 -04:00
Hiep Le
9f529b105a refactor(frontend): migration of command-slice.ts to zustand (#11003) 2025-09-19 23:33:59 +07:00
Graham Neubig
89e3d2a867 Improve OpenHands provider pricing documentation (#10974)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-20 00:22:44 +08:00
Hiep Le
a7b9a4f291 refactor(frontend): migration of status-slice.ts to zustand (#11017) 2025-09-19 22:27:55 +07:00
Hiep Le
88cd16ae21 refactor(frontend): migration of initial-query-slice.ts to zustand (#11020) 2025-09-19 22:27:20 +07:00
Hiep Le
a8a3e9e604 refactor(frontend): remove the code-slice.ts file (#11021) 2025-09-19 21:22:29 +07:00
Hiep Le
0061bcc0b0 refactor(frontend): custom chat input (#10984) 2025-09-19 21:06:18 +07:00
Hiep Le
9c9fa780b0 refactor(frontend): task tracking observation content (#11002) 2025-09-19 20:03:05 +07:00
Alona
569ac16163 Improve token refresh error logging (#11026) 2025-09-19 14:18:38 +07:00
Robert Brennan
46f7738f41 Update Python packages to latest versions (#11023)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 19:52:46 +00:00
Rohit Malhotra
3f3669dd34 Hotfix: rm model choice override (#11022) 2025-09-18 14:40:06 -04:00
sp.wack
cd65645eea Hide Tavily search API key help text in SaaS mode (#11014)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 16:40:29 +00:00
Robert Brennan
8e88a7a277 fix: resolve critical and high CVEs in enterprise Docker image (#10987)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-18 11:25:33 -04:00
Hiep Le
b393d52439 refactor(frontend): conversation main (#10985) 2025-09-18 20:23:13 +07:00
Hiep Le
faeec48365 refactor(frontend): conversation card (#10986) 2025-09-18 20:22:59 +07:00
sp.wack
774caf0607 feat: refactor status indicators (#10983)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 22:32:55 +04:00
sp.wack
7222730df0 Fix SaaS callback URLs and pro pill positioning (#10998)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 16:56:02 +00:00
Hiep Le
910177fc57 refactor(frontend): system message modal (#10969) 2025-09-17 21:56:14 +07:00
Hiep Le
ac9badbd20 refactor(frontend): metrics modal (#10968) 2025-09-17 21:55:25 +07:00
Ray Myers
02c299d88f Fix Slack resolver failing on AWAITING_USER_INPUT state (#10992)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-17 09:20:12 -05:00
mamoodi
f65fbef649 Remove runtime settings (#10996) 2025-09-17 13:59:29 +00:00
Hiep Le
3c2acad28d refactor(frontend): microagents modal (#10970) 2025-09-16 22:32:23 +07:00
Boxuan Li
0f1780728e Update str_replace_editor tool to use dynamic workspace path from SANDBOX_VOLUMES (#10965)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-15 17:46:54 -07:00
sp.wack
d3f3378a4c feat: Upgrade banner for unsubscribed SaaS users (#10890)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-15 23:04:44 +00:00
Engel Nyst
65f4164749 [Docs] Add environment variables reference table (#10926)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-15 18:31:44 +00:00
Hiep Le
3f984d878b refactor(frontend): move conversation APIs to a dedicated service handler (#10957) 2025-09-16 00:57:15 +07:00
Eliot Jones
10b871f4ab feat: Add Cygnal integration (#10898) 2025-09-15 09:57:03 -04:00
Hiep Le
d664f516db refactor(frontend): conversation tab content component (#10956) 2025-09-15 20:56:38 +07:00
Hiep Le
e74bbd81d1 fix(frontend): suppressing event display in the absence of user messages (#10955) 2025-09-15 20:56:16 +07:00
Hiep Le
ab893f93f0 refactor(frontend): use-auto-resize hook (#10959) 2025-09-15 20:49:15 +07:00
Hiep Le
5aba498e77 refactor(frontend): move billing APIs to a dedicated service handler (#10958) 2025-09-15 20:37:07 +07:00
Hiep Le
1523555eea refactor(frontend): remove dead code (#10839) 2025-09-15 20:35:56 +07:00
Kaushik Ashodiya
30604c40fc fix: improve CLI help and version command performance (#10908)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-12 14:23:01 -04:00
Hiep Le
8dc46b7206 refactor(frontend): optimize pre-commit lint script (#10870)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-09-12 15:23:29 +00:00
Hiep Le
69498bebb4 refactor(frontend): new conversation component (#10937)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-09-12 22:15:26 +07:00
tksrmz
77ee9e25d9 fix(frontend): highlight preceding stars on hover in LikertScale (#10948) 2025-09-12 18:01:40 +04:00
Hiep Le
74753036bb refactor(frontend): move user APIs to a dedicated service handler (#10943) 2025-09-12 09:08:15 +07:00
Hiep Le
95d7c10608 refactor(frontend): move option APIs to a dedicated service handler (#10933) 2025-09-12 00:43:15 +07:00
Hiep Le
c142cc27ff refactor(frontend): home header component (#10930) 2025-09-12 00:10:58 +07:00
Hiep Le
0e20fc206b refactor(frontend): move settings APIs to a dedicated service handler (#10941) 2025-09-11 23:39:23 +07:00
Hiep Le
e21475a88e feat(frontend): persist drawer open/close state on page refresh (#10935)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-09-11 15:58:00 +00:00
Hiep Le
921fec0019 refactor(frontend): expand repository pill to full available width (#10936) 2025-09-11 22:37:44 +07:00
Hiep Le
049f839a62 refactor(frontend): move auth APIs to a dedicated service handler (#10932) 2025-09-11 22:31:41 +07:00
Hiep Le
0dde758e13 refactor(frontend): move microagent management API to a dedicated service handler (#10934) 2025-09-11 22:27:56 +07:00
Tim O'Farrell
8257ae70cc Additional logs to debug container working directories (#10902)
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2025-09-11 11:06:19 -04:00
Ray Myers
4513bcc622 chore - MyPy check Enterprise with OpenHands (#10858)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-09-11 11:05:50 -04:00
Hiep Le
b5b9a3f40b refactor(frontend): create waiting for runtime component (#10931) 2025-09-11 21:30:05 +07:00
Xingyao Wang
8ea1259943 Add GitHub workflow for MDX format checking and fix parsing error (#10924)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 23:04:54 +00:00
Ray Myers
ddb2794adf fix - Tag enterprise with the same SHA as app image. (#10921) 2025-09-10 16:47:31 -05:00
sp.wack
79fdcad7ef Fix status indicator and chat input synchronization issue (#10914)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 20:39:14 +00:00
chuckbutkus
1de70b8ce4 Fix runtime init (#10909) 2025-09-10 19:28:12 +00:00
sp.wack
3baeecb27c meta(frontend): Improve UX (#9845)
Co-authored-by: Mislav Lukach <mislavlukach@gmail.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-10 18:12:52 +00:00
Tim O'Farrell
b08238c841 Fix for issue where some attributes in pr_data are defined but are null or undefined (#10827) 2025-09-09 21:28:40 +00:00
sp.wack
831084df4c Remove git authentication requirement for secrets in SaaS mode (#10903)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-09 19:50:13 +00:00
sp.wack
eb4dacb577 Fix ruff formatting in enterprise token_manager.py (#10901) 2025-09-09 18:45:45 +00:00
jrobles98
8e71459601 Fix typo (#10702) 2025-09-09 12:39:58 -04:00
Tim O'Farrell
fc29815aa0 Value logged as error should be info (#10831) 2025-09-09 08:48:29 -06:00
mamoodi
a809d74b7d Release 0.56.0 (#10876) 2025-09-09 10:30:43 -04:00
Ryan H. Tran
b090d097ed Fix Docker build error 'groupadd: GID 1000 already exists' (#10888) 2025-09-09 21:50:23 +08:00
Graham Neubig
79f32a34a0 Fix SANDBOX_VOLUMES format in headless mode documentation (#10887)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-08 14:17:20 -04:00
Ashwin Kumar B V
805bc5608e Update deprecated dependencies: google-genai and yanked ddtrace (#10866)
Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-08 10:04:16 -05:00
Ray Myers
61e1957cee chore - Make enterprise preview work when labeled after the fact (#10862) 2025-09-08 09:54:51 -05:00
Joe Axe
a25826a5f9 fix: resolve empty API keys to None and add Bedrock model support (#10573) 2025-09-08 14:45:10 +02:00
Ryan H. Tran
df9320f8ab Implement model routing support (#9738)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-08 16:19:34 +07:00
Boxuan Li
af0ab5a9f2 Fix working_dir bug in local runtime (#10801)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-07 23:44:55 -07:00
Ruilin Zhou
9960d11d08 feat(runtime): upgrade E2B runtime to v2.0 with full implementation (#10832) 2025-09-08 06:32:08 +02:00
mamoodi
d5d5e265f8 Fix issue #10729: Add x-ai/grok-code-fast-1 to MODELS_WITHOUT_STOP_WORDS (#10867)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-09-08 05:30:45 +02:00
Xingyao Wang
989a4e662b feat: integrate with unified docs repository (#10830)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-09-06 16:10:21 +02:00
骆艺轩
ecfbae2285 refactor: Tweak labels prompt (#10523) (#10757) 2025-09-06 03:17:44 +02:00
748 changed files with 28015 additions and 13621 deletions

23
.github/workflows/dispatch-to-docs.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Dispatch to docs repo
on:
push:
branches: [main]
paths:
- 'docs/**'
workflow_dispatch:
jobs:
dispatch:
runs-on: ubuntu-latest
strategy:
matrix:
repo: ["All-Hands-AI/docs"]
steps:
- name: Push to docs repo
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.ALLHANDS_BOT_GITHUB_PAT }}
repository: ${{ matrix.repo }}
event-type: update
client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "module": "openhands", "branch": "main"}'

View File

@@ -0,0 +1,29 @@
# Feature branch preview for enterprise code
name: Enterprise Preview
# Run on PRs labeled
on:
pull_request:
types: [labeled]
# Match ghcr-build.yml, but don't interrupt it.
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: false
jobs:
# This must happen for the PR Docker workflow when the label is present,
# and also if it's added after the fact. Thus, it exists in both places.
enterprise-preview:
name: Enterprise preview
if: github.event.label.name == 'deploy'
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
# This should match the version in ghcr-build.yml
- name: Trigger remote job
run: |
curl --fail-with-body -sS -X POST \
-H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \
-H "Accept: application/vnd.github+json" \
-d "{\"ref\": \"main\", \"inputs\": {\"openhandsPrNumber\": \"${{ github.event.pull_request.number }}\", \"deployEnvironment\": \"feature\", \"enterpriseImageTag\": \"pr-${{ github.event.pull_request.number }}\" }}" \
https://api.github.com/repos/All-Hands-AI/deploy/actions/workflows/deploy.yaml/dispatches

View File

@@ -176,8 +176,10 @@ jobs:
# Do not build enterprise in forks
if: github.event.pull_request.head.repo.fork != true
steps:
- name: Checkout repository
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# Set up Docker Buildx for better performance
- name: Set up Docker Buildx
@@ -235,12 +237,11 @@ jobs:
enterprise-preview:
name: Enterprise preview
if: |
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'deploy') ||
(github.event_name == 'pull_request' && github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'deploy'))
if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy')
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [ghcr_build_enterprise]
steps:
# This should match the version in enterprise-preview.yml
- name: Trigger remote job
run: |
curl --fail-with-body -sS -X POST \

70
.github/workflows/mdx-lint.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
# Workflow that checks MDX format in docs/ folder
name: MDX Lint
# Run on pushes to main and on pull requests that modify docs/ files
on:
push:
branches:
- main
paths:
- 'docs/**/*.mdx'
pull_request:
paths:
- 'docs/**/*.mdx'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
mdx-lint:
name: Lint MDX files
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- uses: actions/checkout@v4
- name: Install Node.js 22
uses: useblacksmith/setup-node@v5
with:
node-version: 22
- name: Install MDX dependencies
run: |
npm install @mdx-js/mdx@3 glob@10
- name: Validate MDX files
run: |
node -e "
const {compile} = require('@mdx-js/mdx');
const fs = require('fs');
const path = require('path');
const glob = require('glob');
async function validateMDXFiles() {
const files = glob.sync('docs/**/*.mdx');
console.log('Found', files.length, 'MDX files to validate');
let hasErrors = false;
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
await compile(content);
console.log('✅ MDX parsing successful for', file);
} catch (err) {
console.error('❌ MDX parsing failed for', file, ':', err.message);
hasErrors = true;
}
}
if (hasErrors) {
console.error('\\n❌ Some MDX files have parsing errors. Please fix them before merging.');
process.exit(1);
} else {
console.log('\\n✅ All MDX files are valid!');
}
}
validateMDXFiles();
"

View File

@@ -19,12 +19,16 @@ jobs:
# Run python tests on Linux
test-on-linux:
name: Python Tests on Linux
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: blacksmith-4vcpu-ubuntu-2404
env:
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ["3.12"]
permissions:
# For coverage comment and python-coverage-comment-action branch
pull-requests: write
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
@@ -48,10 +52,21 @@ jobs:
- name: Build Environment
run: make build
- name: Run Unit Tests
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv ./tests/unit
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -s ./tests/unit --cov=openhands --cov-branch
env:
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
- name: Run Runtime Tests with CLIRuntime
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
env:
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-openhands
path: |
.coverage.${{ matrix.python_version }}
.coverage.runtime.${{ matrix.python_version }}
include-hidden-files: true
# Run specific Windows python tests
test-on-windows:
name: Python Tests on Windows
@@ -85,7 +100,7 @@ jobs:
DEBUG: "1"
test-enterprise:
name: Enterprise Python Unit Tests
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
matrix:
python-version: ["3.12"]
@@ -102,5 +117,37 @@ jobs:
working-directory: ./enterprise
run: poetry install --with dev,test
- name: Run Unit Tests
working-directory: ./enterprise
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./tests/unit
# Use base working directory for coverage paths to line up.
run: PYTHONPATH=".:$PYTHONPATH" poetry run --project=enterprise pytest --forked -n auto -s -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./enterprise/tests/unit --cov=enterprise --cov-branch
env:
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
- name: Store coverage file
uses: actions/upload-artifact@v4
with:
name: coverage-enterprise
path: ".coverage.enterprise.${{ matrix.python_version }}"
include-hidden-files: true
coverage-comment:
name: Coverage Comment
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [test-on-linux, test-enterprise]
permissions:
pull-requests: write
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v5
id: download
with:
pattern: coverage-*
merge-multiple: true
- name: Coverage comment
id: coverage_comment
uses: py-cov-action/python-coverage-comment-action@v3
with:
GITHUB_TOKEN: ${{ github.token }}
MERGE_COVERAGE_FILES: true

View File

@@ -1,7 +1,7 @@
# Publishes the OpenHands PyPi package
name: Publish PyPi Package
# Triggered manually
on:
workflow_dispatch:
inputs:
@@ -9,6 +9,9 @@ on:
description: 'Reason for manual trigger'
required: true
default: ''
push:
tags:
- "*"
jobs:
release:

View File

@@ -15,7 +15,7 @@ jobs:
stale-issue-message: 'This issue is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
stale-pr-message: 'This PR is stale because it has been open for 40 days with no activity. Remove the stale label or leave a comment, otherwise it will be closed in 10 days.'
days-before-stale: 40
exempt-issue-labels: roadmap,backlog
exempt-issue-labels: roadmap,backlog,app-team
close-issue-message: 'This issue was automatically closed due to 50 days of inactivity. We do this to help keep the issues somewhat manageable and focus on active issues.'
close-pr-message: 'This PR was closed because it had no activity for 50 days. If you feel this was closed in error, and you would like to continue the PR, please resubmit or let us know.'
days-before-close: 10

View File

@@ -87,8 +87,6 @@ VSCode Extension:
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
If you need to add labels when opening a PR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
## Implementation Details
These details may or may not be useful for your current task.

View File

@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.55-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.57-nikolaik`
## Develop inside Docker container

View File

@@ -79,17 +79,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
You can also run OpenHands directly with Docker:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.55
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
</details>

View File

@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.55
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。

View File

@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.55
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

View File

@@ -1,86 +0,0 @@
[
{
"file": "openhands/runtime/utils/runtime_init.py",
"line": 64,
"header": " if existing_user_id == user_id:",
"body_uses_user_id": true,
"body_preview": " logger.debug(\n f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'\n )\n"
},
{
"file": "openhands/resolver/issue_resolver.py",
"line": 252,
"header": " if user_id == 0:",
"body_uses_user_id": true,
"body_preview": " sandbox_config.user_id = get_unique_uid()\n\n"
},
{
"file": "openhands/storage/locations.py",
"line": 5,
"header": " if user_id:",
"body_uses_user_id": true,
"body_preview": " return f'users/{user_id}/conversations/{sid}/'\n"
},
{
"file": "openhands/storage/conversation/conversation_store.py",
"line": 39,
"header": " if not metadata.user_id or metadata.user_id != user_id:",
"body_uses_user_id": false,
"body_preview": " return False\n"
},
{
"file": "openhands/server/routes/manage_conversations.py",
"line": 674,
"header": " if user_id and metadata.user_id != user_id:",
"body_uses_user_id": true,
"body_preview": " logger.warning(\n f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',\n extra={'session_id': conversation_id, 'user_id': user_id},\n )\n return JSONResponse(\n content={\n 'status': 'error',\n 'message': 'Permission denied: You can only update your own conversations',\n 'msg_id': 'AUTHORIZATION$PERMISSION_DENIED',\n },\n"
},
{
"file": "openhands/server/routes/settings.py",
"line": 62,
"header": " if provider_token.token or provider_token.user_id:",
"body_uses_user_id": false,
"body_preview": " provider_tokens_set[provider_type] = provider_token.host\n\n"
},
{
"file": "openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 95,
"header": " if not await session_exists(sid, self.file_store, user_id=user_id):",
"body_uses_user_id": false,
"body_preview": " return None\n\n"
},
{
"file": "openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 257,
"header": " if user_id:",
"body_uses_user_id": true,
"body_preview": " items = (item for item in items if item[1].user_id == user_id)\n\n"
},
{
"file": "openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 273,
"header": " if user_id:",
"body_uses_user_id": true,
"body_preview": " for connection_id, sid in list(connections.items()):\n session = self._local_agent_loops_by_sid.get(sid)\n if not session or session.user_id != user_id:\n connections.pop(connection_id)\n"
},
{
"file": "openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 718,
"header": " if user_id and session.user_id != user_id:",
"body_uses_user_id": false,
"body_preview": " continue\n"
},
{
"file": "openhands/controller/state/state.py",
"line": 137,
"header": " if user_id:",
"body_uses_user_id": false,
"body_preview": " filename = get_conversation_agent_state_filename(sid)\n try:\n file_store.delete(filename)\n except Exception:\n pass\n"
},
{
"file": "openhands/controller/state/state.py",
"line": 164,
"header": " if user_id:",
"body_uses_user_id": false,
"body_preview": " filename = get_conversation_agent_state_filename(sid)\n encoded = file_store.read(filename)\n pickled = base64.b64decode(encoded)\n state = pickle.loads(pickled)\n"
}
]

View File

@@ -1,51 +0,0 @@
[
{
"file": "./openhands/storage/locations.py",
"line": 5,
"header": " if user_id:",
"body_uses_user_id": true,
"body_preview": " return f'users/{user_id}/conversations/{sid}/'\n"
},
{
"file": "./openhands/server/routes/manage_conversations.py",
"line": 674,
"header": " if user_id and metadata.user_id != user_id:",
"body_uses_user_id": true,
"body_preview": " logger.warning(\n\n f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',\n\n extra={'session_id': conversation_id, 'user_id': user_id},\n\n )\n\n return JSONResponse(\n\n content={\n\n 'status': 'error',\n\n 'message': 'Permission denied: You can only update your own conversations',\n"
},
{
"file": "./openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 257,
"header": " if user_id:",
"body_uses_user_id": true,
"body_preview": " items = (item for item in items if item[1].user_id == user_id)\n\n\n"
},
{
"file": "./openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 273,
"header": " if user_id:",
"body_uses_user_id": true,
"body_preview": " for connection_id, sid in list(connections.items()):\n\n session = self._local_agent_loops_by_sid.get(sid)\n\n if not session or session.user_id != user_id:\n\n connections.pop(connection_id)\n"
},
{
"file": "./openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 718,
"header": " if user_id and session.user_id != user_id:",
"body_uses_user_id": false,
"body_preview": " continue\n"
},
{
"file": "./openhands/controller/state/state.py",
"line": 137,
"header": " if user_id:",
"body_uses_user_id": false,
"body_preview": " filename = get_conversation_agent_state_filename(sid)\n\n try:\n\n file_store.delete(filename)\n\n except Exception:\n\n pass\n"
},
{
"file": "./openhands/controller/state/state.py",
"line": 164,
"header": " if user_id:",
"body_uses_user_id": false,
"body_preview": " filename = get_conversation_agent_state_filename(sid)\n\n encoded = file_store.read(filename)\n\n pickled = base64.b64decode(encoded)\n\n state = pickle.loads(pickled)\n"
}
]

View File

@@ -1,23 +0,0 @@
[
{
"file": "./openhands/server/conversation_manager/standalone_conversation_manager.py",
"line": 718,
"header": " if user_id and session.user_id != user_id:",
"body_uses_user_id": false,
"body_preview": " continue\n"
},
{
"file": "./openhands/controller/state/state.py",
"line": 137,
"header": " if user_id:",
"body_uses_user_id": false,
"body_preview": " filename = get_conversation_agent_state_filename(sid)\n\n try:\n\n file_store.delete(filename)\n\n except Exception:\n\n pass\n"
},
{
"file": "./openhands/controller/state/state.py",
"line": 164,
"header": " if user_id:",
"body_uses_user_id": false,
"body_preview": " filename = get_conversation_agent_state_filename(sid)\n\n encoded = file_store.read(filename)\n\n pickled = base64.b64decode(encoded)\n\n state = pickle.loads(pickled)\n"
}
]

View File

@@ -1,88 +0,0 @@
config.template.toml
docs/openapi.json
docs/usage/configuration-options.mdx
openhands/cli/gui_launcher.py
openhands/cli/main.py
openhands/controller/agent_controller.py
openhands/controller/state/state.py
openhands/controller/state/state_tracker.py
openhands/core/config/mcp_config.py
openhands/core/config/sandbox_config.py
openhands/core/main.py
openhands/events/event_store.py
openhands/events/event_store_abc.py
openhands/events/nested_event_store.py
openhands/events/stream.py
openhands/experiments/experiment_manager.py
openhands/integrations/bitbucket/bitbucket_service.py
openhands/integrations/github/github_service.py
openhands/integrations/github/service/features.py
openhands/integrations/gitlab/gitlab_service.py
openhands/integrations/provider.py
openhands/integrations/service_types.py
openhands/resolver/issue_resolver.py
openhands/runtime/action_execution_server.py
openhands/runtime/base.py
openhands/runtime/impl/action_execution/action_execution_client.py
openhands/runtime/impl/cli/cli_runtime.py
openhands/runtime/impl/docker/docker_runtime.py
openhands/runtime/impl/kubernetes/kubernetes_runtime.py
openhands/runtime/impl/local/local_runtime.py
openhands/runtime/impl/remote/remote_runtime.py
openhands/runtime/utils/command.py
openhands/runtime/utils/runtime_init.py
openhands/server/conversation_manager/conversation_manager.py
openhands/server/conversation_manager/docker_nested_conversation_manager.py
openhands/server/conversation_manager/standalone_conversation_manager.py
openhands/server/listen_socket.py
openhands/server/routes/conversation.py
openhands/server/routes/files.py
openhands/server/routes/git.py
openhands/server/routes/manage_conversations.py
openhands/server/routes/mcp.py
openhands/server/routes/secrets.py
openhands/server/routes/settings.py
openhands/server/services/conversation_service.py
openhands/server/services/conversation_stats.py
openhands/server/session/agent_session.py
openhands/server/session/conversation.py
openhands/server/session/session.py
openhands/server/user_auth/__init__.py
openhands/server/user_auth/default_user_auth.py
openhands/server/utils.py
openhands/storage/conversation/conversation_store.py
openhands/storage/conversation/conversation_validator.py
openhands/storage/conversation/file_conversation_store.py
openhands/storage/data_models/conversation_metadata.py
openhands/storage/data_models/user_secrets.py
openhands/storage/locations.py
openhands/storage/secrets/file_secrets_store.py
openhands/storage/secrets/secrets_store.py
openhands/storage/settings/file_settings_store.py
openhands/storage/settings/settings_store.py
openhands/utils/conversation_summary.py
openhands/utils/utils.py
tests/runtime/test_bash.py
tests/unit/controller/test_agent_controller.py
tests/unit/controller/test_agent_delegation.py
tests/unit/core/config/test_config.py
tests/unit/events/test_event_stream.py
tests/unit/events/test_nested_event_store.py
tests/unit/integrations/github/test_github_service.py
tests/unit/integrations/test_provider_immutability.py
tests/unit/runtime/test_runtime_git_tokens.py
tests/unit/server/data_models/test_conversation.py
tests/unit/server/routes/test_conversation_routes.py
tests/unit/server/routes/test_get_microagent_management_conversations.py
tests/unit/server/session/test_agent_session.py
tests/unit/server/session/test_session.py
tests/unit/storage/conversation/test_file_conversation_store.py
tests/unit/storage/data_models/test_secret_store.py
tests/unit/test_conversation_stats.py
tests/unit/test_state_metrics_exposure.py
tests/unit/utils/test_auto_generate_title.py
tests/unit/utils/test_search_utils.py
third_party/runtime/impl/daytona/daytona_runtime.py
third_party/runtime/impl/e2b/e2b_runtime.py
third_party/runtime/impl/modal/modal_runtime.py
third_party/runtime/impl/runloop/runloop_runtime.py

View File

@@ -1,68 +0,0 @@
config.template.toml
docs/openapi.json
docs/usage/configuration-options.mdx
openhands/cli/gui_launcher.py
openhands/cli/main.py
openhands/controller/agent_controller.py
openhands/controller/state/state.py
openhands/controller/state/state_tracker.py
openhands/core/config/mcp_config.py
openhands/core/config/sandbox_config.py
openhands/core/main.py
openhands/events/event_store.py
openhands/events/event_store_abc.py
openhands/events/nested_event_store.py
openhands/events/stream.py
openhands/experiments/experiment_manager.py
openhands/integrations/bitbucket/bitbucket_service.py
openhands/integrations/github/github_service.py
openhands/integrations/github/service/features.py
openhands/integrations/gitlab/gitlab_service.py
openhands/integrations/provider.py
openhands/integrations/service_types.py
openhands/resolver/issue_resolver.py
openhands/runtime/action_execution_server.py
openhands/runtime/base.py
openhands/runtime/impl/action_execution/action_execution_client.py
openhands/runtime/impl/cli/cli_runtime.py
openhands/runtime/impl/docker/docker_runtime.py
openhands/runtime/impl/kubernetes/kubernetes_runtime.py
openhands/runtime/impl/local/local_runtime.py
openhands/runtime/impl/remote/remote_runtime.py
openhands/runtime/utils/command.py
openhands/runtime/utils/runtime_init.py
openhands/server/conversation_manager/conversation_manager.py
openhands/server/conversation_manager/docker_nested_conversation_manager.py
openhands/server/conversation_manager/standalone_conversation_manager.py
openhands/server/listen_socket.py
openhands/server/routes/conversation.py
openhands/server/routes/files.py
openhands/server/routes/git.py
openhands/server/routes/manage_conversations.py
openhands/server/routes/mcp.py
openhands/server/routes/secrets.py
openhands/server/routes/settings.py
openhands/server/services/conversation_service.py
openhands/server/services/conversation_stats.py
openhands/server/session/agent_session.py
openhands/server/session/conversation.py
openhands/server/session/session.py
openhands/server/user_auth/__init__.py
openhands/server/user_auth/default_user_auth.py
openhands/server/utils.py
openhands/storage/conversation/conversation_store.py
openhands/storage/conversation/conversation_validator.py
openhands/storage/conversation/file_conversation_store.py
openhands/storage/data_models/conversation_metadata.py
openhands/storage/data_models/user_secrets.py
openhands/storage/locations.py
openhands/storage/secrets/file_secrets_store.py
openhands/storage/secrets/secrets_store.py
openhands/storage/settings/file_settings_store.py
openhands/storage/settings/settings_store.py
openhands/utils/conversation_summary.py
openhands/utils/utils.py
third_party/runtime/impl/daytona/daytona_runtime.py
third_party/runtime/impl/e2b/e2b_runtime.py
third_party/runtime/impl/modal/modal_runtime.py
third_party/runtime/impl/runloop/runloop_runtime.py

View File

@@ -1,522 +0,0 @@
config.template.toml:288:#user_id = 1000
docs/openapi.json:3309: "user_id": {
docs/usage/configuration-options.mdx:337:- `user_id`
openhands/cli/gui_launcher.py:179: user_id = subprocess.check_output(['id', '-u'], text=True).strip()
openhands/cli/gui_launcher.py:180: docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}'])
openhands/cli/main.py:103: event_stream.user_id,
openhands/cli/main.py:596: settings_store = await FileSettingsStore.get_instance(config=config, user_id=None)
openhands/controller/agent_controller.py:126: user_id: str | None = None,
openhands/controller/agent_controller.py:155: self.user_id = user_id
openhands/controller/agent_controller.py:171: self.state_tracker = StateTracker(sid, file_store, user_id)
openhands/controller/agent_controller.py:719: user_id=self.user_id,
openhands/controller/agent_controller.py:740: user_id=self.user_id,
openhands/controller/state/state.py:82: user_id: str | None = None
openhands/controller/state/state.py:123: self, sid: str, file_store: FileStore, user_id: str | None
openhands/controller/state/state.py:133: get_conversation_agent_state_filename(sid, user_id), encoded
openhands/controller/state/state.py:137: if user_id:
openhands/controller/state/state.py:151: sid: str, file_store: FileStore, user_id: str | None = None
openhands/controller/state/state.py:157: get_conversation_agent_state_filename(sid, user_id)
openhands/controller/state/state.py:162: # if user_id is provided, we are in a saas/remote use case
openhands/controller/state/state.py:164: if user_id:
openhands/controller/state/state.py:277: 'trace_user_id': self.user_id,
openhands/controller/state/state_tracker.py:32: self, sid: str | None, file_store: FileStore | None, user_id: str | None
openhands/controller/state/state_tracker.py:36: self.user_id = user_id
openhands/controller/state/state_tracker.py:75: user_id=self.user_id,
openhands/controller/state/state_tracker.py:249: self.state.save_to_session(self.sid, self.file_store, self.user_id)
openhands/core/config/mcp_config.py:337: host: str, config: 'OpenHandsConfig', user_id: str | None = None
openhands/core/config/mcp_config.py:344: user_id: Optional user ID for the MCP server
openhands/core/config/sandbox_config.py:16: user_id: The user ID for the sandbox.
openhands/core/config/sandbox_config.py:59: user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000)
openhands/core/config/sandbox_config.py:60: logger.debug(f'SandboxConfig user_id default: {user_id}')
openhands/core/main.py:225: event_stream.sid, event_stream.file_store, event_stream.user_id
openhands/events/event_store.py:49: user_id: str | None
openhands/events/event_store.py:69: events_dir = get_conversation_events_dir(self.sid, self.user_id)
openhands/events/event_store.py:139: filename = self._get_filename_for_id(id, self.user_id)
openhands/events/event_store.py:155: def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
openhands/events/event_store.py:156: return get_conversation_event_filename(self.sid, id, user_id)
openhands/events/event_store.py:159: return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
openhands/events/event_store_abc.py:15: user_id: str | None
openhands/events/nested_event_store.py:20: user_id: str | None
openhands/events/stream.py:34: sid: str, file_store: FileStore, user_id: str | None = None
openhands/events/stream.py:37: await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
openhands/events/stream.py:56: def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
openhands/events/stream.py:57: super().__init__(sid, file_store, user_id)
openhands/events/stream.py:189: filename = self._get_filename_for_id(event.id, self.user_id)
openhands/events/stream.py:194: 'user_id': self.user_id,
openhands/experiments/experiment_manager.py:34: user_id: str, conversation_id: str, conversation_settings: ConversationInitData
openhands/experiments/experiment_manager.py:40: user_id: str, conversation_id: str, config: OpenHandsConfig
openhands/integrations/bitbucket/bitbucket_service.py:40: user_id: str | None = None,
openhands/integrations/bitbucket/bitbucket_service.py:47: self.user_id = user_id
openhands/integrations/github/github_service.py:49: user_id: str | None = None,
openhands/integrations/github/github_service.py:56: self.user_id = user_id
openhands/integrations/github/service/features.py:85: 'user_id': self.external_auth_id,
openhands/integrations/github/service/features.py:116: 'user_id': self.external_auth_id,
openhands/integrations/gitlab/gitlab_service.py:47: user_id: str | None = None,
openhands/integrations/gitlab/gitlab_service.py:54: self.user_id = user_id
openhands/integrations/provider.py:43: user_id: str | None = Field(default=None)
openhands/integrations/provider.py:62: user_id = token_value.get('user_id')
openhands/integrations/provider.py:64: return cls(token=SecretStr(token_str), user_id=user_id, host=host)
openhands/integrations/provider.py:154: user_id=token.user_id,
openhands/integrations/service_types.py:442: user_id: str | None = None,
openhands/resolver/issue_resolver.py:251: user_id = os.getuid() if hasattr(os, 'getuid') else 1000
openhands/resolver/issue_resolver.py:252: if user_id == 0:
openhands/resolver/issue_resolver.py:253: sandbox_config.user_id = get_unique_uid()
openhands/resolver/issue_resolver.py:265: openhands_config.sandbox.user_id = sandbox_config.user_id
openhands/runtime/action_execution_server.py:176: user_id: int,
openhands/runtime/action_execution_server.py:183: self.user_id = user_id
openhands/runtime/action_execution_server.py:185: username=username, user_id=self.user_id, initial_cwd=work_dir
openhands/runtime/action_execution_server.py:188: self.user_id = _updated_user_id
openhands/runtime/action_execution_server.py:553: os.chown(filepath, self.user_id, self.user_id)
openhands/runtime/action_execution_server.py:700: user_id=args.user_id,
openhands/runtime/base.py:139: user_id: str | None = None,
openhands/runtime/base.py:172: external_auth_id=user_id,
openhands/runtime/base.py:191: self.user_id = user_id
openhands/runtime/base.py:336: external_auth_id=self.user_id,
openhands/runtime/base.py:901: self.sid, self.event_stream.user_id
openhands/runtime/impl/action_execution/action_execution_client.py:79: user_id: str | None = None,
openhands/runtime/impl/action_execution/action_execution_client.py:97: user_id,
openhands/runtime/impl/cli/cli_runtime.py:103: user_id (str | None, optional): User ID for authentication. Defaults to None.
openhands/runtime/impl/cli/cli_runtime.py:118: user_id: str | None = None,
openhands/runtime/impl/cli/cli_runtime.py:131: user_id,
openhands/runtime/impl/docker/docker_runtime.py:101: user_id: str | None = None,
openhands/runtime/impl/docker/docker_runtime.py:155: user_id,
openhands/runtime/impl/kubernetes/kubernetes_runtime.py:92: user_id: str | None = None,
openhands/runtime/impl/kubernetes/kubernetes_runtime.py:149: user_id,
openhands/runtime/impl/local/local_runtime.py:146: user_id: str | None = None,
openhands/runtime/impl/local/local_runtime.py:198: user_id,
openhands/runtime/impl/local/local_runtime.py:659: user_id, username = get_user_info()
openhands/runtime/impl/local/local_runtime.py:668: override_user_id=user_id,
openhands/runtime/impl/remote/remote_runtime.py:62: user_id: str | None = None,
openhands/runtime/impl/remote/remote_runtime.py:76: user_id,
openhands/runtime/impl/remote/remote_runtime.py:79: logger.debug(f'RemoteRuntime.init user_id {user_id}')
openhands/runtime/utils/command.py:46: user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
openhands/runtime/utils/command.py:61: str(user_id),
openhands/runtime/utils/runtime_init.py:9: username: str, user_id: int, initial_cwd: str
openhands/runtime/utils/runtime_init.py:20: - If the UID differs, it logs a warning and return an updated user_id.
openhands/runtime/utils/runtime_init.py:30: user_id (int): The user ID to assign to the user.
openhands/runtime/utils/runtime_init.py:55: logger.debug(f'Attempting to create user `{username}` with UID {user_id}.')
openhands/runtime/utils/runtime_init.py:64: if existing_user_id == user_id:
openhands/runtime/utils/runtime_init.py:66: f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
openhands/runtime/utils/runtime_init.py:95: f'-g root -G sudo -u {user_id} {username}'
openhands/runtime/utils/runtime_init.py:100: f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
openhands/runtime/utils/runtime_init.py:104: f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
openhands/server/conversation_manager/conversation_manager.py:61: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/conversation_manager.py:75: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:86: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:92: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:101: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:136: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:71: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:85: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:91: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:110: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:120: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:126: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/docker_nested_conversation_manager.py:138: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:152: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:157: await self.ensure_num_conversations_below_limit(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:158: runtime = await self._create_runtime(sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:225: 'user_id': v.user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:385: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:419: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:458: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/docker_nested_conversation_manager.py:465: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:491: self, sid: str, user_id: str | None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:493: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:496: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:497: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:500: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:507: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:508: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:535: self, sid: str, user_id: str | None, settings: Settings
openhands/server/conversation_manager/docker_nested_conversation_manager.py:541: user_id, sid, self.config
openhands/server/conversation_manager/docker_nested_conversation_manager.py:545: create_registry_and_conversation_stats(config, sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:555: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:584: conversation_dir = get_conversation_dir(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:602: event_stream = EventStream(sid, self.file_store, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:92: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:95: if not await session_exists(sid, self.file_store, user_id=user_id):
openhands/server/conversation_manager/standalone_conversation_manager.py:130: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:156: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:160: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:164: agent_loop_info = await self.maybe_start_agent_loop(sid, settings, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:230: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/standalone_conversation_manager.py:237: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:241: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:257: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:258: items = (item for item in items if item[1].user_id == user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:264: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:273: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:276: if not session or session.user_id != user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:284: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:292: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/standalone_conversation_manager.py:300: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:306: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:309: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/standalone_conversation_manager.py:310: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:313: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:320: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/standalone_conversation_manager.py:321: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:341: create_registry_and_conversation_stats(self.config, sid, user_id, settings)
openhands/server/conversation_manager/standalone_conversation_manager.py:350: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:361: user_id, sid, settings, session.llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:475: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:484: user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:495: user_id: str,
openhands/server/conversation_manager/standalone_conversation_manager.py:501: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:538: conversation_id, user_id, self.file_store, settings, llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:714: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:718: if user_id and session.user_id != user_id:
openhands/server/listen_socket.py:71: user_id = await conversation_validator.validate(
openhands/server/listen_socket.py:75: f'User {user_id} is allowed to connect to conversation {conversation_id}'
openhands/server/listen_socket.py:80: conversation_id, conversation_manager.file_store, user_id
openhands/server/listen_socket.py:121: user_id, conversation_id, providers_set
openhands/server/listen_socket.py:128: user_id,
openhands/server/routes/conversation.py:114: user_id: str | None = Depends(get_user_id),
openhands/server/routes/conversation.py:126: user_id: User ID (injected by dependency)
openhands/server/routes/conversation.py:145: user_id=user_id,
openhands/server/routes/files.py:233: user_id: str = Depends(get_user_id),
openhands/server/routes/git.py:43: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:49: external_auth_id=user_id,
openhands/server/routes/git.py:74: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:80: external_auth_id=user_id,
openhands/server/routes/git.py:100: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:109: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:127: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:141: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:147: external_auth_id=user_id,
openhands/server/routes/git.py:162: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:175: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:181: external_auth_id=user_id,
openhands/server/routes/git.py:202: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:214: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:235: logger.info(f'Returning 401 Unauthorized - No providers set for user_id: {user_id}')
openhands/server/routes/git.py:246: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:275: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:300: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:316: user_id: User ID for authentication
openhands/server/routes/git.py:327: external_auth_id=user_id,
openhands/server/routes/git.py:366: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:375: user_id: User ID for authentication
openhands/server/routes/git.py:389: external_auth_id=user_id,
openhands/server/routes/manage_conversations.py:193: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:251: user_id=user_id,
openhands/server/routes/manage_conversations.py:356: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:358: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/manage_conversations.py:381: sid=conversation_id, file_store=file_store, user_id=metadata.user_id
openhands/server/routes/manage_conversations.py:478: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:505: user_id, conversation_id, providers_set.providers_set or []
openhands/server/routes/manage_conversations.py:512: user_id=user_id,
openhands/server/routes/manage_conversations.py:538: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:550: user_id=user_id, filter_to_sids={conversation_id}
openhands/server/routes/manage_conversations.py:644: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:655: user_id: The authenticated user ID
openhands/server/routes/manage_conversations.py:666: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:674: if user_id and metadata.user_id != user_id:
openhands/server/routes/manage_conversations.py:676: f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',
openhands/server/routes/manage_conversations.py:677: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:715: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:723: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:736: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/mcp.py:52: user_id: str | None, conversation_id: str, tool_result: str
openhands/server/routes/mcp.py:54: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/mcp.py:106: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:115: user_id=github_token.user_id,
openhands/server/routes/mcp.py:116: external_auth_id=user_id,
openhands/server/routes/mcp.py:139: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:176: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:185: user_id=github_token.user_id,
openhands/server/routes/mcp.py:186: external_auth_id=user_id,
openhands/server/routes/mcp.py:210: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:243: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:252: user_id=bitbucket_token.user_id,
openhands/server/routes/mcp.py:253: external_auth_id=user_id,
openhands/server/routes/mcp.py:276: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/secrets.py:111: # We don't have direct access to user_id here, but we can log the provider info
openhands/server/routes/settings.py:62: if provider_token.token or provider_token.user_id:
openhands/server/routes/settings.py:79: # Get user_id from settings if available
openhands/server/routes/settings.py:80: user_id = getattr(settings, 'user_id', 'unknown') if settings else 'unknown'
openhands/server/routes/settings.py:82: f'Returning 401 Unauthorized - Invalid token for user_id: {user_id}'
openhands/server/services/conversation_service.py:35: user_id: str | None,
openhands/server/services/conversation_service.py:45: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:50: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:60: user_id=user_id,
openhands/server/services/conversation_service.py:79: user_id: str | None,
openhands/server/services/conversation_service.py:94: 'user_id': user_id,
openhands/server/services/conversation_service.py:99: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:133: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_service.py:138: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:151: user_id,
openhands/server/services/conversation_service.py:160: user_id: str | None,
openhands/server/services/conversation_service.py:175: user_id,
openhands/server/services/conversation_service.py:187: user_id,
openhands/server/services/conversation_service.py:207: provider_information[provider] = ProviderToken(token=None, user_id=None)
openhands/server/services/conversation_service.py:213: user_id: str | None, conversation_id: str, providers_set: list[ProviderType]
openhands/server/services/conversation_service.py:218: user_id: The user ID
openhands/server/services/conversation_service.py:225: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:228: secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:254: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_stats.py:19: user_id: str | None,
openhands/server/services/conversation_stats.py:21: self.metrics_path = get_conversation_stats_filename(conversation_id, user_id)
openhands/server/services/conversation_stats.py:24: self.user_id = user_id
openhands/server/session/agent_session.py:50: user_id: str | None
openhands/server/session/agent_session.py:71: user_id: str | None = None,
openhands/server/session/agent_session.py:80: self.event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/agent_session.py:83: self.user_id = user_id
openhands/server/session/agent_session.py:85: extra={'session_id': sid, 'user_id': user_id}
openhands/server/session/agent_session.py:342: user_id=self.user_id,
openhands/server/session/agent_session.py:429: user_id=self.user_id,
openhands/server/session/agent_session.py:501: self.sid, self.file_store, self.user_id
openhands/server/session/conversation.py:17: user_id: str | None
openhands/server/session/conversation.py:25: user_id: str | None,
openhands/server/session/conversation.py:32: self.user_id = user_id
openhands/server/session/conversation.py:35: event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/session.py:50: user_id: str | None
openhands/server/session/session.py:61: user_id: str | None = None,
openhands/server/session/session.py:76: user_id=user_id,
openhands/server/session/session.py:87: user_id, sid, self.config
openhands/server/session/session.py:90: self.user_id = user_id
openhands/server/session/session.py:178: self.config.mcp_host, self.config, self.user_id
openhands/server/user_auth/__init__.py:26: user_id = await user_auth.get_user_id()
openhands/server/user_auth/__init__.py:27: return user_id
openhands/server/user_auth/default_user_auth.py:25: """The default implementation does not support multi tenancy, so user_id is always None"""
openhands/server/user_auth/default_user_auth.py:40: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:42: shared.config, user_id
openhands/server/user_auth/default_user_auth.py:67: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:69: shared.config, user_id
openhands/server/utils.py:66: user_id = await get_user_id(request)
openhands/server/utils.py:67: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/utils.py:97: conversation_id: str, user_id: str | None = Depends(get_user_id)
openhands/server/utils.py:101: conversation_id, user_id
openhands/server/utils.py:106: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/storage/conversation/conversation_store.py:36: async def validate_metadata(self, conversation_id: str, user_id: str) -> bool:
openhands/storage/conversation/conversation_store.py:39: if not metadata.user_id or metadata.user_id != user_id:
openhands/storage/conversation/conversation_store.py:69: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/conversation/conversation_validator.py:33: user_id = None
openhands/storage/conversation/conversation_validator.py:34: metadata = await self._ensure_metadata_exists(conversation_id, user_id)
openhands/storage/conversation/conversation_validator.py:35: return metadata.user_id
openhands/storage/conversation/conversation_validator.py:40: user_id: str | None,
openhands/storage/conversation/conversation_validator.py:50: config, user_id
openhands/storage/conversation/conversation_validator.py:63: user_id=user_id,
openhands/storage/conversation/file_conversation_store.py:106: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/data_models/conversation_metadata.py:24: user_id: str | None = None
openhands/storage/data_models/user_secrets.py:69: 'user_id': provider_token.user_id,
openhands/storage/locations.py:4:def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:5: if user_id:
openhands/storage/locations.py:6: return f'users/{user_id}/conversations/{sid}/'
openhands/storage/locations.py:11:def get_conversation_events_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:12: return f'{get_conversation_dir(sid, user_id)}events/'
openhands/storage/locations.py:16: sid: str, id: int, user_id: str | None = None
openhands/storage/locations.py:18: return f'{get_conversation_events_dir(sid, user_id)}{id}.json'
openhands/storage/locations.py:21:def get_conversation_metadata_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:22: return f'{get_conversation_dir(sid, user_id)}metadata.json'
openhands/storage/locations.py:25:def get_conversation_init_data_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:26: return f'{get_conversation_dir(sid, user_id)}init.json'
openhands/storage/locations.py:29:def get_conversation_agent_state_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:30: return f'{get_conversation_dir(sid, user_id)}agent_state.pkl'
openhands/storage/locations.py:33:def get_conversation_llm_registry_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:34: return f'{get_conversation_dir(sid, user_id)}llm_registry.json'
openhands/storage/locations.py:37:def get_conversation_stats_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:38: return f'{get_conversation_dir(sid, user_id)}conversation_stats.pkl'
openhands/storage/locations.py:41:def get_experiment_config_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:42: return f'{get_conversation_dir(sid, user_id)}exp_config.json'
openhands/storage/secrets/file_secrets_store.py:40: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/secrets/secrets_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/file_settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/utils/conversation_summary.py:81: user_id: str | None,
openhands/utils/conversation_summary.py:91: user_id: The ID of the user
openhands/utils/conversation_summary.py:98: event_store = EventStore(conversation_id, file_store, user_id)
openhands/utils/utils.py:25: user_id: str | None,
openhands/utils/utils.py:41: conversation_stats = ConversationStats(file_store, sid, user_id)
tests/runtime/test_bash.py:806: # while the user_id we get via os.getuid() is different from root
tests/unit/controller/test_agent_controller.py:79: file_store=file_store, conversation_id=conversation_id, user_id='test-user'
tests/unit/controller/test_agent_delegation.py:53: file_store=file_store, conversation_id=conversation_id, user_id='test-user'
tests/unit/core/config/test_config.py:231:user_id = 1001
tests/unit/core/config/test_config.py:258: assert default_config.sandbox.user_id == 1002
tests/unit/core/config/test_config.py:280:user_id = 1001
tests/unit/core/config/test_config.py:297: assert default_config.sandbox.user_id == 1001
tests/unit/core/config/test_config.py:307: assert default_config.sandbox.user_id == 1002
tests/unit/core/config/test_config.py:329:user_id = 1001
tests/unit/core/config/test_config.py:345: assert default_config.sandbox.user_id == 1001
tests/unit/core/config/test_config.py:536:user_id = 1001
tests/unit/core/config/test_config.py:546: assert default_config.sandbox.user_id == 1001
tests/unit/events/test_event_stream.py:178: event_stream._get_filename_for_id(event.id, event_stream.user_id),
tests/unit/events/test_event_stream.py:717: missing_filename = new_stream._get_filename_for_id(missing_id, new_stream.user_id)
tests/unit/events/test_nested_event_store.py:56: user_id='test-user',
tests/unit/events/test_nested_event_store.py:330: user_id='test-user',
tests/unit/events/test_nested_event_store.py:395: server_stream = EventStream('test-session', fs, user_id='test-user')
tests/unit/integrations/github/test_github_service.py:22: service = GitHubService(user_id=None, token=token)
tests/unit/integrations/github/test_github_service.py:32: service = GitHubService(user_id='test-user')
tests/unit/integrations/github/test_github_service.py:40: service = GitHubService(user_id=None, token=token)
tests/unit/integrations/github/test_github_service.py:68: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:94: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:133: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:172: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:212: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:253: service = GitHubService(user_id='test-user', token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:320: service = GitHubService(user_id='test-user', token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:336: service = GitHubService(user_id='test-user', token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:349: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/github/test_github_service.py:354: user_id=None, token=SecretStr('test-token'), base_domain='github.enterprise.com'
tests/unit/integrations/github/test_github_service.py:360: user_id=None, token=SecretStr('test-token'), base_domain='github.com'
tests/unit/integrations/github/test_github_service.py:382: user_id=None,
tests/unit/integrations/github/test_github_service.py:419: service = GitHubService(user_id=None, token=SecretStr('test-token'))
tests/unit/integrations/test_provider_immutability.py:18: token = ProviderToken(token=SecretStr('test'), user_id='user1')
tests/unit/integrations/test_provider_immutability.py:25: token.user_id = 'new_user'
tests/unit/integrations/test_provider_immutability.py:33: assert token.user_id == 'user1'
tests/unit/integrations/test_provider_immutability.py:159: assert store1.secrets_store.provider_tokens[ProviderType.GITHUB].user_id is None
tests/unit/integrations/test_provider_immutability.py:163: provider_tokens={'github': {'token': 'test_token', 'user_id': 'user1'}}
tests/unit/integrations/test_provider_immutability.py:169: assert store2.provider_tokens[ProviderType.GITHUB].user_id == 'user1'
tests/unit/integrations/test_provider_immutability.py:172: token = ProviderToken(token=SecretStr('test_token'), user_id='user2')
tests/unit/integrations/test_provider_immutability.py:178: assert store3.provider_tokens[ProviderType.GITHUB].user_id == 'user2'
tests/unit/runtime/test_runtime_git_tokens.py:110: user_id='test_user',
tests/unit/runtime/test_runtime_git_tokens.py:132: """Test that no token export happens when user_id is not set"""
tests/unit/runtime/test_runtime_git_tokens.py:155: config=config, event_stream=event_stream, sid='test', user_id='test_user'
tests/unit/runtime/test_runtime_git_tokens.py:198: user_id='test_user',
tests/unit/runtime/test_runtime_git_tokens.py:249: config=config, event_stream=event_stream, sid='test', user_id=None
tests/unit/runtime/test_runtime_git_tokens.py:267: """Test that git init is not run when no repository is selected, no user_id, but workspace_base is set"""
tests/unit/runtime/test_runtime_git_tokens.py:273: config=config, event_stream=event_stream, sid='test', user_id=None
tests/unit/runtime/test_runtime_git_tokens.py:291: config=config, event_stream=event_stream, sid='test', user_id='test_user'
tests/unit/runtime/test_runtime_git_tokens.py:325: user_id='test_user',
tests/unit/runtime/test_runtime_git_tokens.py:360: config=config, event_stream=event_stream, sid='test', user_id='test_user'
tests/unit/runtime/test_runtime_git_tokens.py:398: user_id='test_user',
tests/unit/runtime/test_runtime_git_tokens.py:434: config=config, event_stream=event_stream, sid='test', user_id='test_user'
tests/unit/server/data_models/test_conversation.py:54: 'user_id': '12345',
tests/unit/server/data_models/test_conversation.py:88: user_id='test_user',
tests/unit/server/data_models/test_conversation.py:153: user_id='12345',
tests/unit/server/data_models/test_conversation.py:237: user_id='12345',
tests/unit/server/data_models/test_conversation.py:308: user_id='12345',
tests/unit/server/data_models/test_conversation.py:379: user_id='12345',
tests/unit/server/data_models/test_conversation.py:451: user_id='12345',
tests/unit/server/data_models/test_conversation.py:522: user_id='12345',
tests/unit/server/data_models/test_conversation.py:617: user_id='12345',
tests/unit/server/data_models/test_conversation.py:698: assert call_args['user_id'] == 'test_user'
tests/unit/server/data_models/test_conversation.py:757: assert call_args['user_id'] == 'test_user'
tests/unit/server/data_models/test_conversation.py:853: user_id='12345',
tests/unit/server/data_models/test_conversation.py:878: 'some_conversation_id', user_id='12345'
tests/unit/server/data_models/test_conversation.py:1068: assert call_args['user_id'] == 'test_user'
tests/unit/server/data_models/test_conversation.py:1127: assert call_args['user_id'] == 'test_user'
tests/unit/server/data_models/test_conversation.py:1182: assert call_args['user_id'] == 'test_user'
tests/unit/server/data_models/test_conversation.py:1245: user_id='12345',
tests/unit/server/data_models/test_conversation.py:1316: user_id='12345',
tests/unit/server/data_models/test_conversation.py:1387: user_id='12345',
tests/unit/server/data_models/test_conversation.py:1423: user_id='12345',
tests/unit/server/data_models/test_conversation.py:1502: user_id='12345',
tests/unit/server/data_models/test_conversation.py:1515: user_id='12345',
tests/unit/server/data_models/test_conversation.py:1528: user_id='12345',
tests/unit/server/routes/test_conversation_routes.py:174: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:181: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:207: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:236: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:249: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:269: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:275: user_id=owner_id,
tests/unit/server/routes/test_conversation_routes.py:292: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:313: """Test conversation update when user_id is None and metadata has user_id."""
tests/unit/server/routes/test_conversation_routes.py:315: user_id = None
tests/unit/server/routes/test_conversation_routes.py:321: user_id=owner_id,
tests/unit/server/routes/test_conversation_routes.py:338: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:351: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:357: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:384: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:400: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:415: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:435: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:442: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:468: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:486: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:492: user_id=user_id, # Same user
tests/unit/server/routes/test_conversation_routes.py:518: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:532: user_id = 'test_user_456'
tests/unit/server/routes/test_conversation_routes.py:539: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:565: user_id=user_id,
tests/unit/server/routes/test_conversation_routes.py:581: """Test successful update when both user_id and metadata.user_id are None."""
tests/unit/server/routes/test_conversation_routes.py:583: user_id = None
tests/unit/server/routes/test_conversation_routes.py:586: # Create mock metadata with no user_id
tests/unit/server/routes/test_conversation_routes.py:589: user_id=None,
tests/unit/server/routes/test_conversation_routes.py:615: user_id=user_id,
tests/unit/server/routes/test_get_microagent_management_conversations.py:32: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:43: user_id='user_2',
tests/unit/server/routes/test_get_microagent_management_conversations.py:152: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:163: user_id='user_2',
tests/unit/server/routes/test_get_microagent_management_conversations.py:225: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:236: user_id='user_2',
tests/unit/server/routes/test_get_microagent_management_conversations.py:298: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:309: user_id='user_2',
tests/unit/server/routes/test_get_microagent_management_conversations.py:373: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:436: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:498: user_id='user_1',
tests/unit/server/routes/test_get_microagent_management_conversations.py:510: user_id='user_2',
tests/unit/server/session/test_agent_session.py:38: file_store=file_store, conversation_id='test-conversation', user_id='test-user'
tests/unit/server/session/test_session.py:48: file_store=file_store, conversation_id='test-conversation', user_id='test-user'
tests/unit/server/session/test_session.py:70: user_id='..uid..',
tests/unit/storage/conversation/test_file_conversation_store.py:16: user_id='some-user-id',
tests/unit/storage/conversation/test_file_conversation_store.py:33: 'user_id': '67890',
tests/unit/storage/conversation/test_file_conversation_store.py:43: assert found.user_id == '67890'
tests/unit/storage/conversation/test_file_conversation_store.py:63: 'user_id': '123',
tests/unit/storage/conversation/test_file_conversation_store.py:72: 'user_id': '123',
tests/unit/storage/conversation/test_file_conversation_store.py:81: 'user_id': '123',
tests/unit/storage/conversation/test_file_conversation_store.py:109: 'user_id': '123',
tests/unit/storage/conversation/test_file_conversation_store.py:150: 'user_id': '123',
tests/unit/storage/conversation/test_file_conversation_store.py:178: 'user_id': '123',
tests/unit/storage/conversation/test_file_conversation_store.py:187: 'user_id': '123',
tests/unit/storage/data_models/test_secret_store.py:21: token=SecretStr('github-token-123'), user_id='user1'
tests/unit/storage/data_models/test_secret_store.py:24: token=SecretStr('gitlab-token-456'), user_id='user2'
tests/unit/storage/data_models/test_secret_store.py:43: assert store.provider_tokens[ProviderType.GITHUB].user_id == 'user1'
tests/unit/storage/data_models/test_secret_store.py:48: assert store.provider_tokens[ProviderType.GITLAB].user_id == 'user2'
tests/unit/storage/data_models/test_secret_store.py:88: ProviderType.GITHUB: {'token': 'github-token-123', 'user_id': 'user1'},
tests/unit/storage/data_models/test_secret_store.py:114: token=SecretStr('gitlab-token-456'), user_id='user2'
tests/unit/storage/data_models/test_secret_store.py:141: token=SecretStr('github-token-123'), user_id='user1'
tests/unit/storage/data_models/test_secret_store.py:156: token=SecretStr('gitlab-token-456'), user_id='user2'
tests/unit/storage/data_models/test_secret_store.py:218: token=SecretStr('github-token-123'), user_id='user1'
tests/unit/storage/data_models/test_secret_store.py:242: assert serialized_provider_tokens['github']['user_id'] == 'user1'
tests/unit/storage/data_models/test_secret_store.py:273: 'user_id': 'user1',
tests/unit/storage/data_models/test_secret_store.py:277: 'user_id': 'user2',
tests/unit/storage/data_models/test_secret_store.py:283: token=SecretStr('gitlab-token-456'), user_id='user2'
tests/unit/storage/data_models/test_secret_store.py:303: assert github_token.user_id == 'user1'
tests/unit/storage/data_models/test_secret_store.py:309: assert gitlab_token_result.user_id == 'user2'
tests/unit/test_conversation_stats.py:27: user_id='test-user-id',
tests/unit/test_conversation_stats.py:50: assert conversation_stats.user_id == 'test-user-id'
tests/unit/test_conversation_stats.py:93: user_id = 'test-user-id'
tests/unit/test_conversation_stats.py:98: metrics_path = get_conversation_stats_filename(conversation_id, user_id)
tests/unit/test_conversation_stats.py:105: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_conversation_stats.py:429: user_id = 'test-user-id'
tests/unit/test_conversation_stats.py:432: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_conversation_stats.py:454: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_conversation_stats.py:499: file_store=mock_file_store, conversation_id='conv-merge-a', user_id='user-x'
tests/unit/test_conversation_stats.py:502: file_store=mock_file_store, conversation_id='conv-merge-b', user_id='user-x'
tests/unit/test_conversation_stats.py:565: file_store=mock_file_store, conversation_id='conv-merge-a', user_id='user-x'
tests/unit/test_conversation_stats.py:568: file_store=mock_file_store, conversation_id='conv-merge-b', user_id='user-x'
tests/unit/test_conversation_stats.py:607: user_id = 'test-user-id'
tests/unit/test_conversation_stats.py:611: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_conversation_stats.py:637: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_conversation_stats.py:678: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_conversation_stats.py:696: user_id = 'test-user-id'
tests/unit/test_conversation_stats.py:699: file_store=mock_file_store, conversation_id=conversation_id, user_id=user_id
tests/unit/test_state_metrics_exposure.py:15: self.user_id = None
tests/unit/test_state_metrics_exposure.py:82: file_store=store, conversation_id='cid', user_id=None
tests/unit/test_state_metrics_exposure.py:89: tracker = StateTracker(sid='sid', file_store=store, user_id=None)
tests/unit/utils/test_auto_generate_title.py:31: user_id = 'test-user'
tests/unit/utils/test_auto_generate_title.py:64: conversation_id, user_id, file_store, settings, llm_registry
tests/unit/utils/test_auto_generate_title.py:72: conversation_id, file_store, user_id
tests/unit/utils/test_auto_generate_title.py:88: user_id = 'test-user'
tests/unit/utils/test_auto_generate_title.py:118: conversation_id, user_id, file_store, settings, llm_registry
tests/unit/utils/test_auto_generate_title.py:127: conversation_id, file_store, user_id
tests/unit/utils/test_auto_generate_title.py:140: user_id = 'test-user'
tests/unit/utils/test_auto_generate_title.py:160: conversation_id, user_id, file_store, settings, llm_registry
tests/unit/utils/test_auto_generate_title.py:168: conversation_id, file_store, user_id
tests/unit/utils/test_auto_generate_title.py:184: user_id = 'test-user'
tests/unit/utils/test_auto_generate_title.py:218: user_id, conversation_id, settings, llm_registry
tests/unit/utils/test_search_utils.py:52: 'user_id': '123',
tests/unit/utils/test_search_utils.py:62: 'user_id': '123',
tests/unit/utils/test_search_utils.py:91: 'user_id': '123',
tests/unit/utils/test_search_utils.py:126: 'user_id': '123',
third_party/runtime/impl/daytona/daytona_runtime.py:46: user_id: str | None = None,
third_party/runtime/impl/daytona/daytona_runtime.py:86: user_id,
third_party/runtime/impl/e2b/e2b_runtime.py:36: user_id: str | None = None,
third_party/runtime/impl/e2b/e2b_runtime.py:49: user_id,
third_party/runtime/impl/modal/modal_runtime.py:58: user_id: str | None = None,
third_party/runtime/impl/modal/modal_runtime.py:117: user_id,
third_party/runtime/impl/runloop/runloop_runtime.py:41: user_id: str | None = None,
third_party/runtime/impl/runloop/runloop_runtime.py:66: user_id,

View File

@@ -1,339 +0,0 @@
config.template.toml:288:#user_id = 1000
docs/openapi.json:3309: "user_id": {
docs/usage/configuration-options.mdx:337:- `user_id`
openhands/cli/gui_launcher.py:179: user_id = subprocess.check_output(['id', '-u'], text=True).strip()
openhands/cli/gui_launcher.py:180: docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}'])
openhands/cli/main.py:103: event_stream.user_id,
openhands/cli/main.py:596: settings_store = await FileSettingsStore.get_instance(config=config, user_id=None)
openhands/controller/agent_controller.py:126: user_id: str | None = None,
openhands/controller/agent_controller.py:155: self.user_id = user_id
openhands/controller/agent_controller.py:171: self.state_tracker = StateTracker(sid, file_store, user_id)
openhands/controller/agent_controller.py:719: user_id=self.user_id,
openhands/controller/agent_controller.py:740: user_id=self.user_id,
openhands/controller/state/state.py:82: user_id: str | None = None
openhands/controller/state/state.py:123: self, sid: str, file_store: FileStore, user_id: str | None
openhands/controller/state/state.py:133: get_conversation_agent_state_filename(sid, user_id), encoded
openhands/controller/state/state.py:137: if user_id:
openhands/controller/state/state.py:151: sid: str, file_store: FileStore, user_id: str | None = None
openhands/controller/state/state.py:157: get_conversation_agent_state_filename(sid, user_id)
openhands/controller/state/state.py:162: # if user_id is provided, we are in a saas/remote use case
openhands/controller/state/state.py:164: if user_id:
openhands/controller/state/state.py:277: 'trace_user_id': self.user_id,
openhands/controller/state/state_tracker.py:32: self, sid: str | None, file_store: FileStore | None, user_id: str | None
openhands/controller/state/state_tracker.py:36: self.user_id = user_id
openhands/controller/state/state_tracker.py:75: user_id=self.user_id,
openhands/controller/state/state_tracker.py:249: self.state.save_to_session(self.sid, self.file_store, self.user_id)
openhands/core/config/mcp_config.py:337: host: str, config: 'OpenHandsConfig', user_id: str | None = None
openhands/core/config/mcp_config.py:344: user_id: Optional user ID for the MCP server
openhands/core/config/sandbox_config.py:16: user_id: The user ID for the sandbox.
openhands/core/config/sandbox_config.py:59: user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000)
openhands/core/config/sandbox_config.py:60: logger.debug(f'SandboxConfig user_id default: {user_id}')
openhands/core/main.py:225: event_stream.sid, event_stream.file_store, event_stream.user_id
openhands/events/event_store.py:49: user_id: str | None
openhands/events/event_store.py:69: events_dir = get_conversation_events_dir(self.sid, self.user_id)
openhands/events/event_store.py:139: filename = self._get_filename_for_id(id, self.user_id)
openhands/events/event_store.py:155: def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
openhands/events/event_store.py:156: return get_conversation_event_filename(self.sid, id, user_id)
openhands/events/event_store.py:159: return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
openhands/events/event_store_abc.py:15: user_id: str | None
openhands/events/nested_event_store.py:20: user_id: str | None
openhands/events/stream.py:34: sid: str, file_store: FileStore, user_id: str | None = None
openhands/events/stream.py:37: await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
openhands/events/stream.py:56: def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
openhands/events/stream.py:57: super().__init__(sid, file_store, user_id)
openhands/events/stream.py:189: filename = self._get_filename_for_id(event.id, self.user_id)
openhands/events/stream.py:194: 'user_id': self.user_id,
openhands/experiments/experiment_manager.py:34: user_id: str, conversation_id: str, conversation_settings: ConversationInitData
openhands/experiments/experiment_manager.py:40: user_id: str, conversation_id: str, config: OpenHandsConfig
openhands/integrations/bitbucket/bitbucket_service.py:40: user_id: str | None = None,
openhands/integrations/bitbucket/bitbucket_service.py:47: self.user_id = user_id
openhands/integrations/github/github_service.py:49: user_id: str | None = None,
openhands/integrations/github/github_service.py:56: self.user_id = user_id
openhands/integrations/github/service/features.py:85: 'user_id': self.external_auth_id,
openhands/integrations/github/service/features.py:116: 'user_id': self.external_auth_id,
openhands/integrations/gitlab/gitlab_service.py:47: user_id: str | None = None,
openhands/integrations/gitlab/gitlab_service.py:54: self.user_id = user_id
openhands/integrations/provider.py:43: user_id: str | None = Field(default=None)
openhands/integrations/provider.py:62: user_id = token_value.get('user_id')
openhands/integrations/provider.py:64: return cls(token=SecretStr(token_str), user_id=user_id, host=host)
openhands/integrations/provider.py:154: user_id=token.user_id,
openhands/integrations/service_types.py:442: user_id: str | None = None,
openhands/resolver/issue_resolver.py:251: user_id = os.getuid() if hasattr(os, 'getuid') else 1000
openhands/resolver/issue_resolver.py:252: if user_id == 0:
openhands/resolver/issue_resolver.py:253: sandbox_config.user_id = get_unique_uid()
openhands/resolver/issue_resolver.py:265: openhands_config.sandbox.user_id = sandbox_config.user_id
openhands/runtime/action_execution_server.py:176: user_id: int,
openhands/runtime/action_execution_server.py:183: self.user_id = user_id
openhands/runtime/action_execution_server.py:185: username=username, user_id=self.user_id, initial_cwd=work_dir
openhands/runtime/action_execution_server.py:188: self.user_id = _updated_user_id
openhands/runtime/action_execution_server.py:553: os.chown(filepath, self.user_id, self.user_id)
openhands/runtime/action_execution_server.py:700: user_id=args.user_id,
openhands/runtime/base.py:139: user_id: str | None = None,
openhands/runtime/base.py:172: external_auth_id=user_id,
openhands/runtime/base.py:191: self.user_id = user_id
openhands/runtime/base.py:336: external_auth_id=self.user_id,
openhands/runtime/base.py:901: self.sid, self.event_stream.user_id
openhands/runtime/impl/action_execution/action_execution_client.py:79: user_id: str | None = None,
openhands/runtime/impl/action_execution/action_execution_client.py:97: user_id,
openhands/runtime/impl/cli/cli_runtime.py:103: user_id (str | None, optional): User ID for authentication. Defaults to None.
openhands/runtime/impl/cli/cli_runtime.py:118: user_id: str | None = None,
openhands/runtime/impl/cli/cli_runtime.py:131: user_id,
openhands/runtime/impl/docker/docker_runtime.py:101: user_id: str | None = None,
openhands/runtime/impl/docker/docker_runtime.py:155: user_id,
openhands/runtime/impl/kubernetes/kubernetes_runtime.py:92: user_id: str | None = None,
openhands/runtime/impl/kubernetes/kubernetes_runtime.py:149: user_id,
openhands/runtime/impl/local/local_runtime.py:146: user_id: str | None = None,
openhands/runtime/impl/local/local_runtime.py:198: user_id,
openhands/runtime/impl/local/local_runtime.py:659: user_id, username = get_user_info()
openhands/runtime/impl/local/local_runtime.py:668: override_user_id=user_id,
openhands/runtime/impl/remote/remote_runtime.py:62: user_id: str | None = None,
openhands/runtime/impl/remote/remote_runtime.py:76: user_id,
openhands/runtime/impl/remote/remote_runtime.py:79: logger.debug(f'RemoteRuntime.init user_id {user_id}')
openhands/runtime/utils/command.py:46: user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
openhands/runtime/utils/command.py:61: str(user_id),
openhands/runtime/utils/runtime_init.py:9: username: str, user_id: int, initial_cwd: str
openhands/runtime/utils/runtime_init.py:20: - If the UID differs, it logs a warning and return an updated user_id.
openhands/runtime/utils/runtime_init.py:30: user_id (int): The user ID to assign to the user.
openhands/runtime/utils/runtime_init.py:55: logger.debug(f'Attempting to create user `{username}` with UID {user_id}.')
openhands/runtime/utils/runtime_init.py:64: if existing_user_id == user_id:
openhands/runtime/utils/runtime_init.py:66: f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
openhands/runtime/utils/runtime_init.py:95: f'-g root -G sudo -u {user_id} {username}'
openhands/runtime/utils/runtime_init.py:100: f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
openhands/runtime/utils/runtime_init.py:104: f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
openhands/server/conversation_manager/conversation_manager.py:61: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/conversation_manager.py:75: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:86: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:92: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:101: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:136: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:71: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:85: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:91: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:110: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:120: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:126: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/docker_nested_conversation_manager.py:138: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:152: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:157: await self.ensure_num_conversations_below_limit(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:158: runtime = await self._create_runtime(sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:225: 'user_id': v.user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:385: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:419: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:458: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/docker_nested_conversation_manager.py:465: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:491: self, sid: str, user_id: str | None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:493: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:496: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:497: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:500: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:507: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:508: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:535: self, sid: str, user_id: str | None, settings: Settings
openhands/server/conversation_manager/docker_nested_conversation_manager.py:541: user_id, sid, self.config
openhands/server/conversation_manager/docker_nested_conversation_manager.py:545: create_registry_and_conversation_stats(config, sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:555: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:584: conversation_dir = get_conversation_dir(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:602: event_stream = EventStream(sid, self.file_store, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:92: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:95: if not await session_exists(sid, self.file_store, user_id=user_id):
openhands/server/conversation_manager/standalone_conversation_manager.py:130: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:156: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:160: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:164: agent_loop_info = await self.maybe_start_agent_loop(sid, settings, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:230: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/standalone_conversation_manager.py:237: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:241: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:257: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:258: items = (item for item in items if item[1].user_id == user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:264: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:273: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:276: if not session or session.user_id != user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:284: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:292: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/standalone_conversation_manager.py:300: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:306: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:309: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/standalone_conversation_manager.py:310: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:313: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:320: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/standalone_conversation_manager.py:321: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:341: create_registry_and_conversation_stats(self.config, sid, user_id, settings)
openhands/server/conversation_manager/standalone_conversation_manager.py:350: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:361: user_id, sid, settings, session.llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:475: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:484: user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:495: user_id: str,
openhands/server/conversation_manager/standalone_conversation_manager.py:501: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:538: conversation_id, user_id, self.file_store, settings, llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:714: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:718: if user_id and session.user_id != user_id:
openhands/server/listen_socket.py:71: user_id = await conversation_validator.validate(
openhands/server/listen_socket.py:75: f'User {user_id} is allowed to connect to conversation {conversation_id}'
openhands/server/listen_socket.py:80: conversation_id, conversation_manager.file_store, user_id
openhands/server/listen_socket.py:121: user_id, conversation_id, providers_set
openhands/server/listen_socket.py:128: user_id,
openhands/server/routes/conversation.py:114: user_id: str | None = Depends(get_user_id),
openhands/server/routes/conversation.py:126: user_id: User ID (injected by dependency)
openhands/server/routes/conversation.py:145: user_id=user_id,
openhands/server/routes/files.py:233: user_id: str = Depends(get_user_id),
openhands/server/routes/git.py:43: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:49: external_auth_id=user_id,
openhands/server/routes/git.py:74: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:80: external_auth_id=user_id,
openhands/server/routes/git.py:100: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:109: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:127: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:141: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:147: external_auth_id=user_id,
openhands/server/routes/git.py:162: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:175: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:181: external_auth_id=user_id,
openhands/server/routes/git.py:202: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:214: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:235: logger.info(f'Returning 401 Unauthorized - No providers set for user_id: {user_id}')
openhands/server/routes/git.py:246: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:275: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:300: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:316: user_id: User ID for authentication
openhands/server/routes/git.py:327: external_auth_id=user_id,
openhands/server/routes/git.py:366: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:375: user_id: User ID for authentication
openhands/server/routes/git.py:389: external_auth_id=user_id,
openhands/server/routes/manage_conversations.py:193: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:251: user_id=user_id,
openhands/server/routes/manage_conversations.py:356: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:358: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/manage_conversations.py:381: sid=conversation_id, file_store=file_store, user_id=metadata.user_id
openhands/server/routes/manage_conversations.py:478: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:505: user_id, conversation_id, providers_set.providers_set or []
openhands/server/routes/manage_conversations.py:512: user_id=user_id,
openhands/server/routes/manage_conversations.py:538: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:550: user_id=user_id, filter_to_sids={conversation_id}
openhands/server/routes/manage_conversations.py:644: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:655: user_id: The authenticated user ID
openhands/server/routes/manage_conversations.py:666: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:674: if user_id and metadata.user_id != user_id:
openhands/server/routes/manage_conversations.py:676: f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',
openhands/server/routes/manage_conversations.py:677: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:715: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:723: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:736: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/mcp.py:52: user_id: str | None, conversation_id: str, tool_result: str
openhands/server/routes/mcp.py:54: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/mcp.py:106: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:115: user_id=github_token.user_id,
openhands/server/routes/mcp.py:116: external_auth_id=user_id,
openhands/server/routes/mcp.py:139: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:176: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:185: user_id=github_token.user_id,
openhands/server/routes/mcp.py:186: external_auth_id=user_id,
openhands/server/routes/mcp.py:210: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:243: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:252: user_id=bitbucket_token.user_id,
openhands/server/routes/mcp.py:253: external_auth_id=user_id,
openhands/server/routes/mcp.py:276: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/secrets.py:111: # We don't have direct access to user_id here, but we can log the provider info
openhands/server/routes/settings.py:62: if provider_token.token or provider_token.user_id:
openhands/server/routes/settings.py:79: # Get user_id from settings if available
openhands/server/routes/settings.py:80: user_id = getattr(settings, 'user_id', 'unknown') if settings else 'unknown'
openhands/server/routes/settings.py:82: f'Returning 401 Unauthorized - Invalid token for user_id: {user_id}'
openhands/server/services/conversation_service.py:35: user_id: str | None,
openhands/server/services/conversation_service.py:45: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:50: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:60: user_id=user_id,
openhands/server/services/conversation_service.py:79: user_id: str | None,
openhands/server/services/conversation_service.py:94: 'user_id': user_id,
openhands/server/services/conversation_service.py:99: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:133: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_service.py:138: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:151: user_id,
openhands/server/services/conversation_service.py:160: user_id: str | None,
openhands/server/services/conversation_service.py:175: user_id,
openhands/server/services/conversation_service.py:187: user_id,
openhands/server/services/conversation_service.py:207: provider_information[provider] = ProviderToken(token=None, user_id=None)
openhands/server/services/conversation_service.py:213: user_id: str | None, conversation_id: str, providers_set: list[ProviderType]
openhands/server/services/conversation_service.py:218: user_id: The user ID
openhands/server/services/conversation_service.py:225: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:228: secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:254: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_stats.py:19: user_id: str | None,
openhands/server/services/conversation_stats.py:21: self.metrics_path = get_conversation_stats_filename(conversation_id, user_id)
openhands/server/services/conversation_stats.py:24: self.user_id = user_id
openhands/server/session/agent_session.py:50: user_id: str | None
openhands/server/session/agent_session.py:71: user_id: str | None = None,
openhands/server/session/agent_session.py:80: self.event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/agent_session.py:83: self.user_id = user_id
openhands/server/session/agent_session.py:85: extra={'session_id': sid, 'user_id': user_id}
openhands/server/session/agent_session.py:342: user_id=self.user_id,
openhands/server/session/agent_session.py:429: user_id=self.user_id,
openhands/server/session/agent_session.py:501: self.sid, self.file_store, self.user_id
openhands/server/session/conversation.py:17: user_id: str | None
openhands/server/session/conversation.py:25: user_id: str | None,
openhands/server/session/conversation.py:32: self.user_id = user_id
openhands/server/session/conversation.py:35: event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/session.py:50: user_id: str | None
openhands/server/session/session.py:61: user_id: str | None = None,
openhands/server/session/session.py:76: user_id=user_id,
openhands/server/session/session.py:87: user_id, sid, self.config
openhands/server/session/session.py:90: self.user_id = user_id
openhands/server/session/session.py:178: self.config.mcp_host, self.config, self.user_id
openhands/server/user_auth/__init__.py:26: user_id = await user_auth.get_user_id()
openhands/server/user_auth/__init__.py:27: return user_id
openhands/server/user_auth/default_user_auth.py:25: """The default implementation does not support multi tenancy, so user_id is always None"""
openhands/server/user_auth/default_user_auth.py:40: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:42: shared.config, user_id
openhands/server/user_auth/default_user_auth.py:67: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:69: shared.config, user_id
openhands/server/utils.py:66: user_id = await get_user_id(request)
openhands/server/utils.py:67: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/utils.py:97: conversation_id: str, user_id: str | None = Depends(get_user_id)
openhands/server/utils.py:101: conversation_id, user_id
openhands/server/utils.py:106: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/storage/conversation/conversation_store.py:36: async def validate_metadata(self, conversation_id: str, user_id: str) -> bool:
openhands/storage/conversation/conversation_store.py:39: if not metadata.user_id or metadata.user_id != user_id:
openhands/storage/conversation/conversation_store.py:69: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/conversation/conversation_validator.py:33: user_id = None
openhands/storage/conversation/conversation_validator.py:34: metadata = await self._ensure_metadata_exists(conversation_id, user_id)
openhands/storage/conversation/conversation_validator.py:35: return metadata.user_id
openhands/storage/conversation/conversation_validator.py:40: user_id: str | None,
openhands/storage/conversation/conversation_validator.py:50: config, user_id
openhands/storage/conversation/conversation_validator.py:63: user_id=user_id,
openhands/storage/conversation/file_conversation_store.py:106: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/data_models/conversation_metadata.py:24: user_id: str | None = None
openhands/storage/data_models/user_secrets.py:69: 'user_id': provider_token.user_id,
openhands/storage/locations.py:4:def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:5: if user_id:
openhands/storage/locations.py:6: return f'users/{user_id}/conversations/{sid}/'
openhands/storage/locations.py:11:def get_conversation_events_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:12: return f'{get_conversation_dir(sid, user_id)}events/'
openhands/storage/locations.py:16: sid: str, id: int, user_id: str | None = None
openhands/storage/locations.py:18: return f'{get_conversation_events_dir(sid, user_id)}{id}.json'
openhands/storage/locations.py:21:def get_conversation_metadata_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:22: return f'{get_conversation_dir(sid, user_id)}metadata.json'
openhands/storage/locations.py:25:def get_conversation_init_data_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:26: return f'{get_conversation_dir(sid, user_id)}init.json'
openhands/storage/locations.py:29:def get_conversation_agent_state_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:30: return f'{get_conversation_dir(sid, user_id)}agent_state.pkl'
openhands/storage/locations.py:33:def get_conversation_llm_registry_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:34: return f'{get_conversation_dir(sid, user_id)}llm_registry.json'
openhands/storage/locations.py:37:def get_conversation_stats_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:38: return f'{get_conversation_dir(sid, user_id)}conversation_stats.pkl'
openhands/storage/locations.py:41:def get_experiment_config_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:42: return f'{get_conversation_dir(sid, user_id)}exp_config.json'
openhands/storage/secrets/file_secrets_store.py:40: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/secrets/secrets_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/file_settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/utils/conversation_summary.py:81: user_id: str | None,
openhands/utils/conversation_summary.py:91: user_id: The ID of the user
openhands/utils/conversation_summary.py:98: event_store = EventStore(conversation_id, file_store, user_id)
openhands/utils/utils.py:25: user_id: str | None,
openhands/utils/utils.py:41: conversation_stats = ConversationStats(file_store, sid, user_id)
third_party/runtime/impl/daytona/daytona_runtime.py:46: user_id: str | None = None,
third_party/runtime/impl/daytona/daytona_runtime.py:86: user_id,
third_party/runtime/impl/e2b/e2b_runtime.py:36: user_id: str | None = None,
third_party/runtime/impl/e2b/e2b_runtime.py:49: user_id,
third_party/runtime/impl/modal/modal_runtime.py:58: user_id: str | None = None,
third_party/runtime/impl/modal/modal_runtime.py:117: user_id,
third_party/runtime/impl/runloop/runloop_runtime.py:41: user_id: str | None = None,
third_party/runtime/impl/runloop/runloop_runtime.py:66: user_id,

View File

@@ -1,200 +0,0 @@
- config.template.toml:288:#user_id = 1000
- docs/openapi.json:3309: "user_id": {
- docs/usage/configuration-options.mdx:337:- `user_id`
- openhands/cli/gui_launcher.py:179: user_id = subprocess.check_output(['id', '-u'], text=True).strip()
- openhands/cli/gui_launcher.py:180: docker_cmd.extend(['-e', f'SANDBOX_USER_ID={user_id}'])
- openhands/cli/main.py:103: event_stream.user_id,
- openhands/cli/main.py:596: settings_store = await FileSettingsStore.get_instance(config=config, user_id=None)
- openhands/controller/agent_controller.py:126: user_id: str | None = None,
- openhands/controller/agent_controller.py:155: self.user_id = user_id
- openhands/controller/agent_controller.py:171: self.state_tracker = StateTracker(sid, file_store, user_id)
- openhands/controller/agent_controller.py:719: user_id=self.user_id,
- openhands/controller/agent_controller.py:740: user_id=self.user_id,
- openhands/controller/state/state.py:82: user_id: str | None = None
- openhands/controller/state/state.py:123: self, sid: str, file_store: FileStore, user_id: str | None
- openhands/controller/state/state.py:133: get_conversation_agent_state_filename(sid, user_id), encoded
- openhands/controller/state/state.py:137: if user_id:
- openhands/controller/state/state.py:151: sid: str, file_store: FileStore, user_id: str | None = None
- openhands/controller/state/state.py:157: get_conversation_agent_state_filename(sid, user_id)
- openhands/controller/state/state.py:162: # if user_id is provided, we are in a saas/remote use case
- openhands/controller/state/state.py:164: if user_id:
- openhands/controller/state/state.py:277: 'trace_user_id': self.user_id,
- openhands/controller/state/state_tracker.py:32: self, sid: str | None, file_store: FileStore | None, user_id: str | None
- openhands/controller/state/state_tracker.py:36: self.user_id = user_id
- openhands/controller/state/state_tracker.py:75: user_id=self.user_id,
- openhands/controller/state/state_tracker.py:249: self.state.save_to_session(self.sid, self.file_store, self.user_id)
- openhands/core/config/mcp_config.py:337: host: str, config: 'OpenHandsConfig', user_id: str | None = None
- openhands/core/config/mcp_config.py:344: user_id: Optional user ID for the MCP server
- openhands/core/config/sandbox_config.py:16: user_id: The user ID for the sandbox.
- openhands/core/config/sandbox_config.py:59: user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000)
- openhands/core/config/sandbox_config.py:60: logger.debug(f'SandboxConfig user_id default: {user_id}')
- openhands/core/main.py:225: event_stream.sid, event_stream.file_store, event_stream.user_id
- openhands/events/event_store.py:49: user_id: str | None
- openhands/events/event_store.py:69: events_dir = get_conversation_events_dir(self.sid, self.user_id)
- openhands/events/event_store.py:139: filename = self._get_filename_for_id(id, self.user_id)
- openhands/events/event_store.py:155: def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
- openhands/events/event_store.py:156: return get_conversation_event_filename(self.sid, id, user_id)
- openhands/events/event_store.py:159: return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
- openhands/events/event_store_abc.py:15: user_id: str | None
- openhands/events/nested_event_store.py:20: user_id: str | None
- openhands/events/stream.py:34: sid: str, file_store: FileStore, user_id: str | None = None
- openhands/events/stream.py:37: await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
- openhands/events/stream.py:56: def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
- openhands/events/stream.py:57: super().__init__(sid, file_store, user_id)
- openhands/events/stream.py:189: filename = self._get_filename_for_id(event.id, self.user_id)
- openhands/events/stream.py:194: 'user_id': self.user_id,
- openhands/experiments/experiment_manager.py:34: user_id: str, conversation_id: str, conversation_settings: ConversationInitData
- openhands/experiments/experiment_manager.py:40: user_id: str, conversation_id: str, config: OpenHandsConfig
- openhands/integrations/bitbucket/bitbucket_service.py:40: user_id: str | None = None,
- openhands/integrations/bitbucket/bitbucket_service.py:47: self.user_id = user_id
- openhands/integrations/github/github_service.py:49: user_id: str | None = None,
- openhands/integrations/github/github_service.py:56: self.user_id = user_id
- openhands/integrations/github/service/features.py:85: 'user_id': self.external_auth_id,
- openhands/integrations/github/service/features.py:116: 'user_id': self.external_auth_id,
- openhands/integrations/gitlab/gitlab_service.py:47: user_id: str | None = None,
- openhands/integrations/gitlab/gitlab_service.py:54: self.user_id = user_id
- openhands/integrations/provider.py:43: user_id: str | None = Field(default=None)
- openhands/integrations/provider.py:62: user_id = token_value.get('user_id')
- openhands/integrations/provider.py:64: return cls(token=SecretStr(token_str), user_id=user_id, host=host)
- openhands/integrations/provider.py:154: user_id=token.user_id,
- openhands/integrations/service_types.py:442: user_id: str | None = None,
- openhands/resolver/issue_resolver.py:251: user_id = os.getuid() if hasattr(os, 'getuid') else 1000
- openhands/resolver/issue_resolver.py:252: if user_id == 0:
- openhands/resolver/issue_resolver.py:253: sandbox_config.user_id = get_unique_uid()
- openhands/resolver/issue_resolver.py:265: openhands_config.sandbox.user_id = sandbox_config.user_id
- openhands/runtime/action_execution_server.py:176: user_id: int,
- openhands/runtime/action_execution_server.py:183: self.user_id = user_id
- openhands/runtime/action_execution_server.py:185: username=username, user_id=self.user_id, initial_cwd=work_dir
- openhands/runtime/action_execution_server.py:188: self.user_id = _updated_user_id
- openhands/runtime/action_execution_server.py:553: os.chown(filepath, self.user_id, self.user_id)
- openhands/runtime/action_execution_server.py:700: user_id=args.user_id,
- openhands/runtime/base.py:139: user_id: str | None = None,
- openhands/runtime/base.py:172: external_auth_id=user_id,
- openhands/runtime/base.py:191: self.user_id = user_id
- openhands/runtime/base.py:336: external_auth_id=self.user_id,
- openhands/runtime/base.py:901: self.sid, self.event_stream.user_id
- openhands/runtime/impl/action_execution/action_execution_client.py:79: user_id: str | None = None,
- openhands/runtime/impl/action_execution/action_execution_client.py:97: user_id,
- openhands/runtime/impl/cli/cli_runtime.py:103: user_id (str | None, optional): User ID for authentication. Defaults to None.
- openhands/runtime/impl/cli/cli_runtime.py:118: user_id: str | None = None,
- openhands/runtime/impl/cli/cli_runtime.py:131: user_id,
- openhands/runtime/impl/docker/docker_runtime.py:101: user_id: str | None = None,
- openhands/runtime/impl/docker/docker_runtime.py:155: user_id,
- openhands/runtime/impl/kubernetes/kubernetes_runtime.py:92: user_id: str | None = None,
- openhands/runtime/impl/kubernetes/kubernetes_runtime.py:149: user_id,
- openhands/runtime/impl/local/local_runtime.py:146: user_id: str | None = None,
- openhands/runtime/impl/local/local_runtime.py:198: user_id,
- openhands/runtime/impl/local/local_runtime.py:659: user_id, username = get_user_info()
- openhands/runtime/impl/local/local_runtime.py:668: override_user_id=user_id,
- openhands/runtime/impl/remote/remote_runtime.py:62: user_id: str | None = None,
- openhands/runtime/impl/remote/remote_runtime.py:76: user_id,
- openhands/runtime/impl/remote/remote_runtime.py:79: logger.debug(f'RemoteRuntime.init user_id {user_id}')
- openhands/runtime/utils/command.py:46: user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
- openhands/runtime/utils/command.py:61: str(user_id),
- openhands/runtime/utils/runtime_init.py:9: username: str, user_id: int, initial_cwd: str
- openhands/runtime/utils/runtime_init.py:20: - If the UID differs, it logs a warning and return an updated user_id.
- openhands/runtime/utils/runtime_init.py:30: user_id (int): The user ID to assign to the user.
- openhands/runtime/utils/runtime_init.py:55: logger.debug(f'Attempting to create user `{username}` with UID {user_id}.')
- openhands/runtime/utils/runtime_init.py:64: if existing_user_id == user_id:
- openhands/runtime/utils/runtime_init.py:66: f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
- openhands/runtime/utils/runtime_init.py:95: f'-g root -G sudo -u {user_id} {username}'
- openhands/runtime/utils/runtime_init.py:100: f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
- openhands/runtime/utils/runtime_init.py:104: f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
- openhands/server/conversation_manager/conversation_manager.py:61: self, sid: str, user_id: str | None = None
- openhands/server/conversation_manager/conversation_manager.py:75: user_id: str | None,
- openhands/server/conversation_manager/conversation_manager.py:86: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/conversation_manager.py:92: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/conversation_manager.py:101: user_id: str | None,
- openhands/server/conversation_manager/conversation_manager.py:136: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:71: self, sid: str, user_id: str | None = None
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:85: user_id: str | None,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:91: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:110: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:120: user_id: str | None,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:126: sid, settings, user_id, initial_user_msg, replay_json
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:138: user_id=user_id,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:152: user_id: str | None,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:157: await self.ensure_num_conversations_below_limit(sid, user_id)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:158: runtime = await self._create_runtime(sid, user_id, settings)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:225: 'user_id': v.user_id,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:385: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:419: user_id=user_id,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:458: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:465: store = await conversation_store_class.get_instance(self.config, user_id)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:491: self, sid: str, user_id: str | None
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:493: response_ids = await self.get_running_agent_loops(user_id)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:496: f'too_many_sessions_for:{user_id or ""}',
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:497: extra={'session_id': sid, 'user_id': user_id},
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:500: conversation_store = await self._get_conversation_store(user_id)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:507: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:508: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:535: self, sid: str, user_id: str | None, settings: Settings
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:541: user_id, sid, self.config
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:545: create_registry_and_conversation_stats(config, sid, user_id, settings)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:555: user_id=user_id,
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:584: conversation_dir = get_conversation_dir(sid, user_id)
- openhands/server/conversation_manager/docker_nested_conversation_manager.py:602: event_stream = EventStream(sid, self.file_store, user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:92: self, sid: str, user_id: str | None = None
- openhands/server/conversation_manager/standalone_conversation_manager.py:95: if not await session_exists(sid, self.file_store, user_id=user_id):
- openhands/server/conversation_manager/standalone_conversation_manager.py:130: user_id=user_id,
- openhands/server/conversation_manager/standalone_conversation_manager.py:156: user_id: str | None,
- openhands/server/conversation_manager/standalone_conversation_manager.py:160: extra={'session_id': sid, 'user_id': user_id},
- openhands/server/conversation_manager/standalone_conversation_manager.py:164: agent_loop_info = await self.maybe_start_agent_loop(sid, settings, user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:230: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
- openhands/server/conversation_manager/standalone_conversation_manager.py:237: store = await conversation_store_class.get_instance(self.config, user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:241: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/standalone_conversation_manager.py:257: if user_id:
- openhands/server/conversation_manager/standalone_conversation_manager.py:258: items = (item for item in items if item[1].user_id == user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:264: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/standalone_conversation_manager.py:273: if user_id:
- openhands/server/conversation_manager/standalone_conversation_manager.py:276: if not session or session.user_id != user_id:
- openhands/server/conversation_manager/standalone_conversation_manager.py:284: user_id: str | None,
- openhands/server/conversation_manager/standalone_conversation_manager.py:292: sid, settings, user_id, initial_user_msg, replay_json
- openhands/server/conversation_manager/standalone_conversation_manager.py:300: user_id: str | None,
- openhands/server/conversation_manager/standalone_conversation_manager.py:306: response_ids = await self.get_running_agent_loops(user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:309: f'too_many_sessions_for:{user_id or ""}',
- openhands/server/conversation_manager/standalone_conversation_manager.py:310: extra={'session_id': sid, 'user_id': user_id},
- openhands/server/conversation_manager/standalone_conversation_manager.py:313: conversation_store = await self._get_conversation_store(user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:320: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
- openhands/server/conversation_manager/standalone_conversation_manager.py:321: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
- openhands/server/conversation_manager/standalone_conversation_manager.py:341: create_registry_and_conversation_stats(self.config, sid, user_id, settings)
- openhands/server/conversation_manager/standalone_conversation_manager.py:350: user_id=user_id,
- openhands/server/conversation_manager/standalone_conversation_manager.py:361: user_id, sid, settings, session.llm_registry
- openhands/server/conversation_manager/standalone_conversation_manager.py:475: user_id: str | None,
- openhands/server/conversation_manager/standalone_conversation_manager.py:484: user_id,
- openhands/server/conversation_manager/standalone_conversation_manager.py:495: user_id: str,
- openhands/server/conversation_manager/standalone_conversation_manager.py:501: conversation_store = await self._get_conversation_store(user_id)
- openhands/server/conversation_manager/standalone_conversation_manager.py:538: conversation_id, user_id, self.file_store, settings, llm_registry
- openhands/server/conversation_manager/standalone_conversation_manager.py:714: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- openhands/server/conversation_manager/standalone_conversation_manager.py:718: if user_id and session.user_id != user_id:
- openhands/server/listen_socket.py:71: user_id = await conversation_validator.validate(
- openhands/server/listen_socket.py:75: f'User {user_id} is allowed to connect to conversation {conversation_id}'
- openhands/server/listen_socket.py:80: conversation_id, conversation_manager.file_store, user_id
- openhands/server/listen_socket.py:121: user_id, conversation_id, providers_set
- openhands/server/listen_socket.py:128: user_id,
- openhands/server/routes/conversation.py:114: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/conversation.py:126: user_id: User ID (injected by dependency)
- openhands/server/routes/conversation.py:145: user_id=user_id,
- openhands/server/routes/files.py:233: user_id: str = Depends(get_user_id),
- openhands/server/routes/git.py:43: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:49: external_auth_id=user_id,
- openhands/server/routes/git.py:74: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:80: external_auth_id=user_id,
- openhands/server/routes/git.py:100: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
- openhands/server/routes/git.py:109: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:127: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
- openhands/server/routes/git.py:141: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:147: external_auth_id=user_id,
- openhands/server/routes/git.py:162: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
- openhands/server/routes/git.py:175: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:181: external_auth_id=user_id,
- openhands/server/routes/git.py:202: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
- openhands/server/routes/git.py:214: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:235: logger.info(f'Returning 401 Unauthorized - No providers set for user_id: {user_id}')
- openhands/server/routes/git.py:246: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:275: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
- openhands/server/routes/git.py:300: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:316: user_id: User ID for authentication
- openhands/server/routes/git.py:327: external_auth_id=user_id,
- openhands/server/routes/git.py:366: user_id: str | None = Depends(get_user_id),
- openhands/server/routes/git.py:375: user_id: User ID for authentication

View File

@@ -1,299 +0,0 @@
config.template.toml:288:#user_id = 1000
docs/openapi.json:3309: "user_id": {
docs/usage/configuration-options.mdx:337:- `user_id`
openhands/cli/main.py:103: event_stream.user_id,
openhands/cli/main.py:596: settings_store = await FileSettingsStore.get_instance(config=config, user_id=None)
openhands/controller/agent_controller.py:126: user_id: str | None = None,
openhands/controller/agent_controller.py:155: self.user_id = user_id
openhands/controller/agent_controller.py:171: self.state_tracker = StateTracker(sid, file_store, user_id)
openhands/controller/agent_controller.py:719: user_id=self.user_id,
openhands/controller/agent_controller.py:740: user_id=self.user_id,
openhands/controller/state/state.py:82: user_id: str | None = None
openhands/controller/state/state.py:123: self, sid: str, file_store: FileStore, user_id: str | None
openhands/controller/state/state.py:133: get_conversation_agent_state_filename(sid, user_id), encoded
openhands/controller/state/state.py:137: if user_id:
openhands/controller/state/state.py:151: sid: str, file_store: FileStore, user_id: str | None = None
openhands/controller/state/state.py:157: get_conversation_agent_state_filename(sid, user_id)
openhands/controller/state/state.py:162: # if user_id is provided, we are in a saas/remote use case
openhands/controller/state/state.py:164: if user_id:
openhands/controller/state/state.py:277: 'trace_user_id': self.user_id,
openhands/controller/state/state_tracker.py:32: self, sid: str | None, file_store: FileStore | None, user_id: str | None
openhands/controller/state/state_tracker.py:36: self.user_id = user_id
openhands/controller/state/state_tracker.py:75: user_id=self.user_id,
openhands/controller/state/state_tracker.py:249: self.state.save_to_session(self.sid, self.file_store, self.user_id)
openhands/core/config/mcp_config.py:337: host: str, config: 'OpenHandsConfig', user_id: str | None = None
openhands/core/config/mcp_config.py:344: user_id: Optional user ID for the MCP server
openhands/core/main.py:225: event_stream.sid, event_stream.file_store, event_stream.user_id
openhands/events/event_store.py:49: user_id: str | None
openhands/events/event_store.py:69: events_dir = get_conversation_events_dir(self.sid, self.user_id)
openhands/events/event_store.py:139: filename = self._get_filename_for_id(id, self.user_id)
openhands/events/event_store.py:155: def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
openhands/events/event_store.py:156: return get_conversation_event_filename(self.sid, id, user_id)
openhands/events/event_store.py:159: return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
openhands/events/event_store_abc.py:15: user_id: str | None
openhands/events/nested_event_store.py:20: user_id: str | None
openhands/events/stream.py:34: sid: str, file_store: FileStore, user_id: str | None = None
openhands/events/stream.py:37: await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
openhands/events/stream.py:56: def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
openhands/events/stream.py:57: super().__init__(sid, file_store, user_id)
openhands/events/stream.py:189: filename = self._get_filename_for_id(event.id, self.user_id)
openhands/events/stream.py:194: 'user_id': self.user_id,
openhands/experiments/experiment_manager.py:34: user_id: str, conversation_id: str, conversation_settings: ConversationInitData
openhands/experiments/experiment_manager.py:40: user_id: str, conversation_id: str, config: OpenHandsConfig
openhands/integrations/bitbucket/bitbucket_service.py:40: user_id: str | None = None,
openhands/integrations/bitbucket/bitbucket_service.py:47: self.user_id = user_id
openhands/integrations/github/github_service.py:49: user_id: str | None = None,
openhands/integrations/github/github_service.py:56: self.user_id = user_id
openhands/integrations/github/service/features.py:85: 'user_id': self.external_auth_id,
openhands/integrations/github/service/features.py:116: 'user_id': self.external_auth_id,
openhands/integrations/gitlab/gitlab_service.py:47: user_id: str | None = None,
openhands/integrations/gitlab/gitlab_service.py:54: self.user_id = user_id
openhands/integrations/provider.py:43: user_id: str | None = Field(default=None)
openhands/integrations/provider.py:62: user_id = token_value.get('user_id')
openhands/integrations/provider.py:64: return cls(token=SecretStr(token_str), user_id=user_id, host=host)
openhands/integrations/provider.py:154: user_id=token.user_id,
openhands/integrations/service_types.py:442: user_id: str | None = None,
openhands/resolver/issue_resolver.py:251: user_id = os.getuid() if hasattr(os, 'getuid') else 1000
openhands/resolver/issue_resolver.py:252: if user_id == 0:
openhands/runtime/action_execution_server.py:176: user_id: int,
openhands/runtime/action_execution_server.py:183: self.user_id = user_id
openhands/runtime/action_execution_server.py:185: username=username, user_id=self.user_id, initial_cwd=work_dir
openhands/runtime/action_execution_server.py:188: self.user_id = _updated_user_id
openhands/runtime/action_execution_server.py:553: os.chown(filepath, self.user_id, self.user_id)
openhands/runtime/action_execution_server.py:700: user_id=args.user_id,
openhands/runtime/base.py:139: user_id: str | None = None,
openhands/runtime/base.py:172: external_auth_id=user_id,
openhands/runtime/base.py:191: self.user_id = user_id
openhands/runtime/base.py:336: external_auth_id=self.user_id,
openhands/runtime/base.py:901: self.sid, self.event_stream.user_id
openhands/runtime/utils/command.py:46: user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
openhands/runtime/utils/command.py:61: str(user_id),
openhands/server/conversation_manager/conversation_manager.py:61: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/conversation_manager.py:75: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:86: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:92: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:101: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:136: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:71: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:85: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:91: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:110: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:120: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:126: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/docker_nested_conversation_manager.py:138: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:152: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:157: await self.ensure_num_conversations_below_limit(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:158: runtime = await self._create_runtime(sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:225: 'user_id': v.user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:385: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:419: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:458: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/docker_nested_conversation_manager.py:465: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:491: self, sid: str, user_id: str | None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:493: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:496: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:497: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:500: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:507: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:508: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:535: self, sid: str, user_id: str | None, settings: Settings
openhands/server/conversation_manager/docker_nested_conversation_manager.py:541: user_id, sid, self.config
openhands/server/conversation_manager/docker_nested_conversation_manager.py:545: create_registry_and_conversation_stats(config, sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:555: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:584: conversation_dir = get_conversation_dir(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:602: event_stream = EventStream(sid, self.file_store, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:92: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:95: if not await session_exists(sid, self.file_store, user_id=user_id):
openhands/server/conversation_manager/standalone_conversation_manager.py:130: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:156: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:160: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:164: agent_loop_info = await self.maybe_start_agent_loop(sid, settings, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:230: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/standalone_conversation_manager.py:237: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:241: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:257: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:258: items = (item for item in items if item[1].user_id == user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:264: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:273: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:276: if not session or session.user_id != user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:284: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:292: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/standalone_conversation_manager.py:300: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:306: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:309: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/standalone_conversation_manager.py:310: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:313: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:320: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/standalone_conversation_manager.py:321: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:341: create_registry_and_conversation_stats(self.config, sid, user_id, settings)
openhands/server/conversation_manager/standalone_conversation_manager.py:350: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:361: user_id, sid, settings, session.llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:475: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:484: user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:495: user_id: str,
openhands/server/conversation_manager/standalone_conversation_manager.py:501: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:538: conversation_id, user_id, self.file_store, settings, llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:714: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:718: if user_id and session.user_id != user_id:
openhands/server/listen_socket.py:71: user_id = await conversation_validator.validate(
openhands/server/listen_socket.py:75: f'User {user_id} is allowed to connect to conversation {conversation_id}'
openhands/server/listen_socket.py:80: conversation_id, conversation_manager.file_store, user_id
openhands/server/listen_socket.py:121: user_id, conversation_id, providers_set
openhands/server/listen_socket.py:128: user_id,
openhands/server/routes/conversation.py:114: user_id: str | None = Depends(get_user_id),
openhands/server/routes/conversation.py:126: user_id: User ID (injected by dependency)
openhands/server/routes/conversation.py:145: user_id=user_id,
openhands/server/routes/files.py:233: user_id: str = Depends(get_user_id),
openhands/server/routes/git.py:43: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:49: external_auth_id=user_id,
openhands/server/routes/git.py:74: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:80: external_auth_id=user_id,
openhands/server/routes/git.py:100: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:109: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:127: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:141: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:147: external_auth_id=user_id,
openhands/server/routes/git.py:162: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:175: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:181: external_auth_id=user_id,
openhands/server/routes/git.py:202: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:214: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:235: logger.info(f'Returning 401 Unauthorized - No providers set for user_id: {user_id}')
openhands/server/routes/git.py:246: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:275: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:300: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:316: user_id: User ID for authentication
openhands/server/routes/git.py:327: external_auth_id=user_id,
openhands/server/routes/git.py:366: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:375: user_id: User ID for authentication
openhands/server/routes/git.py:389: external_auth_id=user_id,
openhands/server/routes/manage_conversations.py:193: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:251: user_id=user_id,
openhands/server/routes/manage_conversations.py:356: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:358: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/manage_conversations.py:381: sid=conversation_id, file_store=file_store, user_id=metadata.user_id
openhands/server/routes/manage_conversations.py:478: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:505: user_id, conversation_id, providers_set.providers_set or []
openhands/server/routes/manage_conversations.py:512: user_id=user_id,
openhands/server/routes/manage_conversations.py:538: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:550: user_id=user_id, filter_to_sids={conversation_id}
openhands/server/routes/manage_conversations.py:644: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:655: user_id: The authenticated user ID
openhands/server/routes/manage_conversations.py:666: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:674: if user_id and metadata.user_id != user_id:
openhands/server/routes/manage_conversations.py:676: f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',
openhands/server/routes/manage_conversations.py:677: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:715: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:723: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:736: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/mcp.py:52: user_id: str | None, conversation_id: str, tool_result: str
openhands/server/routes/mcp.py:54: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/mcp.py:106: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:115: user_id=github_token.user_id,
openhands/server/routes/mcp.py:116: external_auth_id=user_id,
openhands/server/routes/mcp.py:139: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:176: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:185: user_id=github_token.user_id,
openhands/server/routes/mcp.py:186: external_auth_id=user_id,
openhands/server/routes/mcp.py:210: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:243: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:252: user_id=bitbucket_token.user_id,
openhands/server/routes/mcp.py:253: external_auth_id=user_id,
openhands/server/routes/mcp.py:276: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/secrets.py:111: # We don't have direct access to user_id here, but we can log the provider info
openhands/server/routes/settings.py:62: if provider_token.token or provider_token.user_id:
openhands/server/routes/settings.py:79: # Get user_id from settings if available
openhands/server/routes/settings.py:80: user_id = getattr(settings, 'user_id', 'unknown') if settings else 'unknown'
openhands/server/routes/settings.py:82: f'Returning 401 Unauthorized - Invalid token for user_id: {user_id}'
openhands/server/services/conversation_service.py:35: user_id: str | None,
openhands/server/services/conversation_service.py:45: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:50: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:60: user_id=user_id,
openhands/server/services/conversation_service.py:79: user_id: str | None,
openhands/server/services/conversation_service.py:94: 'user_id': user_id,
openhands/server/services/conversation_service.py:99: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:133: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_service.py:138: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:151: user_id,
openhands/server/services/conversation_service.py:160: user_id: str | None,
openhands/server/services/conversation_service.py:175: user_id,
openhands/server/services/conversation_service.py:187: user_id,
openhands/server/services/conversation_service.py:207: provider_information[provider] = ProviderToken(token=None, user_id=None)
openhands/server/services/conversation_service.py:213: user_id: str | None, conversation_id: str, providers_set: list[ProviderType]
openhands/server/services/conversation_service.py:218: user_id: The user ID
openhands/server/services/conversation_service.py:225: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:228: secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:254: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_stats.py:19: user_id: str | None,
openhands/server/services/conversation_stats.py:21: self.metrics_path = get_conversation_stats_filename(conversation_id, user_id)
openhands/server/services/conversation_stats.py:24: self.user_id = user_id
openhands/server/session/agent_session.py:50: user_id: str | None
openhands/server/session/agent_session.py:71: user_id: str | None = None,
openhands/server/session/agent_session.py:80: self.event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/agent_session.py:83: self.user_id = user_id
openhands/server/session/agent_session.py:85: extra={'session_id': sid, 'user_id': user_id}
openhands/server/session/agent_session.py:342: user_id=self.user_id,
openhands/server/session/agent_session.py:429: user_id=self.user_id,
openhands/server/session/agent_session.py:501: self.sid, self.file_store, self.user_id
openhands/server/session/conversation.py:17: user_id: str | None
openhands/server/session/conversation.py:25: user_id: str | None,
openhands/server/session/conversation.py:32: self.user_id = user_id
openhands/server/session/conversation.py:35: event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/session.py:50: user_id: str | None
openhands/server/session/session.py:61: user_id: str | None = None,
openhands/server/session/session.py:76: user_id=user_id,
openhands/server/session/session.py:87: user_id, sid, self.config
openhands/server/session/session.py:90: self.user_id = user_id
openhands/server/session/session.py:178: self.config.mcp_host, self.config, self.user_id
openhands/server/user_auth/__init__.py:26: user_id = await user_auth.get_user_id()
openhands/server/user_auth/__init__.py:27: return user_id
openhands/server/user_auth/default_user_auth.py:25: """The default implementation does not support multi tenancy, so user_id is always None"""
openhands/server/user_auth/default_user_auth.py:40: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:42: shared.config, user_id
openhands/server/user_auth/default_user_auth.py:67: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:69: shared.config, user_id
openhands/server/utils.py:66: user_id = await get_user_id(request)
openhands/server/utils.py:67: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/utils.py:97: conversation_id: str, user_id: str | None = Depends(get_user_id)
openhands/server/utils.py:101: conversation_id, user_id
openhands/server/utils.py:106: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/storage/conversation/conversation_store.py:36: async def validate_metadata(self, conversation_id: str, user_id: str) -> bool:
openhands/storage/conversation/conversation_store.py:39: if not metadata.user_id or metadata.user_id != user_id:
openhands/storage/conversation/conversation_store.py:69: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/conversation/conversation_validator.py:33: user_id = None
openhands/storage/conversation/conversation_validator.py:34: metadata = await self._ensure_metadata_exists(conversation_id, user_id)
openhands/storage/conversation/conversation_validator.py:35: return metadata.user_id
openhands/storage/conversation/conversation_validator.py:40: user_id: str | None,
openhands/storage/conversation/conversation_validator.py:50: config, user_id
openhands/storage/conversation/conversation_validator.py:63: user_id=user_id,
openhands/storage/conversation/file_conversation_store.py:106: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/data_models/conversation_metadata.py:24: user_id: str | None = None
openhands/storage/data_models/user_secrets.py:69: 'user_id': provider_token.user_id,
openhands/storage/locations.py:4:def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:5: if user_id:
openhands/storage/locations.py:6: return f'users/{user_id}/conversations/{sid}/'
openhands/storage/locations.py:11:def get_conversation_events_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:12: return f'{get_conversation_dir(sid, user_id)}events/'
openhands/storage/locations.py:16: sid: str, id: int, user_id: str | None = None
openhands/storage/locations.py:18: return f'{get_conversation_events_dir(sid, user_id)}{id}.json'
openhands/storage/locations.py:21:def get_conversation_metadata_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:22: return f'{get_conversation_dir(sid, user_id)}metadata.json'
openhands/storage/locations.py:25:def get_conversation_init_data_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:26: return f'{get_conversation_dir(sid, user_id)}init.json'
openhands/storage/locations.py:29:def get_conversation_agent_state_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:30: return f'{get_conversation_dir(sid, user_id)}agent_state.pkl'
openhands/storage/locations.py:33:def get_conversation_llm_registry_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:34: return f'{get_conversation_dir(sid, user_id)}llm_registry.json'
openhands/storage/locations.py:37:def get_conversation_stats_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:38: return f'{get_conversation_dir(sid, user_id)}conversation_stats.pkl'
openhands/storage/locations.py:41:def get_experiment_config_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:42: return f'{get_conversation_dir(sid, user_id)}exp_config.json'
openhands/storage/secrets/file_secrets_store.py:40: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/secrets/secrets_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/file_settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/utils/conversation_summary.py:81: user_id: str | None,
openhands/utils/conversation_summary.py:91: user_id: The ID of the user
openhands/utils/conversation_summary.py:98: event_store = EventStore(conversation_id, file_store, user_id)
openhands/utils/utils.py:25: user_id: str | None,
openhands/utils/utils.py:41: conversation_stats = ConversationStats(file_store, sid, user_id)

View File

@@ -1,290 +0,0 @@
config.template.toml:288:#user_id = 1000
docs/openapi.json:3309: "user_id": {
docs/usage/configuration-options.mdx:337:- `user_id`
openhands/cli/main.py:103: event_stream.user_id,
openhands/cli/main.py:596: settings_store = await FileSettingsStore.get_instance(config=config, user_id=None)
openhands/controller/agent_controller.py:126: user_id: str | None = None,
openhands/controller/agent_controller.py:155: self.user_id = user_id
openhands/controller/agent_controller.py:171: self.state_tracker = StateTracker(sid, file_store, user_id)
openhands/controller/agent_controller.py:719: user_id=self.user_id,
openhands/controller/agent_controller.py:740: user_id=self.user_id,
openhands/controller/state/state.py:82: user_id: str | None = None
openhands/controller/state/state.py:123: self, sid: str, file_store: FileStore, user_id: str | None
openhands/controller/state/state.py:133: get_conversation_agent_state_filename(sid, user_id), encoded
openhands/controller/state/state.py:137: if user_id:
openhands/controller/state/state.py:151: sid: str, file_store: FileStore, user_id: str | None = None
openhands/controller/state/state.py:157: get_conversation_agent_state_filename(sid, user_id)
openhands/controller/state/state.py:162: # if user_id is provided, we are in a saas/remote use case
openhands/controller/state/state.py:164: if user_id:
openhands/controller/state/state.py:277: 'trace_user_id': self.user_id,
openhands/controller/state/state_tracker.py:32: self, sid: str | None, file_store: FileStore | None, user_id: str | None
openhands/controller/state/state_tracker.py:36: self.user_id = user_id
openhands/controller/state/state_tracker.py:75: user_id=self.user_id,
openhands/controller/state/state_tracker.py:249: self.state.save_to_session(self.sid, self.file_store, self.user_id)
openhands/core/config/mcp_config.py:337: host: str, config: 'OpenHandsConfig', user_id: str | None = None
openhands/core/config/mcp_config.py:344: user_id: Optional user ID for the MCP server
openhands/core/main.py:225: event_stream.sid, event_stream.file_store, event_stream.user_id
openhands/events/event_store.py:49: user_id: str | None
openhands/events/event_store.py:69: events_dir = get_conversation_events_dir(self.sid, self.user_id)
openhands/events/event_store.py:139: filename = self._get_filename_for_id(id, self.user_id)
openhands/events/event_store.py:155: def _get_filename_for_id(self, id: int, user_id: str | None) -> str:
openhands/events/event_store.py:156: return get_conversation_event_filename(self.sid, id, user_id)
openhands/events/event_store.py:159: return f'{get_conversation_dir(self.sid, self.user_id)}event_cache/{start}-{end}.json'
openhands/events/event_store_abc.py:15: user_id: str | None
openhands/events/nested_event_store.py:20: user_id: str | None
openhands/events/stream.py:34: sid: str, file_store: FileStore, user_id: str | None = None
openhands/events/stream.py:37: await call_sync_from_async(file_store.list, get_conversation_dir(sid, user_id))
openhands/events/stream.py:56: def __init__(self, sid: str, file_store: FileStore, user_id: str | None = None):
openhands/events/stream.py:57: super().__init__(sid, file_store, user_id)
openhands/events/stream.py:189: filename = self._get_filename_for_id(event.id, self.user_id)
openhands/events/stream.py:194: 'user_id': self.user_id,
openhands/experiments/experiment_manager.py:34: user_id: str, conversation_id: str, conversation_settings: ConversationInitData
openhands/experiments/experiment_manager.py:40: user_id: str, conversation_id: str, config: OpenHandsConfig
openhands/integrations/bitbucket/bitbucket_service.py:40: user_id: str | None = None,
openhands/integrations/bitbucket/bitbucket_service.py:47: self.user_id = user_id
openhands/integrations/github/github_service.py:49: user_id: str | None = None,
openhands/integrations/github/github_service.py:56: self.user_id = user_id
openhands/integrations/github/service/features.py:85: 'user_id': self.external_auth_id,
openhands/integrations/github/service/features.py:116: 'user_id': self.external_auth_id,
openhands/integrations/gitlab/gitlab_service.py:47: user_id: str | None = None,
openhands/integrations/gitlab/gitlab_service.py:54: self.user_id = user_id
openhands/integrations/provider.py:43: user_id: str | None = Field(default=None)
openhands/integrations/provider.py:62: user_id = token_value.get('user_id')
openhands/integrations/provider.py:64: return cls(token=SecretStr(token_str), user_id=user_id, host=host)
openhands/integrations/provider.py:154: user_id=token.user_id,
openhands/integrations/service_types.py:442: user_id: str | None = None,
openhands/resolver/issue_resolver.py:252: if user_id == 0:
openhands/runtime/base.py:139: user_id: str | None = None,
openhands/runtime/base.py:172: external_auth_id=user_id,
openhands/runtime/base.py:191: self.user_id = user_id
openhands/runtime/base.py:336: external_auth_id=self.user_id,
openhands/runtime/base.py:901: self.sid, self.event_stream.user_id
openhands/server/conversation_manager/conversation_manager.py:61: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/conversation_manager.py:75: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:86: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:92: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/conversation_manager.py:101: user_id: str | None,
openhands/server/conversation_manager/conversation_manager.py:136: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:71: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:85: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:91: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:110: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:120: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:126: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/docker_nested_conversation_manager.py:138: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:152: user_id: str | None,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:157: await self.ensure_num_conversations_below_limit(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:158: runtime = await self._create_runtime(sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:225: 'user_id': v.user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:385: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:419: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:458: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/docker_nested_conversation_manager.py:465: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:491: self, sid: str, user_id: str | None
openhands/server/conversation_manager/docker_nested_conversation_manager.py:493: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:496: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:497: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:500: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:507: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/docker_nested_conversation_manager.py:508: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/docker_nested_conversation_manager.py:535: self, sid: str, user_id: str | None, settings: Settings
openhands/server/conversation_manager/docker_nested_conversation_manager.py:541: user_id, sid, self.config
openhands/server/conversation_manager/docker_nested_conversation_manager.py:545: create_registry_and_conversation_stats(config, sid, user_id, settings)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:555: user_id=user_id,
openhands/server/conversation_manager/docker_nested_conversation_manager.py:584: conversation_dir = get_conversation_dir(sid, user_id)
openhands/server/conversation_manager/docker_nested_conversation_manager.py:602: event_stream = EventStream(sid, self.file_store, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:92: self, sid: str, user_id: str | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:95: if not await session_exists(sid, self.file_store, user_id=user_id):
openhands/server/conversation_manager/standalone_conversation_manager.py:130: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:156: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:160: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:164: agent_loop_info = await self.maybe_start_agent_loop(sid, settings, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:230: async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
openhands/server/conversation_manager/standalone_conversation_manager.py:237: store = await conversation_store_class.get_instance(self.config, user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:241: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:257: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:258: items = (item for item in items if item[1].user_id == user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:264: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:273: if user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:276: if not session or session.user_id != user_id:
openhands/server/conversation_manager/standalone_conversation_manager.py:284: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:292: sid, settings, user_id, initial_user_msg, replay_json
openhands/server/conversation_manager/standalone_conversation_manager.py:300: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:306: response_ids = await self.get_running_agent_loops(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:309: f'too_many_sessions_for:{user_id or ""}',
openhands/server/conversation_manager/standalone_conversation_manager.py:310: extra={'session_id': sid, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:313: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:320: f'closing_from_too_many_sessions:{user_id or ""}:{oldest_conversation_id}',
openhands/server/conversation_manager/standalone_conversation_manager.py:321: extra={'session_id': oldest_conversation_id, 'user_id': user_id},
openhands/server/conversation_manager/standalone_conversation_manager.py:341: create_registry_and_conversation_stats(self.config, sid, user_id, settings)
openhands/server/conversation_manager/standalone_conversation_manager.py:350: user_id=user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:361: user_id, sid, settings, session.llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:475: user_id: str | None,
openhands/server/conversation_manager/standalone_conversation_manager.py:484: user_id,
openhands/server/conversation_manager/standalone_conversation_manager.py:495: user_id: str,
openhands/server/conversation_manager/standalone_conversation_manager.py:501: conversation_store = await self._get_conversation_store(user_id)
openhands/server/conversation_manager/standalone_conversation_manager.py:538: conversation_id, user_id, self.file_store, settings, llm_registry
openhands/server/conversation_manager/standalone_conversation_manager.py:714: self, user_id: str | None = None, filter_to_sids: set[str] | None = None
openhands/server/conversation_manager/standalone_conversation_manager.py:718: if user_id and session.user_id != user_id:
openhands/server/listen_socket.py:71: user_id = await conversation_validator.validate(
openhands/server/listen_socket.py:75: f'User {user_id} is allowed to connect to conversation {conversation_id}'
openhands/server/listen_socket.py:80: conversation_id, conversation_manager.file_store, user_id
openhands/server/listen_socket.py:121: user_id, conversation_id, providers_set
openhands/server/listen_socket.py:128: user_id,
openhands/server/routes/conversation.py:114: user_id: str | None = Depends(get_user_id),
openhands/server/routes/conversation.py:126: user_id: User ID (injected by dependency)
openhands/server/routes/conversation.py:145: user_id=user_id,
openhands/server/routes/files.py:233: user_id: str = Depends(get_user_id),
openhands/server/routes/git.py:43: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:49: external_auth_id=user_id,
openhands/server/routes/git.py:74: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:80: external_auth_id=user_id,
openhands/server/routes/git.py:100: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:109: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:127: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:141: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:147: external_auth_id=user_id,
openhands/server/routes/git.py:162: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:175: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:181: external_auth_id=user_id,
openhands/server/routes/git.py:202: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:214: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:235: logger.info(f'Returning 401 Unauthorized - No providers set for user_id: {user_id}')
openhands/server/routes/git.py:246: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:275: f'Returning 401 Unauthorized - Git provider token required for user_id: {user_id}'
openhands/server/routes/git.py:300: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:316: user_id: User ID for authentication
openhands/server/routes/git.py:327: external_auth_id=user_id,
openhands/server/routes/git.py:366: user_id: str | None = Depends(get_user_id),
openhands/server/routes/git.py:375: user_id: User ID for authentication
openhands/server/routes/git.py:389: external_auth_id=user_id,
openhands/server/routes/manage_conversations.py:193: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:251: user_id=user_id,
openhands/server/routes/manage_conversations.py:356: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:358: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/manage_conversations.py:381: sid=conversation_id, file_store=file_store, user_id=metadata.user_id
openhands/server/routes/manage_conversations.py:478: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:505: user_id, conversation_id, providers_set.providers_set or []
openhands/server/routes/manage_conversations.py:512: user_id=user_id,
openhands/server/routes/manage_conversations.py:538: user_id: str = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:550: user_id=user_id, filter_to_sids={conversation_id}
openhands/server/routes/manage_conversations.py:644: user_id: str | None = Depends(get_user_id),
openhands/server/routes/manage_conversations.py:655: user_id: The authenticated user ID
openhands/server/routes/manage_conversations.py:666: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:674: if user_id and metadata.user_id != user_id:
openhands/server/routes/manage_conversations.py:676: f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',
openhands/server/routes/manage_conversations.py:677: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:715: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:723: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/manage_conversations.py:736: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/server/routes/mcp.py:52: user_id: str | None, conversation_id: str, tool_result: str
openhands/server/routes/mcp.py:54: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/routes/mcp.py:106: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:115: user_id=github_token.user_id,
openhands/server/routes/mcp.py:116: external_auth_id=user_id,
openhands/server/routes/mcp.py:139: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:176: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:185: user_id=github_token.user_id,
openhands/server/routes/mcp.py:186: external_auth_id=user_id,
openhands/server/routes/mcp.py:210: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/mcp.py:243: user_id = await get_user_id(request)
openhands/server/routes/mcp.py:252: user_id=bitbucket_token.user_id,
openhands/server/routes/mcp.py:253: external_auth_id=user_id,
openhands/server/routes/mcp.py:276: await save_pr_metadata(user_id, conversation_id, response)
openhands/server/routes/secrets.py:111: # We don't have direct access to user_id here, but we can log the provider info
openhands/server/routes/settings.py:62: if provider_token.token or provider_token.user_id:
openhands/server/routes/settings.py:79: # Get user_id from settings if available
openhands/server/routes/settings.py:80: user_id = getattr(settings, 'user_id', 'unknown') if settings else 'unknown'
openhands/server/routes/settings.py:82: f'Returning 401 Unauthorized - Invalid token for user_id: {user_id}'
openhands/server/services/conversation_service.py:35: user_id: str | None,
openhands/server/services/conversation_service.py:45: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:50: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:60: user_id=user_id,
openhands/server/services/conversation_service.py:79: user_id: str | None,
openhands/server/services/conversation_service.py:94: 'user_id': user_id,
openhands/server/services/conversation_service.py:99: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:133: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_service.py:138: extra={'user_id': user_id, 'session_id': conversation_id},
openhands/server/services/conversation_service.py:151: user_id,
openhands/server/services/conversation_service.py:160: user_id: str | None,
openhands/server/services/conversation_service.py:175: user_id,
openhands/server/services/conversation_service.py:187: user_id,
openhands/server/services/conversation_service.py:207: provider_information[provider] = ProviderToken(token=None, user_id=None)
openhands/server/services/conversation_service.py:213: user_id: str | None, conversation_id: str, providers_set: list[ProviderType]
openhands/server/services/conversation_service.py:218: user_id: The user ID
openhands/server/services/conversation_service.py:225: settings_store = await SettingsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:228: secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
openhands/server/services/conversation_service.py:254: user_id, conversation_id, conversation_init_data
openhands/server/services/conversation_stats.py:19: user_id: str | None,
openhands/server/services/conversation_stats.py:21: self.metrics_path = get_conversation_stats_filename(conversation_id, user_id)
openhands/server/services/conversation_stats.py:24: self.user_id = user_id
openhands/server/session/agent_session.py:50: user_id: str | None
openhands/server/session/agent_session.py:71: user_id: str | None = None,
openhands/server/session/agent_session.py:80: self.event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/agent_session.py:83: self.user_id = user_id
openhands/server/session/agent_session.py:85: extra={'session_id': sid, 'user_id': user_id}
openhands/server/session/agent_session.py:342: user_id=self.user_id,
openhands/server/session/agent_session.py:429: user_id=self.user_id,
openhands/server/session/agent_session.py:501: self.sid, self.file_store, self.user_id
openhands/server/session/conversation.py:17: user_id: str | None
openhands/server/session/conversation.py:25: user_id: str | None,
openhands/server/session/conversation.py:32: self.user_id = user_id
openhands/server/session/conversation.py:35: event_stream = EventStream(sid, file_store, user_id)
openhands/server/session/session.py:50: user_id: str | None
openhands/server/session/session.py:61: user_id: str | None = None,
openhands/server/session/session.py:76: user_id=user_id,
openhands/server/session/session.py:87: user_id, sid, self.config
openhands/server/session/session.py:90: self.user_id = user_id
openhands/server/session/session.py:178: self.config.mcp_host, self.config, self.user_id
openhands/server/user_auth/__init__.py:26: user_id = await user_auth.get_user_id()
openhands/server/user_auth/__init__.py:27: return user_id
openhands/server/user_auth/default_user_auth.py:25: """The default implementation does not support multi tenancy, so user_id is always None"""
openhands/server/user_auth/default_user_auth.py:40: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:42: shared.config, user_id
openhands/server/user_auth/default_user_auth.py:67: user_id = await self.get_user_id()
openhands/server/user_auth/default_user_auth.py:69: shared.config, user_id
openhands/server/utils.py:66: user_id = await get_user_id(request)
openhands/server/utils.py:67: conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
openhands/server/utils.py:97: conversation_id: str, user_id: str | None = Depends(get_user_id)
openhands/server/utils.py:101: conversation_id, user_id
openhands/server/utils.py:106: extra={'session_id': conversation_id, 'user_id': user_id},
openhands/storage/conversation/conversation_store.py:36: async def validate_metadata(self, conversation_id: str, user_id: str) -> bool:
openhands/storage/conversation/conversation_store.py:39: if not metadata.user_id or metadata.user_id != user_id:
openhands/storage/conversation/conversation_store.py:69: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/conversation/conversation_validator.py:33: user_id = None
openhands/storage/conversation/conversation_validator.py:34: metadata = await self._ensure_metadata_exists(conversation_id, user_id)
openhands/storage/conversation/conversation_validator.py:35: return metadata.user_id
openhands/storage/conversation/conversation_validator.py:40: user_id: str | None,
openhands/storage/conversation/conversation_validator.py:50: config, user_id
openhands/storage/conversation/conversation_validator.py:63: user_id=user_id,
openhands/storage/conversation/file_conversation_store.py:106: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/data_models/conversation_metadata.py:24: user_id: str | None = None
openhands/storage/data_models/user_secrets.py:69: 'user_id': provider_token.user_id,
openhands/storage/locations.py:4:def get_conversation_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:5: if user_id:
openhands/storage/locations.py:6: return f'users/{user_id}/conversations/{sid}/'
openhands/storage/locations.py:11:def get_conversation_events_dir(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:12: return f'{get_conversation_dir(sid, user_id)}events/'
openhands/storage/locations.py:16: sid: str, id: int, user_id: str | None = None
openhands/storage/locations.py:18: return f'{get_conversation_events_dir(sid, user_id)}{id}.json'
openhands/storage/locations.py:21:def get_conversation_metadata_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:22: return f'{get_conversation_dir(sid, user_id)}metadata.json'
openhands/storage/locations.py:25:def get_conversation_init_data_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:26: return f'{get_conversation_dir(sid, user_id)}init.json'
openhands/storage/locations.py:29:def get_conversation_agent_state_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:30: return f'{get_conversation_dir(sid, user_id)}agent_state.pkl'
openhands/storage/locations.py:33:def get_conversation_llm_registry_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:34: return f'{get_conversation_dir(sid, user_id)}llm_registry.json'
openhands/storage/locations.py:37:def get_conversation_stats_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:38: return f'{get_conversation_dir(sid, user_id)}conversation_stats.pkl'
openhands/storage/locations.py:41:def get_experiment_config_filename(sid: str, user_id: str | None = None) -> str:
openhands/storage/locations.py:42: return f'{get_conversation_dir(sid, user_id)}exp_config.json'
openhands/storage/secrets/file_secrets_store.py:40: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/secrets/secrets_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/file_settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/storage/settings/settings_store.py:34: cls, config: OpenHandsConfig, user_id: str | None
openhands/utils/conversation_summary.py:81: user_id: str | None,
openhands/utils/conversation_summary.py:91: user_id: The ID of the user
openhands/utils/conversation_summary.py:98: event_store = EventStore(conversation_id, file_store, user_id)
openhands/utils/utils.py:25: user_id: str | None,
openhands/utils/utils.py:41: conversation_stats = ConversationStats(file_store, sid, user_id)

View File

@@ -219,6 +219,14 @@ correct_num = 5
api_key = ""
model = "gpt-4o"
# Example routing LLM configuration for multimodal model routing
# Uncomment and configure to enable model routing with a secondary model
#[llm.secondary_model]
#model = "kimi-k2"
#api_key = ""
#for_routing = true
#max_input_tokens = 128000
#################################### Agent ###################################
# Configuration for agents (group name starts with 'agent')
@@ -480,3 +488,55 @@ type = "noop"
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
#privileged = false
#################################### MCP #####################################
# Configuration for Model Context Protocol (MCP) servers
# MCP allows OpenHands to communicate with external tool servers
##############################################################################
[mcp]
# SSE servers - Server-Sent Events transport (legacy)
#sse_servers = [
# # Basic SSE server with just a URL
# "http://localhost:8080/mcp/sse",
#
# # SSE server with authentication
# {url = "https://api.example.com/mcp/sse", api_key = "your-api-key"}
#]
# SHTTP servers - Streamable HTTP transport (recommended)
#shttp_servers = [
# # Basic SHTTP server with default 60s timeout
# "https://api.example.com/mcp/shttp",
#
# # SHTTP server with custom timeout for long-running tools
# {
# url = "https://api.example.com/mcp/shttp",
# api_key = "your-api-key",
# timeout = 180 # 3 minutes for processing-heavy tools (1-3600 seconds)
# }
#]
# Stdio servers - Direct process communication (development only)
#stdio_servers = [
# # Basic stdio server
# {name = "filesystem", command = "npx", args = ["@modelcontextprotocol/server-filesystem", "/"]},
#
# # Stdio server with environment variables
# {
# name = "fetch",
# command = "uvx",
# args = ["mcp-server-fetch"],
# env = {DEBUG = "true"}
# }
#]
#################################### Model Routing ############################
# Configuration for experimental model routing feature
# Enables intelligent switching between different LLM models for specific purposes
##############################################################################
[model_routing]
# Router to use for model selection
# Available options:
# - "noop_router" (default): No routing, always uses primary LLM
# - "multimodal_router": A router that switches between primary and secondary models, depending on whether the input is multimodal or not
#router_name = "noop_router"

View File

@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.55-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.57-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -1,17 +1,36 @@
# Setup
# OpenHands Documentation
```
This directory contains the documentation for OpenHands. The documentation is automatically synchronized with the [All-Hands-AI/docs](https://github.com/All-Hands-AI/docs) repository, which hosts the unified documentation site using Mintlify.
## Documentation Structure
The documentation files in this directory are automatically included in the main documentation site via Git submodules. When you make changes to documentation in this repository, they will be automatically synchronized to the docs repository.
## How It Works
1. **Automatic Sync**: When documentation changes are pushed to the `main` branch, a GitHub Action automatically notifies the docs repository
2. **Submodule Update**: The docs repository updates its submodule reference to include your latest changes
3. **Site Rebuild**: Mintlify automatically rebuilds and deploys the documentation site
## Making Documentation Changes
Simply edit the documentation files in this directory as usual. The synchronization happens automatically when changes are merged to the main branch.
## Local Development
For local documentation development in this repository only:
```bash
npm install -g mint
```
or
```
# or
yarn global add mint
```
# Preview
```
# Preview local changes
mint dev
```
For the complete unified documentation site, work with the [All-Hands-AI/docs](https://github.com/All-Hands-AI/docs) repository.
## Configuration
The Mintlify configuration (`docs.json`) has been moved to the root of the [All-Hands-AI/docs](https://github.com/All-Hands-AI/docs) repository to enable unified documentation across multiple repositories.

View File

@@ -31,6 +31,7 @@
"group": "OpenHands Cloud",
"pages": [
"usage/cloud/openhands-cloud",
"usage/cloud/pro-subscription",
{
"group": "Integrations",
"pages": [
@@ -109,8 +110,7 @@
},
"usage/configuration-options",
"usage/how-to/custom-sandbox-guide",
"usage/search-engine-setup",
"usage/mcp"
"usage/search-engine-setup"
]
}
]
@@ -118,7 +118,13 @@
{
"group": "Customizations & Settings",
"pages": [
"usage/common-settings",
{
"group": "OpenHands Settings",
"pages": [
"usage/settings/secrets-settings",
"usage/settings/mcp-settings"
]
},
"usage/prompting/repository",
{
"group": "Microagents",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 663 KiB

View File

@@ -124,7 +124,7 @@ This tagging approach allows OpenHands to efficiently manage both development an
OpenHands supports both bind mounts and Docker named volumes in SandboxConfig.volumes:
- Bind mount: "/abs/host/path:/container/path[:mode]"
- Named volume: "volume:<name>:/container/path[:mode]" or any non-absolute host spec treated as a named volume
- Named volume: "volume:`<name>`:/container/path[:mode]" or any non-absolute host spec treated as a named volume
Overlay mode (copy-on-write layer) is supported for bind mounts by appending ":overlay" to the mode (e.g., ":ro,overlay").
To enable overlay COW, set SANDBOX_VOLUME_OVERLAYS to a writable host directory; per-container upper/work dirs are created under it. If SANDBOX_VOLUME_OVERLAYS is unset, overlay mounts are skipped.

View File

@@ -8,9 +8,21 @@ description: This guide walks you through the process of installing OpenHands Cl
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a Bitbucket account](/usage/cloud/openhands-cloud).
## Adding Bitbucket Repository Access
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
## Working With Bitbucket Repos in Openhands Cloud
After signing in with a Bitbucket account, use the `Open Repository` section to select the appropriate repository and
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
## IP Whitelisting
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow OpenHands to access your repositories:
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow
OpenHands to access your repositories:
### Core App IP
```
@@ -31,17 +43,6 @@ If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist t
34.60.55.59
```
## Adding Bitbucket Repository Access
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
## Working With Bitbucket Repos in Openhands Cloud
After signing in with a Bitbucket account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo-no-github.png)
## Next Steps
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).

View File

@@ -12,13 +12,10 @@ For the available API endpoints, refer to the
To use the OpenHands Cloud API, you'll need to generate an API key:
1. Log in to your [OpenHands Cloud](https://app.all-hands.dev) account.
2. Navigate to the [Settings page](https://app.all-hands.dev/settings).
3. Select the `API Keys` tab.
4. Click `Create API Key`.
5. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
6. Copy the generated API key and store it securely. It will only be shown once.
![API Key Generation](/static/img/api-key-generation.png)
2. Navigate to the [Settings > API Keys](https://app.all-hands.dev/settings/api-keys) page.
3. Click `Create API Key`.
4. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
5. Copy the generated API key and store it securely. It will only be shown once.
## API Usage

View File

@@ -8,24 +8,39 @@ description: The Cloud UI provides a web interface for interacting with OpenHand
The landing page is where you can:
- [Add GitHub repository access](/usage/cloud/github-installation#adding-github-repository-access) to OpenHands.
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud),
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) or
[a Bitbucket repo](/usage/cloud/bitbucket-installation#working-with-bitbucket-repos-in-openhands-cloud) to start working on.
- Launch an empty conversation using `New Conversation`.
- See `Suggested Tasks` for repositories that OpenHands has access to.
- Launch an empty conversation using `Launch from Scratch`.
- See your `Recent Conversations`.
## Settings
The Settings page allows you to:
Settings are divided across tabs, with each tab focusing on a specific area of configuration.
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
- Set application settings like your preferred language, notifications and other preferences.
- Add credits to your account.
- [Generate custom secrets](/usage/common-settings#secrets-management).
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
- Change your email address.
- `User`
- Change your email address.
- `Integrations`
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
- `Application`
- Set your preferred language, notifications and other preferences.
- Toggle task suggestions on GitHub.
- Toggle Solvability Analysis.
- Set a maximum budget per conversation.
- Configure the username and email that OpenHands uses for commits.
- `LLM` (Available for `Pro Users`)
- Choose to use another LLM or use different models from the OpenHands provider.
- `Billing`
- Add credits for using the OpenHands provider.
- Cancel your `Pro` subscription.
- `Secrets`
- [Generate custom secrets](/usage/settings/secrets-settings).
- `API Keys`
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
- `MCP`
- [Setup an MCP server](/usage/settings/mcp-settings)
## Key Features

View File

@@ -12,7 +12,7 @@ description: This guide walks you through the process of installing OpenHands Cl
You can grant OpenHands access to specific GitHub repositories:
1. Click on `Add GitHub repos` on the landing page.
1. Click on `+ Add GitHub Repos` in the repository selection dropdown.
2. Select your organization and choose the specific repositories to grant OpenHands access to.
<Accordion title="OpenHands permissions">
- OpenHands requests short-lived tokens (8-hour expiration) with these permissions:
@@ -34,20 +34,22 @@ You can grant OpenHands access to specific GitHub repositories:
## Modifying Repository Access
You can modify GitHub repository access at any time by:
- Selecting `Add GitHub repos` on the landing page or
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Integrations` tab
- Selecting `+ Add GitHub Repos` in the repository selection dropdown or
- Visiting the `Settings > Integrations` page and selecting `Configure GitHub Repositories`
## Working With GitHub Repos in Openhands Cloud
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the `select a repo`
and `select a branch` dropdowns to select the appropriate repository and branch you'd like OpenHands to work on. Then
click on `Launch` to start the conversation!
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the
`Open Repository` section to select the appropriate repository and branch you'd like OpenHands to work on. Then click
on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo.png)
## Working on Github Issues and Pull Requests Using Openhands
## Working on GitHub Issues and Pull Requests Using Openhands
Giving GitHub repository access to OpenHands also allows you to work on GitHub issues and pull requests directly.
To allow OpenHands to work directly from GitHub directly, you must
[give OpenHands access to your repository](/usage/cloud/github-installation#modifying-repository-access). Once access is
given, you can use OpenHands by labeling the issue or by tagging `@openhands`.
### Working with Issues
@@ -64,7 +66,12 @@ To get OpenHands to work on pull requests, mention `@openhands` in the comments
- Request updates
- Get code explanations
**Important Note**: The `@openhands` mention functionality in pull requests only works if the pull request is both *to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate permissions to access both repositories.
<Note>
The `@openhands` mention functionality in pull requests only works if the pull request is both
*to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate
permissions to access both repositories.
</Note>
## Next Steps

View File

@@ -14,16 +14,17 @@ Upon signing into OpenHands Cloud with a GitLab account, OpenHands will have acc
## Working With GitLab Repos in Openhands Cloud
After signing in with a Gitlab account, use the `select a repo` and `select a branch` dropdowns to select the
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
After signing in with a Gitlab account, use the `Open Repository` section to select the appropriate repository and
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
![Connect Repo](/static/img/connect-repo-no-github.png)
![Connect Repo](/static/img/connect-repo.png)
## Using Tokens with Reduced Scopes
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
To restrict the agent's permissions, [you can define a custom secret](/usage/settings/secrets-settings) `GITLAB_TOKEN`,
which will override the default token assigned to the agent. While the high-permission API token is still requested
and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
## Working on GitLab Issues and Merge Requests Using Openhands
@@ -32,7 +33,8 @@ This feature works for personal projects and is available for group projects wit
[Premium or Ultimate tier subscription](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).
A webhook is automatically installed within a few minutes after the owner/maintainer of the project or group logs into
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but we are planning to improve this in a future release.
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but
we are planning to improve this in a future release.
</Note>
Giving GitLab repository access to OpenHands also allows you to work on GitLab issues and merge requests directly.

View File

@@ -0,0 +1,45 @@
---
title: "Pro Subscription"
description: "Learn about OpenHands Cloud Pro Subscription features and pricing"
---
The OpenHands Pro Subscription unlocks additional features and better pricing when you run OpenHands conversations in OpenHands Cloud.
## Base Features
All users start on the Pay-as-you-go plan and have access to these base features when they sign up:
* **Run multiple OpenHands conversations on OpenHands Cloud runtimes**
* **API keys to the OpenHands LLM provider for use in tools like OpenHands CLI or OpenHands Local GUI**
* **$20 in initial OpenHands Cloud credits to get started**
* **Support for GitHub, GitLab, Bitbucket, Slack, and more**
## What you get with a Pro Subscription
The $20/month Pro Subscription covers the cost of runtime compute in OpenHands Cloud, plus enables the following features:
* **Bring Your Own LLM Keys:** Bring your own API keys from OpenAI, Anthropic, Mistral, and other providers.
* **Model Choice:** Unlocks access to OpenHands LLM provider models for use within OpenHands Cloud.
* **No Markup Pricing on LLM usage:** When you use the OpenHands LLM provider in OpenHands Cloud, you pay for LLM usage at-cost (zero markup) based on API prices.
## Plan Comparison
Here are the key differences between Pay-as-you-go and Pro subscriptions:
### When running OpenHands conversations in OpenHands Cloud
| | Pay-as-you-go | Pro Subscription |
| :---- | ----- | ----- |
| Monthly price | None \- no commitment | $20/month |
| Can I bring my own LLM key? | No | ✅ Yes |
| Do I pay for LLM usage? | ✅ Yes | ✅ Yes |
| Can I select from different LLMs without bringing my own LLM key? | No \- defaults to Claude Sonnet 4 | ✅ Yes \- via OpenHands LLM provider <br/><br/>[*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
| How much am I charged for LLM usage? | **Marked up pricing** \- 2x Claude Sonnet 4 API prices *This markup helps cover the cost of runtime compute.* | **No markup** \- 1x API prices *The $20 monthly subscription covers the cost of runtime compute.* |
### When using the OpenHands LLM Provider outside of OpenHands Cloud
The following applies to **both** the Pay-as-you-go and Pro subscription:
| | Pay-as-you-go or Pro Subscription |
| :---- | :---- |
| Do I have access to multiple models via the OpenHands LLM provider? | ✅ Yes <br/><br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
| Can I generate and refresh OpenHands LLM API keys? | ✅ Yes |
| How much am I charged for LLM usage when I use the OpenHands LLM provider in other AI coding tools? | **No markup** \- pay 1x API prices <br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) <br/><br/> *Usage is deducted from your OpenHands Cloud credit balance.* <br/><br/> *The OpenHands LLM provider is available to all OpenHands Cloud users, and LLM usage is billed at-cost (zero markup). Use these models with OpenHands CLI, OpenHands Local GUI, or even other AI coding agents\! [Learn more.](https://www.all-hands.dev/blog/access-state-of-the-art-llm-models-at-cost-via-openhands-gui-and-cli)* |

View File

@@ -13,7 +13,9 @@ description: This guide walks you through installing the OpenHands Slack app.
</iframe>
<Info>
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete. While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to validate critical information independently.
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete.
While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to
validate critical information independently.
</Info>
## Prerequisites
@@ -39,7 +41,7 @@ OpenHands utilizes a large language model (LLM), which may generate responses th
**Make sure your Slack workspace admin/owner has installed OpenHands Slack App first.**
Every user in the Slack workspace (including admins/owners) must link their OpenHands Cloud account to the OpenHands Slack App. To do this:
1. Visit [integrations settings](https://app.all-hands.dev/settings/integrations) in OpenHands Cloud.
1. Visit the [Settings > Integrations](https://app.all-hands.dev/settings/integrations) page in OpenHands Cloud.
2. Click `Install OpenHands Slack App`.
3. In the top right corner, select the workspace to install the OpenHands Slack app.
4. Review permissions and click allow.
@@ -57,7 +59,8 @@ To start a new conversation, you can mention `@openhands` in a new message or a
Once a conversation is started, all thread messages underneath it will be follow-up messages to OpenHands.
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message. You must be the user who started the conversation.
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message.
You must be the user who started the conversation.
## Example conversation

View File

@@ -8,6 +8,11 @@ description: This page outlines all available configuration options for OpenHand
In GUI Mode, any settings applied through the Settings UI will take precedence.
</Note>
<Note>
**Looking for Environment Variables?** All configuration options can also be set using environment variables.
See the [Environment Variables Reference](./environment-variables) for a complete list with examples.
</Note>
## Location of the `config.toml` File
When running OpenHands in CLI, headless, or development mode, you can use a project-specific `config.toml` file for configuration, which must be
@@ -18,6 +23,11 @@ specify a different path to the `config.toml` file.
The core configuration options are defined in the `[core]` section of the `config.toml` file.
Core configuration options can be set as environment variables by converting to uppercase. For example:
- `debug` → `DEBUG`
- `cache_dir` → `CACHE_DIR`
- `runtime` → `RUNTIME`
### Workspace
- `workspace_base` **(Deprecated)**
- Type: `str`
@@ -141,6 +151,11 @@ The LLM (Large Language Model) configuration options are defined in the `[llm]`
To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LLM_NUM_RETRIES`.
All LLM configuration options can be set as environment variables by prefixing with `LLM_` and converting to uppercase. For example:
- `model` → `LLM_MODEL`
- `api_key` → `LLM_API_KEY`
- `base_url` → `LLM_BASE_URL`
<Note>
For development setups, you can also define custom named LLM configurations. See [Custom LLM Configurations](./llms/custom-llm-configs) for details.
</Note>
@@ -277,6 +292,11 @@ For development setups, you can also define custom named LLM configurations. See
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
Agent configuration options can be set as environment variables by prefixing with `AGENT_` and converting to uppercase. For example:
- `enable_browsing` → `AGENT_ENABLE_BROWSING`
- `function_calling` → `AGENT_FUNCTION_CALLING`
- `llm_config` → `AGENT_LLM_CONFIG`
### LLM Configuration
- `llm_config`
- Type: `str`
@@ -328,6 +348,11 @@ The sandbox configuration options are defined in the `[sandbox]` section of the
To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-e SANDBOX_TIMEOUT`.
All sandbox configuration options can be set as environment variables by prefixing with `SANDBOX_` and converting to uppercase. For example:
- `timeout` → `SANDBOX_TIMEOUT`
- `user_id` → `SANDBOX_USER_ID`
- `base_container_image` → `SANDBOX_BASE_CONTAINER_IMAGE`
### Execution
- `timeout`
- Type: `int`
@@ -390,6 +415,10 @@ The security configuration options are defined in the `[security]` section of th
To use these with the docker command, pass in `-e SECURITY_<option>`. Example: `-e SECURITY_CONFIRMATION_MODE`.
All security configuration options can be set as environment variables by prefixing with `SECURITY_` and converting to uppercase. For example:
- `confirmation_mode` → `SECURITY_CONFIRMATION_MODE`
- `security_analyzer` → `SECURITY_SECURITY_ANALYZER`
### Confirmation Mode
- `confirmation_mode`
- Type: `bool`

View File

@@ -0,0 +1,251 @@
---
title: Environment Variables Reference
description: Complete reference of all environment variables supported by OpenHands
---
This page provides a reference of environment variables that can be used to configure OpenHands. Environment variables provide an alternative to TOML configuration files and are particularly useful for containerized deployments, CI/CD pipelines, and cloud environments.
## Environment Variable Naming Convention
OpenHands follows a consistent naming pattern for environment variables:
- **Core settings**: Direct uppercase mapping (e.g., `debug` → `DEBUG`)
- **LLM settings**: Prefixed with `LLM_` (e.g., `model` → `LLM_MODEL`)
- **Agent settings**: Prefixed with `AGENT_` (e.g., `enable_browsing` → `AGENT_ENABLE_BROWSING`)
- **Sandbox settings**: Prefixed with `SANDBOX_` (e.g., `timeout` → `SANDBOX_TIMEOUT`)
- **Security settings**: Prefixed with `SECURITY_` (e.g., `confirmation_mode` → `SECURITY_CONFIRMATION_MODE`)
## Core Configuration Variables
These variables correspond to the `[core]` section in `config.toml`:
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `DEBUG` | boolean | `false` | Enable debug logging throughout the application |
| `DISABLE_COLOR` | boolean | `false` | Disable colored output in terminal |
| `CACHE_DIR` | string | `"/tmp/cache"` | Directory path for caching |
| `SAVE_TRAJECTORY_PATH` | string | `"./trajectories"` | Path to store conversation trajectories |
| `REPLAY_TRAJECTORY_PATH` | string | `""` | Path to load and replay a trajectory file |
| `FILE_STORE_PATH` | string | `"/tmp/file_store"` | File store directory path |
| `FILE_STORE` | string | `"memory"` | File store type (`memory`, `local`, etc.) |
| `FILE_UPLOADS_MAX_FILE_SIZE_MB` | integer | `0` | Maximum file upload size in MB (0 = no limit) |
| `FILE_UPLOADS_RESTRICT_FILE_TYPES` | boolean | `false` | Whether to restrict file upload types |
| `FILE_UPLOADS_ALLOWED_EXTENSIONS` | list | `[".*"]` | List of allowed file extensions for uploads |
| `MAX_BUDGET_PER_TASK` | float | `0.0` | Maximum budget per task (0.0 = no limit) |
| `MAX_ITERATIONS` | integer | `100` | Maximum number of iterations per task |
| `RUNTIME` | string | `"docker"` | Runtime environment (`docker`, `local`, `cli`, etc.) |
| `DEFAULT_AGENT` | string | `"CodeActAgent"` | Default agent class to use |
| `JWT_SECRET` | string | auto-generated | JWT secret for authentication |
| `RUN_AS_OPENHANDS` | boolean | `true` | Whether to run as the openhands user |
| `VOLUMES` | string | `""` | Volume mounts in format `host:container[:mode]` |
## LLM Configuration Variables
These variables correspond to the `[llm]` section in `config.toml`:
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `LLM_MODEL` | string | `"claude-3-5-sonnet-20241022"` | LLM model to use |
| `LLM_API_KEY` | string | `""` | API key for the LLM provider |
| `LLM_BASE_URL` | string | `""` | Custom API base URL |
| `LLM_API_VERSION` | string | `""` | API version to use |
| `LLM_TEMPERATURE` | float | `0.0` | Sampling temperature |
| `LLM_TOP_P` | float | `1.0` | Top-p sampling parameter |
| `LLM_MAX_INPUT_TOKENS` | integer | `0` | Maximum input tokens (0 = no limit) |
| `LLM_MAX_OUTPUT_TOKENS` | integer | `0` | Maximum output tokens (0 = no limit) |
| `LLM_MAX_MESSAGE_CHARS` | integer | `30000` | Maximum characters that will be sent to the model in observation content |
| `LLM_TIMEOUT` | integer | `0` | API timeout in seconds (0 = no timeout) |
| `LLM_NUM_RETRIES` | integer | `8` | Number of retry attempts |
| `LLM_RETRY_MIN_WAIT` | integer | `15` | Minimum wait time between retries (seconds) |
| `LLM_RETRY_MAX_WAIT` | integer | `120` | Maximum wait time between retries (seconds) |
| `LLM_RETRY_MULTIPLIER` | float | `2.0` | Exponential backoff multiplier |
| `LLM_DROP_PARAMS` | boolean | `false` | Drop unsupported parameters without error |
| `LLM_CACHING_PROMPT` | boolean | `true` | Enable prompt caching if supported |
| `LLM_DISABLE_VISION` | boolean | `false` | Disable vision capabilities for cost reduction |
| `LLM_CUSTOM_LLM_PROVIDER` | string | `""` | Custom LLM provider name |
| `LLM_OLLAMA_BASE_URL` | string | `""` | Base URL for Ollama API |
| `LLM_INPUT_COST_PER_TOKEN` | float | `0.0` | Cost per input token |
| `LLM_OUTPUT_COST_PER_TOKEN` | float | `0.0` | Cost per output token |
| `LLM_REASONING_EFFORT` | string | `""` | Reasoning effort for o-series models (`low`, `medium`, `high`) |
### AWS Configuration
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `LLM_AWS_ACCESS_KEY_ID` | string | `""` | AWS access key ID |
| `LLM_AWS_SECRET_ACCESS_KEY` | string | `""` | AWS secret access key |
| `LLM_AWS_REGION_NAME` | string | `""` | AWS region name |
## Agent Configuration Variables
These variables correspond to the `[agent]` section in `config.toml`:
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `AGENT_LLM_CONFIG` | string | `""` | Name of LLM config group to use |
| `AGENT_FUNCTION_CALLING` | boolean | `true` | Enable function calling |
| `AGENT_ENABLE_BROWSING` | boolean | `false` | Enable browsing delegate |
| `AGENT_ENABLE_LLM_EDITOR` | boolean | `false` | Enable LLM-based editor |
| `AGENT_ENABLE_JUPYTER` | boolean | `false` | Enable Jupyter integration |
| `AGENT_ENABLE_HISTORY_TRUNCATION` | boolean | `true` | Enable history truncation |
| `AGENT_ENABLE_PROMPT_EXTENSIONS` | boolean | `true` | Enable microagents (prompt extensions) |
| `AGENT_DISABLED_MICROAGENTS` | list | `[]` | List of microagents to disable |
## Sandbox Configuration Variables
These variables correspond to the `[sandbox]` section in `config.toml`:
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `SANDBOX_TIMEOUT` | integer | `120` | Sandbox timeout in seconds |
| `SANDBOX_USER_ID` | integer | `1000` | User ID for sandbox processes |
| `SANDBOX_BASE_CONTAINER_IMAGE` | string | `"nikolaik/python-nodejs:python3.12-nodejs22"` | Base container image |
| `SANDBOX_USE_HOST_NETWORK` | boolean | `false` | Use host networking |
| `SANDBOX_RUNTIME_BINDING_ADDRESS` | string | `"0.0.0.0"` | Runtime binding address |
| `SANDBOX_ENABLE_AUTO_LINT` | boolean | `false` | Enable automatic linting |
| `SANDBOX_INITIALIZE_PLUGINS` | boolean | `true` | Initialize sandbox plugins |
| `SANDBOX_RUNTIME_EXTRA_DEPS` | string | `""` | Extra dependencies to install |
| `SANDBOX_RUNTIME_STARTUP_ENV_VARS` | dict | `{}` | Environment variables for runtime |
| `SANDBOX_BROWSERGYM_EVAL_ENV` | string | `""` | BrowserGym evaluation environment |
| `SANDBOX_VOLUMES` | string | `""` | Volume mounts (replaces deprecated workspace settings) |
| `SANDBOX_RUNTIME_CONTAINER_IMAGE` | string | `""` | Pre-built runtime container image |
| `SANDBOX_KEEP_RUNTIME_ALIVE` | boolean | `false` | Keep runtime alive after session ends |
| `SANDBOX_PAUSE_CLOSED_RUNTIMES` | boolean | `false` | Pause instead of stopping closed runtimes |
| `SANDBOX_CLOSE_DELAY` | integer | `300` | Delay before closing idle runtimes (seconds) |
| `SANDBOX_RM_ALL_CONTAINERS` | boolean | `false` | Remove all containers when stopping |
| `SANDBOX_ENABLE_GPU` | boolean | `false` | Enable GPU support |
| `SANDBOX_CUDA_VISIBLE_DEVICES` | string | `""` | Specify GPU devices by ID |
| `SANDBOX_VSCODE_PORT` | integer | auto | Specific port for VSCode server |
### Sandbox Environment Variables
Variables prefixed with `SANDBOX_ENV_` are passed through to the sandbox environment:
| Environment Variable | Description |
|---------------------|-------------|
| `SANDBOX_ENV_*` | Any variable with this prefix is passed to the sandbox (e.g., `SANDBOX_ENV_OPENAI_API_KEY`) |
## Security Configuration Variables
These variables correspond to the `[security]` section in `config.toml`:
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `SECURITY_CONFIRMATION_MODE` | boolean | `false` | Enable confirmation mode for actions |
| `SECURITY_SECURITY_ANALYZER` | string | `"llm"` | Security analyzer to use (`llm`, `invariant`) |
| `SECURITY_ENABLE_SECURITY_ANALYZER` | boolean | `true` | Enable security analysis |
## Debug and Logging Variables
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `DEBUG` | boolean | `false` | Enable general debug logging |
| `DEBUG_LLM` | boolean | `false` | Enable LLM-specific debug logging |
| `DEBUG_RUNTIME` | boolean | `false` | Enable runtime debug logging |
| `LOG_TO_FILE` | boolean | auto | Log to file (auto-enabled when DEBUG=true) |
## Runtime-Specific Variables
### Docker Runtime
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `SANDBOX_VOLUME_OVERLAYS` | string | `""` | Volume overlay configurations |
### Remote Runtime
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `SANDBOX_API_KEY` | string | `""` | API key for remote runtime |
| `SANDBOX_REMOTE_RUNTIME_API_URL` | string | `""` | Remote runtime API URL |
### Local Runtime
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `RUNTIME_URL` | string | `""` | Runtime URL for local runtime |
| `RUNTIME_URL_PATTERN` | string | `""` | Runtime URL pattern |
| `RUNTIME_ID` | string | `""` | Runtime identifier |
| `LOCAL_RUNTIME_MODE` | string | `""` | Enable local runtime mode (`1` to enable) |
## Integration Variables
### GitHub Integration
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `GITHUB_TOKEN` | string | `""` | GitHub personal access token |
### Third-Party API Keys
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `OPENAI_API_KEY` | string | `""` | OpenAI API key |
| `ANTHROPIC_API_KEY` | string | `""` | Anthropic API key |
| `GOOGLE_API_KEY` | string | `""` | Google API key |
| `AZURE_API_KEY` | string | `""` | Azure API key |
| `TAVILY_API_KEY` | string | `""` | Tavily search API key |
## Server Configuration Variables
These are primarily used when running OpenHands as a server:
| Environment Variable | Type | Default | Description |
|---------------------|------|---------|-------------|
| `FRONTEND_PORT` | integer | `3000` | Frontend server port |
| `BACKEND_PORT` | integer | `8000` | Backend server port |
| `FRONTEND_HOST` | string | `"localhost"` | Frontend host address |
| `BACKEND_HOST` | string | `"localhost"` | Backend host address |
| `WEB_HOST` | string | `"localhost"` | Web server host |
| `SERVE_FRONTEND` | boolean | `true` | Whether to serve frontend |
## Deprecated Variables
These variables are deprecated and should be replaced:
| Environment Variable | Replacement | Description |
|---------------------|-------------|-------------|
| `WORKSPACE_BASE` | `SANDBOX_VOLUMES` | Use volume mounting instead |
| `WORKSPACE_MOUNT_PATH` | `SANDBOX_VOLUMES` | Use volume mounting instead |
| `WORKSPACE_MOUNT_PATH_IN_SANDBOX` | `SANDBOX_VOLUMES` | Use volume mounting instead |
| `WORKSPACE_MOUNT_REWRITE` | `SANDBOX_VOLUMES` | Use volume mounting instead |
## Usage Examples
### Basic Setup with OpenAI
```bash
export LLM_MODEL="gpt-4o"
export LLM_API_KEY="your-openai-api-key"
export DEBUG=true
```
### Docker Deployment with Custom Volumes
```bash
export RUNTIME="docker"
export SANDBOX_VOLUMES="/host/workspace:/workspace:rw,/host/data:/data:ro"
export SANDBOX_TIMEOUT=300
```
### Remote Runtime Configuration
```bash
export RUNTIME="remote"
export SANDBOX_API_KEY="your-remote-api-key"
export SANDBOX_REMOTE_RUNTIME_API_URL="https://your-runtime-api.com"
```
### Security-Enhanced Setup
```bash
export SECURITY_CONFIRMATION_MODE=true
export SECURITY_SECURITY_ANALYZER="llm"
export DEBUG_RUNTIME=true
```
## Notes
1. **Boolean Values**: Environment variables expecting boolean values accept `true`/`false`, `1`/`0`, or `yes`/`no` (case-insensitive).
2. **List Values**: Lists should be provided as Python literal strings, e.g., `AGENT_DISABLED_MICROAGENTS='["microagent1", "microagent2"]'`.
3. **Dictionary Values**: Dictionaries should be provided as Python literal strings, e.g., `SANDBOX_RUNTIME_STARTUP_ENV_VARS='{"KEY": "value"}'`.
4. **Precedence**: Environment variables take precedence over TOML configuration files.
5. **Docker Usage**: When using Docker, pass environment variables with the `-e` flag:
```bash
docker run -e LLM_API_KEY="your-key" -e DEBUG=true openhands/openhands
```
6. **Validation**: Invalid environment variable values will be logged as errors and fall back to defaults.

View File

@@ -113,7 +113,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -122,7 +122,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.55 \
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
python -m openhands.cli.entry --override-cli-mode true
```

View File

@@ -85,11 +85,11 @@ You can use the Settings page at any time to:
- Setup the LLM provider and model for OpenHands.
- [Setup the search engine](/usage/search-engine-setup).
- [Configure MCP servers](/usage/mcp).
- [Configure MCP servers](/usage/settings/mcp-settings).
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup), [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
and [connect to Bitbucket](/usage/how-to/gui-mode#bitbucket-setup).
- Set application settings like your preferred language, notifications and other preferences.
- [Manage custom secrets](/usage/common-settings#secrets-management).
- [Manage custom secrets](/usage/settings/secrets-settings).
#### GitHub Setup

View File

@@ -52,7 +52,7 @@ Set environment variables and run the Docker command:
```bash
# Set required environment variables
export SANDBOX_VOLUMES="/path/to/workspace" # See SANDBOX_VOLUMES docs for details
export SANDBOX_VOLUMES="/path/to/workspace:/workspace:rw" # Format: host_path:container_path:mode
export LLM_MODEL="anthropic/claude-sonnet-4-20250514"
export LLM_API_KEY="your-api-key"
export SANDBOX_SELECTED_REPO="owner/repo-name" # Optional: requires GITHUB_TOKEN
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.55 \
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.55
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.55
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.57
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None

View File

@@ -30,6 +30,20 @@ When running OpenHands, you'll need to set the following in the OpenHands UI thr
## Pricing
Pricing follows official API provider rates. [You can view model prices here.](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json)
Pricing follows official API provider rates. Below are the current pricing details for OpenHands models:
For `qwen3-coder-480b`, we charge the cheapest FP8 rate available on openrouter: \$0.4 per million input tokens and \$1.6 per million output tokens.
| Model | Input Cost (per 1M tokens) | Cached Input Cost (per 1M tokens) | Output Cost (per 1M tokens) | Max Input Tokens | Max Output Tokens |
|-------|----------------------------|-----------------------------------|------------------------------|------------------|-------------------|
| claude-opus-4-20250514 | $15.00 | $1.50 | $75.00 | 200,000 | 32,000 |
| claude-sonnet-4-20250514 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
| devstral-medium-2507 | $0.40 | N/A | $2.00 | 128,000 | 128,000 |
| devstral-small-2505 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
| devstral-small-2507 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
| gemini-2.5-pro | $1.25 | $0.31 | $10.00 | 1,048,576 | 65,535 |
| gpt-5-2025-08-07 | $1.25 | $0.125 | $10.00 | 400,000 | 128,000 |
| gpt-5-mini-2025-08-07 | $0.25 | $0.025 | $2.00 | 400,000 | 128,000 |
| o3 | $2.00 | $0.50 | $8.00 | 200,000 | 100,000 |
| o4-mini | $1.10 | $0.28 | $4.40 | 200,000 | 100,000 |
| qwen3-coder-480b | $0.40 | N/A | $1.60 | N/A | N/A |
**Note:** Cached input tokens are charged at a reduced rate when the same content is reused across requests. Models that don't support prompt caching show "N/A" for cached input cost.

View File

@@ -116,17 +116,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
<Accordion title="Docker Command (Click to expand)">
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.55-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.55
docker.all-hands.dev/all-hands-ai/openhands:0.57
```
</Accordion>

View File

@@ -10,12 +10,15 @@ Model Context Protocol (MCP) is a mechanism that allows OpenHands to communicate
servers can provide additional functionality to the agent, such as specialized data processing, external API access,
or custom tools. MCP is based on the open standard defined at [modelcontextprotocol.io](https://modelcontextprotocol.io).
## Supported MCPs
<Note>
MCP is currently not available on OpenHands Cloud. This feature is only available when running OpenHands locally.
</Note>
OpenHands supports the following MCP transport protocols:
### How MCP Works
* [Server-Sent Events (SSE)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse)
* [Streamable HTTP (SHTTP)](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http)
* [Standard Input/Output (stdio)](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio)
## How MCP Works
When OpenHands starts, it:
@@ -33,15 +36,90 @@ The agent can then use these tools just like any built-in tool. When the agent c
## Configuration
MCP configuration can be defined in:
* The OpenHands UI through the Settings under the `MCP` tab.
* The OpenHands UI in the `Settings > MCP` page.
* The `config.toml` file under the `[mcp]` section if not using the UI.
### Configuration Options
#### SSE Servers
SSE servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SSE server.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
#### SHTTP Servers
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server.
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication.
- `timeout` (optional)
- Type: `int`
- Default: `60`
- Range: `1-3600` seconds (1 hour maximum)
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
- **Use Cases:**
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries.
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls.
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations.
<Note>
This timeout only applies to individual tool calls, not server connection establishment.
</Note>
#### Stdio Servers
<Note>
While stdio servers are supported, [we recommend using MCP proxies](/usage/settings/mcp-settings#configuration-examples) for
better reliability and performance.
</Note>
Stdio servers are configured using an object with the following properties:
- `name` (required)
- Type: `str`
- Description: A unique name for the server.
- `command` (required)
- Type: `str`
- Description: The command to run the server.
- `args` (optional)
- Type: `list of str`
- Default: `[]`
- Description: Command-line arguments to pass to the server.
- `env` (optional)
- Type: `dict of str to str`
- Default: `{}`
- Description: Environment variables to set for the server process.
##### When to Use Direct Stdio
Direct stdio connections may still be appropriate in these scenarios:
- **Development and testing**: Quick prototyping of MCP servers.
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access.
- **Local-only environments**: When you don't want to manage additional proxy processes.
### Configuration Examples
#### Recommended: Using Proxy Servers (SSE/HTTP)
For stdio-based MCP servers, we recommend using MCP proxy tools like [`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to HTTP/SSE endpoints:
For stdio-based MCP servers, we recommend using MCP proxy tools like
[`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to
HTTP/SSE endpoints.
Start the proxy servers separately:
```bash
@@ -67,10 +145,21 @@ sse_servers = [
# External MCP service with authentication
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
]
# SHTTP Servers - Modern streamable HTTP transport (recommended)
shttp_servers = [
# Basic SHTTP server with default 60s timeout
"https://api.example.com/mcp/shttp",
# Server with custom timeout for heavy operations
{
url = "https://files.example.com/mcp/shttp",
api_key = "your-api-key",
timeout = 1800 # 30 minutes for large file processing
}
]
```
#### Alternative: Direct Stdio Servers (Not Recommended for Production)
```toml
@@ -92,105 +181,12 @@ stdio_servers = [
]
```
## Configuration Options
### SSE Servers
SSE servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SSE server
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication
### SHTTP Servers
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
- `url` (required)
- Type: `str`
- Description: The URL of the SHTTP server
- `api_key` (optional)
- Type: `str`
- Description: API key for authentication
### Stdio Servers
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
Stdio servers are configured using an object with the following properties:
- `name` (required)
- Type: `str`
- Description: A unique name for the server
- `command` (required)
- Type: `str`
- Description: The command to run the server
- `args` (optional)
- Type: `list of str`
- Default: `[]`
- Description: Command-line arguments to pass to the server
- `env` (optional)
- Type: `dict of str to str`
- Default: `{}`
- Description: Environment variables to set for the server process
#### When to Use Direct Stdio
Direct stdio connections may still be appropriate in these scenarios:
- **Development and testing**: Quick prototyping of MCP servers
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access
- **Local-only environments**: When you don't want to manage additional proxy processes
For production use, we recommend using proxy tools like SuperGateway.
### Other Proxy Tools
Other options include:
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers
- **Docker-based proxies**: Containerized solutions for better isolation
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints
### Troubleshooting MCP Connections
#### Common Issues with Stdio Servers
- **Process crashes**: Stdio processes may crash without proper error handling
- **Deadlocks**: Stdio communication can deadlock under high load
- **Resource leaks**: Zombie processes if not properly managed
- **Debugging difficulty**: Hard to inspect stdio communication
#### Benefits of Using Proxies
- **HTTP status codes**: Clear error reporting via standard HTTP responses
- **Request logging**: Easy to log and monitor HTTP requests
- **Load balancing**: Can distribute requests across multiple server instances
- **Health checks**: HTTP endpoints can provide health status
- **CORS support**: Better integration with web-based tools
## Transport Protocols
OpenHands supports three different MCP transport protocols:
### Server-Sent Events (SSE)
SSE is a legacy HTTP-based transport that uses Server-Sent Events for server-to-client communication and HTTP POST requests for client-to-server communication. This transport is suitable for basic streaming scenarios but has limitations in session management and connection resumability.
### Streamable HTTP (SHTTP)
SHTTP is the modern HTTP-based transport protocol that provides enhanced features over SSE:
- **Improved Session Management**: Supports stateful sessions with session IDs for maintaining context across requests
- **Connection Resumability**: Can resume broken connections and replay missed messages using event IDs
- **Bidirectional Communication**: Uses HTTP POST for client-to-server and optional SSE streams for server-to-client communication
- **Better Error Handling**: Enhanced error reporting and recovery mechanisms
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
### Standard Input/Output (stdio)
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers.
- **Docker-based proxies**: Containerized solutions for better isolation.
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints.

View File

@@ -1,28 +1,19 @@
---
title: OpenHands Settings
description: Overview of some of the settings available in OpenHands.
title: Secrets Management
description: How to manage secrets in OpenHands.
---
## Openhands Cloud vs Running on Your Own
There are some differences between the settings available in OpenHands Cloud and those available when running OpenHands
on your own:
* [OpenHands Cloud settings](/usage/cloud/cloud-ui#settings)
* [Settings available when running on your own](/usage/how-to/gui-mode#settings)
Refer to these pages for more detailed information.
## Secrets Management
## Overview
OpenHands provides a secrets manager that allows you to securely store and manage sensitive information that can be
accessed by the agent during runtime, such as API keys. These secrets are automatically exported as environment
variables in the agent's runtime environment.
### Accessing the Secrets Manager
## Accessing the Secrets Manager
In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of all your existing custom secrets.
Navigate to the `Settings > Secrets` page. Here, you'll see a list of all your existing custom secrets.
### Adding a New Secret
## Adding a New Secret
1. Click `Add a new secret`.
2. Fill in the following fields:
- **Name**: A unique identifier for your secret (e.g., `AWS_ACCESS_KEY`). This will be the environment variable name.
@@ -30,7 +21,7 @@ In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of
- **Description** (optional): A brief description of what the secret is used for, which is also provided to the agent.
3. Click `Add secret` to save.
### Editing a Secret
## Editing a Secret
1. Click the `Edit` button next to the secret you want to modify.
2. You can update the name and description of the secret.
@@ -39,14 +30,13 @@ In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of
value, delete the secret and create a new one.
</Note>
### Deleting a Secret
## Deleting a Secret
1. Click the `Delete` button next to the secret you want to remove.
2. Select `Confirm` to delete the secret.
### Using Secrets in the Agent
## Using Secrets in the Agent
- All custom secrets are automatically exported as environment variables in the agent's runtime environment.
- You can access them in your code using standard environment variable access methods
(e.g., `os.environ['SECRET_NAME']` in Python).
- Example: If you create a secret named `OPENAI_API_KEY`, you can access it in your code as
`process.env.OPENAI_API_KEY` in JavaScript or `os.environ['OPENAI_API_KEY']` in Python.
- You can access them in your code using standard environment variable access methods. For example, if you create a
secret named `OPENAI_API_KEY`, you can access it in your code as `process.env.OPENAI_API_KEY` in JavaScript or
`os.environ['OPENAI_API_KEY']` in Python.

View File

@@ -7,14 +7,28 @@ LABEL com.datadoghq.tags.service="deploy"
LABEL com.datadoghq.tags.env="${DD_ENV}"
# Install Node.js v20+ and npm (which includes npx)
# Apply security updates to fix CVEs
RUN apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
apt-get install -y jq gettext && \
apt-get clean
# Apply security updates for packages with available fixes
apt-get upgrade -y \
libc-bin \
libc6 \
libgnutls30 \
libsqlite3-0 \
perl-base && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace posthog "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy
# Install Python packages with security fixes
RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gspread stripe python-keycloak asyncpg sqlalchemy[asyncio] resend tenacity slack-sdk ddtrace posthog "limits==5.2.0" coredis prometheus-client shap scikit-learn pandas numpy && \
# Update packages with known CVE fixes
pip install --upgrade \
"mcp>=1.10.0" \
"pillow>=11.3.0"
WORKDIR /app
COPY enterprise .

View File

@@ -27,7 +27,7 @@ repos:
- id: ruff
entry: ruff check --config enterprise/dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
args: [--fix]
args: [--fix, --show-fixes]
files: ^enterprise/
# Run the formatter.
- id: ruff-format
@@ -46,7 +46,8 @@ repos:
- types-toml
- types-redis
- lxml
# TODO: Add OpenHands in parent
# OpenHands package in repo root
- ./
- stripe==11.5.0
- pygithub==2.6.1
# To see gaps add `--html-report mypy-report/`

View File

@@ -7,15 +7,11 @@ warn_unreachable = True
warn_redundant_casts = True
no_implicit_optional = True
strict_optional = True
exclude = (^enterprise/migrations/.*|^openhands/.*)
disable_error_code = type-abstract
exclude = (^enterprise/migrations/.*)
[mypy-enterprise.tests.unit.test_auth_routes.*]
disable_error_code = union-attr
[mypy-enterprise.sync.install_gitlab_webhooks.*]
disable_error_code = redundant-cast
# Let the other config check base openhands packages
[mypy-openhands.*]
follow_imports = skip
ignore_missing_imports = True

View File

@@ -2,7 +2,6 @@ from experiments.constants import (
ENABLE_EXPERIMENT_MANAGER,
)
from experiments.experiment_versions import (
handle_claude4_vs_gpt5_experiment,
handle_condenser_max_step_experiment,
handle_system_prompt_experiment,
)
@@ -44,9 +43,6 @@ class SaaSExperimentManager(ExperimentManager):
return conversation_settings
# Apply conversation-scoped experiments
conversation_settings = handle_claude4_vs_gpt5_experiment(
user_id, conversation_id, conversation_settings
)
conversation_settings = handle_condenser_max_step_experiment(
user_id, conversation_id, conversation_settings
)
@@ -55,7 +51,7 @@ class SaaSExperimentManager(ExperimentManager):
@staticmethod
def run_config_variant_test(
user_id: str, conversation_id: str, config: OpenHandsConfig
user_id: str | None, conversation_id: str, config: OpenHandsConfig
) -> OpenHandsConfig:
"""
Run agent config variant test and potentially modify the OpenHands config

View File

@@ -390,24 +390,24 @@ class GitHubDataCollector:
merged_by = None
merge_commit_sha = None
if is_merged:
merged_by = pr_data.get('mergedBy', {}).get('login')
merge_commit_sha = pr_data.get('mergeCommit', {}).get('oid')
merged_by = (pr_data.get('mergedBy') or {}).get('login')
merge_commit_sha = (pr_data.get('mergeCommit') or {}).get('oid')
return {
'repo_metadata': self._extract_repo_metadata(repo_data),
'pr_metadata': {
'username': pr_data.get('author', {}).get('login'),
'number': pr_data['number'],
'title': pr_data['title'],
'body': pr_data['body'],
'username': (pr_data.get('author') or {}).get('login'),
'number': pr_data.get('number'),
'title': pr_data.get('title'),
'body': pr_data.get('body'),
'comments': pr_comments,
},
'commits': commits,
'review_comments': review_comments,
'merge_status': {
'merged': pr_data['merged'],
'merged': pr_data.get('merged'),
'merged_by': merged_by,
'state': pr_data['state'],
'state': pr_data.get('state'),
'merge_commit_sha': merge_commit_sha,
},
'openhands_stats': {

View File

@@ -62,7 +62,13 @@ class GitlabManager(Manager):
logger.warning(f'Got invalid keyloak user id for GitLab User {user_id}')
return False
gitlab_service = GitLabServiceImpl(external_auth_id=keycloak_user_id)
# Importing here prevents circular import
from integrations.gitlab.gitlab_service import SaaSGitLabService
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
external_auth_id=keycloak_user_id
)
return await gitlab_service.user_has_write_access(project_id)
async def receive_message(self, message: Message):
@@ -119,7 +125,13 @@ class GitlabManager(Manager):
gitlab_view: The GitLab view object containing issue/PR/comment info
"""
keycloak_user_id = gitlab_view.user_info.keycloak_user_id
gitlab_service = GitLabServiceImpl(external_auth_id=keycloak_user_id)
# Importing here prevents circular import
from integrations.gitlab.gitlab_service import SaaSGitLabService
gitlab_service: SaaSGitLabService = GitLabServiceImpl(
external_auth_id=keycloak_user_id
)
outgoing_message = message.message

View File

@@ -47,14 +47,14 @@ class GitlabIssue(ResolverViewInterface):
)
self.previous_comments = await gitlab_service.get_issue_or_mr_comments(
self.project_id, self.issue_number, is_mr=self.is_mr
str(self.project_id), self.issue_number, is_mr=self.is_mr
)
(
self.title,
self.description,
) = await gitlab_service.get_issue_or_mr_title_and_body(
self.project_id, self.issue_number, is_mr=self.is_mr
str(self.project_id), self.issue_number, is_mr=self.is_mr
)
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
@@ -199,11 +199,11 @@ class GitlabInlineMRComment(GitlabMRComment):
self.title,
self.description,
) = await gitlab_service.get_issue_or_mr_title_and_body(
self.project_id, self.issue_number, is_mr=self.is_mr
str(self.project_id), self.issue_number, is_mr=self.is_mr
)
self.previous_comments = await gitlab_service.get_review_thread_comments(
self.project_id, self.issue_number, self.discussion_id
str(self.project_id), self.issue_number, self.discussion_id
)
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:

View File

@@ -172,6 +172,17 @@ def get_summary_for_agent_state(
return f'OpenHands encountered an error: **{reason}**.\n\n[See the conversation]({conversation_link}) for more information.'
if state == AgentState.AWAITING_USER_INPUT:
logger.info(
'Agent is awaiting user input',
extra={
'agent_state': state.value,
'conversation_link': conversation_link,
'observation_reason': getattr(observation, 'reason', None),
},
)
return f'OpenHands is waiting for your input. [Continue the conversation]({conversation_link}) to provide additional instructions.'
# Log unknown agent state as error
logger.error(
'Unknown error: Unhandled agent state',

View File

@@ -0,0 +1,50 @@
"""add cancellation fields to subscription_access
Revision ID: 075
Revises: 074
Create Date: 2025-01-11
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '075'
down_revision: Union[str, None] = '074'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add cancelled_at field to track cancellation timestamp
op.add_column(
'subscription_access',
sa.Column('cancelled_at', sa.DateTime(timezone=True), nullable=True),
)
# Add stripe_subscription_id field to enable cancellation via Stripe API
op.add_column(
'subscription_access',
sa.Column('stripe_subscription_id', sa.String(), nullable=True),
)
# Create index on stripe_subscription_id for efficient lookups
op.create_index(
'ix_subscription_access_stripe_subscription_id',
'subscription_access',
['stripe_subscription_id'],
)
def downgrade() -> None:
# Drop index
op.drop_index(
'ix_subscription_access_stripe_subscription_id', 'subscription_access'
)
# Drop columns
op.drop_column('subscription_access', 'stripe_subscription_id')
op.drop_column('subscription_access', 'cancelled_at')

315
enterprise/poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -766,7 +766,7 @@ version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "test"]
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@@ -836,6 +836,7 @@ files = [
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
[package.dependencies]
pycparser = "*"
@@ -1426,73 +1427,73 @@ yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "ddtrace"
version = "3.12.4"
version = "3.13.0"
description = "Datadog APM client library"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "ddtrace-3.12.4-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:222dc483f22a065795f473cad6fc6e798ecf9da9f4fc99ca87f1ba70f34d21b1"},
{file = "ddtrace-3.12.4-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:196f114a70b75320876f6861c10435c6d4ea50e0f406328b0862a021c344d002"},
{file = "ddtrace-3.12.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4200e8b057b29ce3ba0889a9d423e4d105b0ba35d4bd58ba2670763018909623"},
{file = "ddtrace-3.12.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fc1449d511e04e8b2596eee6d1ad2d3420dff23f6dfd8a899c5e3e03dfe8ba5"},
{file = "ddtrace-3.12.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ebae69206957837341cd94bbe78e5242395f7571455dfe911b56ea2f7404ada"},
{file = "ddtrace-3.12.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a08cd25234358a2427494d4059ee12afc83e083bad65f2bd62417fd935caa737"},
{file = "ddtrace-3.12.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fbe90ff2c914c753116807ddffde9065ecbf9944bdc4932862c3f5835485004d"},
{file = "ddtrace-3.12.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b3be9452bc76f730203b86272f8312c7e195b3125f964900df3f41c39ec0c94"},
{file = "ddtrace-3.12.4-cp310-cp310-win32.whl", hash = "sha256:b331bc0c3000cea1fd70febcf004b5a617c63b9050094f08100891a23638986d"},
{file = "ddtrace-3.12.4-cp310-cp310-win_amd64.whl", hash = "sha256:018d19e2a1e7585df65d938ae51c385d673e8001b66827a47e499ade3b227ad2"},
{file = "ddtrace-3.12.4-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0de9563bad27007fd64059e3b5bb3a791184e39619fdb096044e68a454b4427b"},
{file = "ddtrace-3.12.4-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:d0c5b84d066ca3d60da9636df526382416dae4288f66fcdaca7a2e765ca2f0bd"},
{file = "ddtrace-3.12.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff1812b1d7e8344088a978f1d4f621257fe1ad5d8efc07317a3c90c280e5bdc4"},
{file = "ddtrace-3.12.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd0ac6ba50d36689bf0eeadc88ce91b60bc863036f3dea90dd5656f39bce3ac4"},
{file = "ddtrace-3.12.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8f99761f946b2b7cc2ea4cba821a7a94d05a9eb8cd8a3feabdb49eeacc18bb9"},
{file = "ddtrace-3.12.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c4f66c48eca7d6759766fcaf24ac3a65e712e62ae7b1f521a7da2b8d7f101849"},
{file = "ddtrace-3.12.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:42d46f17baaa5040e4f438544603033af8eeec32067c3712a9e620392d75f484"},
{file = "ddtrace-3.12.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aa0606a07e7d05881f2ef1172f4175733ae3006bfc3c7cfd58b82ea3ed75c914"},
{file = "ddtrace-3.12.4-cp311-cp311-win32.whl", hash = "sha256:efde4b33502f3897993a564ee56d0ea30a65d658d616d16c5ef23c850d0e3417"},
{file = "ddtrace-3.12.4-cp311-cp311-win_amd64.whl", hash = "sha256:7d6117fabcd98d3a696d1f80314c9b9e4325b362b31714551efd729a02152ff1"},
{file = "ddtrace-3.12.4-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:734d782d9f64de378f632516554b9da0dfbf54cf1bb7be4bb1085165e7c052ad"},
{file = "ddtrace-3.12.4-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:fbf2543856b4ed5a1d6ac59c82f8c76cef5f4ef65361d59f60ce01db92a4c8d1"},
{file = "ddtrace-3.12.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:751ce0410405113286bd558fd402f8a58f5b455cee4deb467ae9ae87e5713547"},
{file = "ddtrace-3.12.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd804c06d62926cc18a354987f7d5c1fecd1da30983041d3f98bc402d9d23713"},
{file = "ddtrace-3.12.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e55b911d5b9f1bd73731870962809f9089677f4d3736d52587b4ba76eee56962"},
{file = "ddtrace-3.12.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8cc90fdcd7f021d06383b88c0e40726706c06088dddd528e31cf3c65a9fea9"},
{file = "ddtrace-3.12.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:585b7b26f03c64390c800e180304639b4226c34c533f16bc6cd9c328ee4f727a"},
{file = "ddtrace-3.12.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe967af58f2e0033caa977c512a4bfb7af3c6f5ad57e9bdef9241609a4d8a99b"},
{file = "ddtrace-3.12.4-cp312-cp312-win32.whl", hash = "sha256:fe03b8f513513e28c35bc792cd7ef0602b21cbcfe71d17a2dd962aee23e980d9"},
{file = "ddtrace-3.12.4-cp312-cp312-win_amd64.whl", hash = "sha256:9fd79c44ecffb36ac5b3168f0f196778ed0dd538beb07961ce10e06b8045af35"},
{file = "ddtrace-3.12.4-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:2edf755f4bfd823ce8b560c233cb17137ef79d097bc1ade7914f684b39011bcb"},
{file = "ddtrace-3.12.4-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:6dad7ca193810beb931e81b7430dd074a53bf8f8bd5bdc19acd198d460b2438a"},
{file = "ddtrace-3.12.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de7aa6b6ea3d41f8f20c5e00dd85b2f2b3bb1591f3b7deab5d4c527620c3cb3"},
{file = "ddtrace-3.12.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80e0acbbe85365f113bf6e57f77a82f0e0612a7a4cb57f16e9e184748a2bc478"},
{file = "ddtrace-3.12.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46de7dd48256d8e347f2ab436644bd8946d3605caedb150eb46327a9f5b005b6"},
{file = "ddtrace-3.12.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d5c9ddacecb0072292360813b453129998ca293e13c542fa51771c7734ef03a"},
{file = "ddtrace-3.12.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d0b694838e6c7ea2da6de7ccd7b866ec439c49fa40b68ac46f657163cb571d93"},
{file = "ddtrace-3.12.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e89a17cdb4b5442b97a219e8522b9c665cf7a5116f7e97049dd145f837bad5b1"},
{file = "ddtrace-3.12.4-cp313-cp313-win32.whl", hash = "sha256:d0b3ec8228950e7ff68c39537630cd12880656d96461ef021d6484b2df8dba84"},
{file = "ddtrace-3.12.4-cp313-cp313-win_amd64.whl", hash = "sha256:fad78414731b242e86016a124299f2f41575ccf58444edca777b425dbd9faf0c"},
{file = "ddtrace-3.12.4-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:9f639f70f1689ec1a1049cd64132491ee09bcfe7609d73f8c220e38261611045"},
{file = "ddtrace-3.12.4-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:6b5b150e9d362f7242159dd5a5a7107f1be091282c0ee69301fb7ede60f28d3c"},
{file = "ddtrace-3.12.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda3b6ebd275f7f7272f45f4e8ee0e0720c1e217c80140270f8c5e415e11133e"},
{file = "ddtrace-3.12.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe644904b44d39a93eb40fb033aef26a03e4096d135ee844b71ed49d1bd647ad"},
{file = "ddtrace-3.12.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62a48fc36308919afb1fae22a268a96cff3448f1feb860db97d130498ddfa428"},
{file = "ddtrace-3.12.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:77de49365f55033d7e14b544f92d0cae71969b78c4ab8642c3340124e0200739"},
{file = "ddtrace-3.12.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:87fbd5126f8339bcb508a52455f58b0c92870a1c3748849a4d6543198b5f8752"},
{file = "ddtrace-3.12.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5845d7c2ed46b44e02bd5d36ca7f8e80a4e942683473c867393b9fd4553f9d64"},
{file = "ddtrace-3.12.4-cp38-cp38-win32.whl", hash = "sha256:ebde5af8c5d98f435d7dec960c97151142a4b302e94c20da79ed58fe8a08052e"},
{file = "ddtrace-3.12.4-cp38-cp38-win_amd64.whl", hash = "sha256:18dfe9a1a02bfa4ef4f614122135509f454abeff625039b764bc461462ba0923"},
{file = "ddtrace-3.12.4-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e78957120c64bd56ce5592bc10587d7c0d1ca68f21f5b46f6a18dafbc43ad234"},
{file = "ddtrace-3.12.4-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:3936243dc989b8e8e3bb004262abe68a1cc3e0b9356671c01233b84d2c837903"},
{file = "ddtrace-3.12.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed76d10787fc288ea94808ce601df243fc3953c7142baefac446015bed799790"},
{file = "ddtrace-3.12.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c1d3f7f93146653f8ed06d8cd54030b2c902ceca6de55f6df7f40d23037181e"},
{file = "ddtrace-3.12.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5ab24c82fc7532386b02530f90fed2964718cea296adf6d35fc31bd30d301d"},
{file = "ddtrace-3.12.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30bd9e57923a99d5b4e6562976e9f7307d685caff1544b3d2f7438e6ef8e87e8"},
{file = "ddtrace-3.12.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3bf18fd5898940fb7f236b4c9796f0ee517eb755fd0c17965d3a0342f865ee5a"},
{file = "ddtrace-3.12.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8ff1c70da37c05a29f0be091b0fdc6bb1d91d448f56861c51df614649441070c"},
{file = "ddtrace-3.12.4-cp39-cp39-win32.whl", hash = "sha256:66c007170698e3d12638d03e80f02e93c3bb3e55e96a7f5517e638056562ec1a"},
{file = "ddtrace-3.12.4-cp39-cp39-win_amd64.whl", hash = "sha256:a4f2dabbc95e5c6bf4c43eb141e94021789c81a929588f4000f876f89882c124"},
{file = "ddtrace-3.12.4.tar.gz", hash = "sha256:c422977fc4f6e9ba7d4eef9b7e6ce00f8b81c68b034682c6a63eb5c9670e37d8"},
{file = "ddtrace-3.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:12122a8e7089ab40cad2cd6bb51834859aa0a27babf3256a73630e6ee2315455"},
{file = "ddtrace-3.13.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:02fab2c444b87f290850b3d750e17ccdf49ace3baf8ff3305e8147f6fdf0dc50"},
{file = "ddtrace-3.13.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a003ffa4649dab4971d3557ce2d85eb2c5d335ebc7152196cbf780171fd4b5e1"},
{file = "ddtrace-3.13.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:52b2458b6f0f4725156d46c6cb5410f98568a61cc890bb270515c9caad3a522d"},
{file = "ddtrace-3.13.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:9160222e476e18af95ef687bd548f8e86b3815896bf7cd1d42a9b43005e058e2"},
{file = "ddtrace-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:464e245c2114c722ad4240b73b1c598f83cc1c7bdc9001aec3083f914c1cacc0"},
{file = "ddtrace-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21901a58e938dbeba0ca6c49b8ba1480d07eea5b057845ae4ff3a706d833137f"},
{file = "ddtrace-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:40e00faced483a3eac0b499cf191a38fbf8bb060a3872029ee3299871f87bdd9"},
{file = "ddtrace-3.13.0-cp310-cp310-win32.whl", hash = "sha256:d15593cb804d74094df1a71167a70136b7616579259ce2b26279f2762354e709"},
{file = "ddtrace-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:5de44e7c595d25745665fa1cc44c0f0b4c7ad79be06d0de74f6e0edb2c8ec351"},
{file = "ddtrace-3.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:68c38ac75cc3668e9284873f5e84c3e104880d68c3891ed13614e0614c46f5b0"},
{file = "ddtrace-3.13.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:6d8811c4b7397384aff7e54b7399647f4c1c0e9167792cb45adb2d3553fc20a2"},
{file = "ddtrace-3.13.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029b6e6c50984b1976c6b0970e60184919dab9514441d08683a50a5d52a05326"},
{file = "ddtrace-3.13.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8de2a060400ee89422ecfd3269dfd2e113f4f9dae00f6fcd3ed9e53e2223a26a"},
{file = "ddtrace-3.13.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:bb0738048ea0e49e6bec9be2bf5c68a24d7ea3b27bf956147378366aacb4ca4b"},
{file = "ddtrace-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:04cf4776c52cfb19914bf6e84242d110197d15426c34e45b14fa63d9085767d5"},
{file = "ddtrace-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c32774e90593ebb264d53d6523b71243b9ba794ae5689e38ad522afddd06c0b"},
{file = "ddtrace-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a01f99b0287c2bbd8b305e0cb54b382eaf2a0fe89ba82f2f68fcbdd9fed040cd"},
{file = "ddtrace-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b37efa3e7b487bd60e6fb89186d98c1ad1727871074f3519c9ca92feea7e5cd0"},
{file = "ddtrace-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:112e4d96f02f94247528b65f046c69d360d6eca75b9e7cd2f95fde1c14e2002e"},
{file = "ddtrace-3.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:13ac5bc306df5719d00a8b1f6925efbb9dd0ba5e121edcc2acfef24c57b3deb5"},
{file = "ddtrace-3.13.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b3bdfc3cabab85f91a4f24264a2d0f6f74984a5b5994c62072c6e3b5e05320f3"},
{file = "ddtrace-3.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:11b10f8dfadb4b1372aee820be6c22071138ede2ddb32f73486255d5879b283f"},
{file = "ddtrace-3.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3d68007602797f280c971a286c3f05bdff66c12a68a3e0bd67cb5bbc1c4a67a"},
{file = "ddtrace-3.13.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:abd00a5b83d85a951dd976a59c8673bedacdc1ea9e6acb8e72545f73bddc7879"},
{file = "ddtrace-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5dbe392b2182e6dd617e946cf41da7e3207387b912809ebe8338b794b08750b2"},
{file = "ddtrace-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6b38b4ad9e3f1b3421022587748f6a687ed722eae16033392fc875b5c67d6c5a"},
{file = "ddtrace-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f38a1545495c8db3318621400a3d407db457e3550a397e39cf883f41919e1dc8"},
{file = "ddtrace-3.13.0-cp312-cp312-win32.whl", hash = "sha256:e01bb1b305b777001d310911bd73d1fd88c9c212258caaf65f1422a0dbef1a3b"},
{file = "ddtrace-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8dbb9aa23a36599754932e79df28eb07fdd3aaca515297bf58dfcdac608273da"},
{file = "ddtrace-3.13.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:397a68e476d8bd9aa14f8c097bc9014510948e76a0110842ab6f5fa1143ad153"},
{file = "ddtrace-3.13.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:fab1b06476169e2cf6a098130c44eeb3d9d8205b5a91ae8afdb7d2b4d2d0b0be"},
{file = "ddtrace-3.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:653f75c3e838366108464f9555120f61ef0589974f346ed2c2c9cb3001d3fc6a"},
{file = "ddtrace-3.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80f694c3d3984c9bd3bd7818268be7ece02071c67671c6d8c815e6888ae4e78c"},
{file = "ddtrace-3.13.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:be16f9c0583767db13403e78ac7ac7b4c103e8b7eaac6deef7c897408f24b940"},
{file = "ddtrace-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5490a715fbb70ee03840c6a3146c76d7bfa27d5b679ce4c1a7b368eff7dee9f"},
{file = "ddtrace-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:45235a81c828e2d6bdb4ac1bbe55582c190bc27e8820eeae5c0478ea11f1ed81"},
{file = "ddtrace-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a9374a8cf405169a9eab7791cc94d5dc5753eefe806b5bee9909eef3d5e339d"},
{file = "ddtrace-3.13.0-cp313-cp313-win32.whl", hash = "sha256:6bc1648a1c046e6061e29d94d2003c17820cc3a7f1c24322dab654abe9bb30db"},
{file = "ddtrace-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8823e95f69dd3fc8a884d092fdc54a3c3078daf0f90e824fceda7e0f26acbc70"},
{file = "ddtrace-3.13.0-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:338932a8511a815d5198ec09d55f6850fcb9c679a1b50a3a28fdc0ff99bd800a"},
{file = "ddtrace-3.13.0-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:c14fe68cfc1c11b9d560a3026e3e5dcdd59b725b6ce79cda66d23a26b37751e6"},
{file = "ddtrace-3.13.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fd70631f5c70ccafde14df98a9f807e537222f13d6f03fa08bf1308eaf89301"},
{file = "ddtrace-3.13.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09c71f464afb05d7f1a2758112f4feaf2bca39daa22a6c3f75999227eb40e2ec"},
{file = "ddtrace-3.13.0-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:481b13365e3cf100bf35f305bd0680695fa369e67a9ec4e1b41788df62ac1d0b"},
{file = "ddtrace-3.13.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d99ebbef96f406e0436bd21a92354c3c338fc6a8fe85d0a26fe942bc563b721"},
{file = "ddtrace-3.13.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:28086003f1c5ce3e84239eea9d624afcc386b38f2115c3438ea49beff84ff861"},
{file = "ddtrace-3.13.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f280e80560f5c953bb16b168bed1b6f7d527ef98f81860422500040ee57a7aba"},
{file = "ddtrace-3.13.0-cp38-cp38-win32.whl", hash = "sha256:82f0b76c83e368c686594f42809d727143ee89a879d1a76cde9f75d4cea07cb4"},
{file = "ddtrace-3.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:dd7b3a9933b11b2fce4dd4cb34ee465bc3c87024444a2e6a5a653f424bae8e37"},
{file = "ddtrace-3.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:c1ce2123615e4618050ec7fc96e296283f23c45eddcf3a2fe94386f7513795a4"},
{file = "ddtrace-3.13.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:9dae3459edd5cc7a1124596b524b743b1d2bddf4155ca9679c599740ad71546d"},
{file = "ddtrace-3.13.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d36d0cf84a39b29f88dcb06a20fc3f2c7a9eca8eb1fd5d15bc5a51de095962c"},
{file = "ddtrace-3.13.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4a55277a3db32fee06030fd0dbf77c2e867541c3e4b65e68e46b03971401173"},
{file = "ddtrace-3.13.0-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:cb97593d9739f0be6647e19edc6fc6998dfba3e78fb9d2df5fef9ebfb117aa85"},
{file = "ddtrace-3.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f905e5bb2db4c154fca25ded15c3e1d633951db2d6ed2989f630ee3afd589cc0"},
{file = "ddtrace-3.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:de3ecc6428330117ef063ef6a90326669a9a4cf3e766674228ec384edca52bb1"},
{file = "ddtrace-3.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eec340ef5152e971dc6ab075945dfa7c41285f8441bea0a78f5f4cd1f6b9aab6"},
{file = "ddtrace-3.13.0-cp39-cp39-win32.whl", hash = "sha256:8c2831f928393f934bfe9f9b5f0eeb22a0f5c88fbebe32cc5106b24409847d6b"},
{file = "ddtrace-3.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:e04f4c41e7216422e9cd101bee70a823f56dddb8333158e1e72b73332e1a311d"},
{file = "ddtrace-3.13.0.tar.gz", hash = "sha256:d7d3d82795d29cf2385aa692ee5c65e469ebfa34469941055af66eae2eefa374"},
]
[package.dependencies]
@@ -1901,25 +1902,25 @@ files = [
[[package]]
name = "fastapi"
version = "0.116.1"
version = "0.117.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
{file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
{file = "fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552"},
{file = "fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.40.0,<0.48.0"
starlette = ">=0.40.0,<0.49.0"
typing-extensions = ">=4.8.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastjsonschema"
@@ -2291,6 +2292,72 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""]
tqdm = ["tqdm"]
[[package]]
name = "gevent"
version = "25.9.1"
description = "Coroutine-based network library"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"},
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"},
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"},
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"},
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"},
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"},
{file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"},
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"},
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"},
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"},
{file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"},
{file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"},
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"},
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"},
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"},
{file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"},
{file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"},
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"},
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"},
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"},
{file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"},
{file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"},
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"},
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"},
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"},
{file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"},
{file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"},
{file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"},
{file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"},
{file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"},
{file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"},
]
[package.dependencies]
cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""}
"zope.event" = "*"
"zope.interface" = "*"
[package.extras]
dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""]
docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"]
[[package]]
name = "gitdb"
version = "4.0.12"
@@ -2325,27 +2392,6 @@ gitdb = ">=4.0.1,<5"
doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"]
test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""]
[[package]]
name = "google-ai-generativelanguage"
version = "0.6.15"
description = "Google Ai Generativelanguage API client library"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_ai_generativelanguage-0.6.15-py3-none-any.whl", hash = "sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c"},
{file = "google_ai_generativelanguage-0.6.15.tar.gz", hash = "sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3"},
]
[package.dependencies]
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
[[package]]
name = "google-api-core"
version = "2.25.1"
@@ -2684,30 +2730,6 @@ websockets = ">=13.0.0,<15.1.0"
[package.extras]
aiohttp = ["aiohttp (<4.0.0)"]
[[package]]
name = "google-generativeai"
version = "0.8.5"
description = "Google Generative AI High level API client library and tools."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "google_generativeai-0.8.5-py3-none-any.whl", hash = "sha256:22b420817fb263f8ed520b33285f45976d5b21e904da32b80d4fd20c055123a2"},
]
[package.dependencies]
google-ai-generativelanguage = "0.6.15"
google-api-core = "*"
google-api-python-client = "*"
google-auth = ">=2.15.0"
protobuf = "*"
pydantic = "*"
tqdm = "*"
typing-extensions = "*"
[package.extras]
dev = ["Pillow", "absl-py", "black", "ipython", "nose2", "pandas", "pytype", "pyyaml"]
[[package]]
name = "google-resumable-media"
version = "2.7.2"
@@ -2752,7 +2774,7 @@ version = "3.2.4"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.9"
groups = ["main"]
groups = ["main", "test"]
files = [
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
@@ -2809,6 +2831,7 @@ files = [
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
]
markers = {test = "platform_python_implementation == \"CPython\""}
[package.extras]
docs = ["Sphinx", "furo"]
@@ -5408,7 +5431,7 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]]
name = "openhands-ai"
version = "0.55.0"
version = "0.57.0"
description = "OpenHands: Code Less, Make More"
optional = false
python-versions = "^3.12,<3.14"
@@ -5432,7 +5455,7 @@ google-api-python-client = "^2.164.0"
google-auth-httplib2 = "*"
google-auth-oauthlib = "*"
google-cloud-aiplatform = "*"
google-generativeai = "*"
google-genai = "*"
html2text = "*"
httpx-aiohttp = "^0.1.8"
ipywidgets = "^8.1.5"
@@ -5442,7 +5465,7 @@ json-repair = "*"
jupyter_kernel_gateway = "*"
kubernetes = "^33.1.0"
libtmux = ">=0.37,<0.40"
litellm = "^1.74.3, !=1.64.4, !=1.67.*"
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*"
memory-profiler = "^0.61.0"
numpy = "*"
openai = "1.99.9"
@@ -5451,6 +5474,7 @@ opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1"
pexpect = "*"
pillow = "^11.3.0"
poetry = "^2.1.2"
prompt-toolkit = "^3.0.50"
protobuf = "^5.0.0,<6.0.0"
@@ -5458,6 +5482,7 @@ psutil = "*"
pygithub = "^2.5.0"
pyjwt = "^2.9.0"
pylatexenc = "*"
pypdf = "^6.0.0"
PyPDF2 = "*"
python-docx = "*"
python-dotenv = "*"
@@ -5471,19 +5496,23 @@ pyyaml = "^6.0.2"
qtconsole = "^5.6.1"
rapidfuzz = "^3.9.0"
redis = ">=5.2,<7.0"
requests = "^2.32.5"
setuptools = ">=78.1.1"
shellingham = "^1.5.4"
sse-starlette = "^2.1.3"
sse-starlette = "^3.0.2"
starlette = "^0.48.0"
tenacity = ">=8.5,<10.0"
termcolor = "*"
toml = "*"
tornado = "*"
types-toml = "*"
urllib3 = "^2.5.0"
uvicorn = "*"
whatthepatch = "^1.0.6"
zope-interface = "7.2"
[package.extras]
third-party-runtimes = ["daytona (==0.24.2)", "e2b (>=1.0.5,<1.8.0)", "modal (>=0.66.26,<1.2.0)", "runloop-api-client (==0.50.0)"]
third-party-runtimes = ["daytona (==0.24.2)", "e2b-code-interpreter (>=2.0.0,<3.0.0)", "modal (>=0.66.26,<1.2.0)", "runloop-api-client (==0.50.0)"]
[package.source]
type = "directory"
@@ -6516,11 +6545,12 @@ version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "test"]
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
]
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
[[package]]
name = "pydantic"
@@ -8310,7 +8340,7 @@ version = "80.9.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
groups = ["main"]
groups = ["main", "test"]
files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
@@ -8676,14 +8706,14 @@ sqlcipher = ["sqlcipher3_binary"]
[[package]]
name = "sse-starlette"
version = "2.4.1"
version = "3.0.2"
description = "SSE plugin for Starlette"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a"},
{file = "sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926"},
{file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"},
{file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"},
]
[package.dependencies]
@@ -8691,7 +8721,7 @@ anyio = ">=4.7.0"
[package.extras]
daphne = ["daphne (>=4.2.0)"]
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio,examples] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
granian = ["granian (>=2.3.1)"]
uvicorn = ["uvicorn (>=0.34.0)"]
@@ -8747,14 +8777,14 @@ files = [
[[package]]
name = "starlette"
version = "0.47.3"
version = "0.48.0"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"},
{file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"},
{file = "starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659"},
{file = "starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46"},
]
[package.dependencies]
@@ -9883,13 +9913,32 @@ enabler = ["pytest-enabler (>=2.2)"]
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
type = ["pytest-mypy"]
[[package]]
name = "zope-event"
version = "6.0"
description = "Very basic event publishing system"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3"},
{file = "zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92"},
]
[package.dependencies]
setuptools = ">=75.8.2"
[package.extras]
docs = ["Sphinx"]
test = ["zope.testrunner (>=6.4)"]
[[package]]
name = "zope-interface"
version = "7.2"
description = "Interfaces for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
groups = ["main", "test"]
files = [
{file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
{file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
@@ -10053,4 +10102,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "0e611931bd3823ee8b6d832b6ef444868a644e21927a9fb72d4aeaab8170028e"
content-hash = "8c460070dce6bdec5ee0ee7bc0c2246fcf2602d1e64a0867b4f5e3a0e334fe93"

View File

@@ -37,7 +37,7 @@ sqlalchemy = { extras = [ "asyncio" ], version = "^2.0.40" }
resend = "^2.7.0"
tenacity = "^9.1.2"
slack-sdk = "^3.35.0"
ddtrace = "^3.5.1"
ddtrace = "3.13.0" #pin to avoid yanked version 3.12.4
posthog = "^4.2.0"
limits = "^5.2.0"
coredis = "^4.22.0"
@@ -63,6 +63,7 @@ openai = "*"
opencv-python = "*"
pandas = "*"
reportlab = "*"
gevent = ">=24.2.1,<26.0.0"
[tool.poetry-dynamic-versioning]
enable = true
@@ -85,3 +86,7 @@ lint.pydocstyle.convention = "google"
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
[tool.coverage.run]
relative_files = true
omit = [ "tests/*" ]

View File

@@ -227,12 +227,23 @@ class SaasUserAuth(UserAuth):
def get_api_key_from_header(request: Request):
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
return auth_header.replace('Bearer ', '')
api_key = auth_header.replace('Bearer ', '')
logger.info(
f'[TOKEN_DEBUG] Got API key from Authorization header: '
f'key_preview={api_key[:10] if api_key else "None"}...'
)
return api_key
# This is a temp hack
# Streamable HTTP MCP Client works via redirect requests, but drops the Authorization header for reason
# We include `X-Session-API-Key` header by default due to nested runtimes, so it used as a drop in replacement here
return request.headers.get('X-Session-API-Key')
session_api_key = request.headers.get('X-Session-API-Key')
if session_api_key:
logger.info(
f'[TOKEN_DEBUG] Got API key from X-Session-API-Key header: '
f'key_preview={session_api_key[:10] if session_api_key else "None"}...'
)
return session_api_key
async def saas_user_auth_from_bearer(request: Request) -> SaasUserAuth | None:
@@ -259,7 +270,11 @@ async def saas_user_auth_from_cookie(request: Request) -> SaasUserAuth | None:
try:
signed_token = request.cookies.get('keycloak_auth')
if not signed_token:
logger.info('[TOKEN_DEBUG] No keycloak_auth cookie found in request')
return None
logger.info(
f'[TOKEN_DEBUG] Found keycloak_auth cookie, size={len(signed_token)}'
)
return await saas_user_auth_from_signed_token(signed_token)
except Exception as exc:
raise CookieError from exc
@@ -272,6 +287,10 @@ async def saas_user_auth_from_signed_token(signed_token: str) -> SaasUserAuth:
logger.debug('saas_user_auth_from_signed_token:decoded')
access_token = decoded['access_token']
refresh_token = decoded['refresh_token']
logger.info(
f'[TOKEN_DEBUG] Cookie tokens: '
f'refresh_token_preview={refresh_token[:20] if refresh_token else "None"}...'
)
logger.debug(
'saas_user_auth_from_signed_token',
extra={
@@ -287,6 +306,35 @@ async def saas_user_auth_from_signed_token(signed_token: str) -> SaasUserAuth:
user_id = access_token_payload['sub']
email = access_token_payload['email']
email_verified = access_token_payload['email_verified']
# Check if we have an offline token in the database
logger.info(f'[TOKEN_DEBUG] Checking for offline token for user {user_id}')
try:
offline_token = await token_manager.load_offline_token(user_id)
if offline_token:
# Compare tokens definitively
tokens_match = offline_token == refresh_token
logger.info(
f'[TOKEN_DEBUG] Token comparison: '
f'TOKENS_ARE_{"SAME" if tokens_match else "DIFFERENT"}! '
f'Cookie len={len(refresh_token) if refresh_token else 0}, '
f'DB len={len(offline_token) if offline_token else 0}'
)
if not tokens_match:
# Log first 50 chars for better comparison
logger.info(
f'[TOKEN_DEBUG] Cookie token: {refresh_token[:50] if refresh_token else "None"}...'
)
logger.info(
f'[TOKEN_DEBUG] DB offline token: {offline_token[:50] if offline_token else "None"}...'
)
# TODO: Consider using offline_token instead of refresh_token
# refresh_token = offline_token
else:
logger.info('[TOKEN_DEBUG] No offline token in DB, using cookie token')
except Exception as e:
logger.error(f'[TOKEN_DEBUG] Error loading offline token: {e}')
logger.debug('saas_user_auth_from_signed_token:return')
return SaasUserAuth(
@@ -304,6 +352,11 @@ async def get_user_auth_from_keycloak_id(keycloak_user_id: str) -> UserAuth:
offline_token = await token_manager.load_offline_token(keycloak_user_id)
if offline_token is None:
logger.info('no_offline_token_found')
else:
logger.info(
f'[TOKEN_DEBUG] Using offline token from DB for user {keycloak_user_id}: '
f'{offline_token[:20] if offline_token else "None"}...'
)
user_auth = SaasUserAuth(
user_id=keycloak_user_id,

View File

@@ -266,6 +266,10 @@ class TokenManager:
user_id = user_info.get('sub')
username = user_info.get('preferred_username')
logger.info(f'Getting token for user {username} and IDP {idp}')
logger.info(
'[TOKEN_SOURCE_DEBUG] get_idp_token called with access_token '
'(from cookie/request), will check DB for stored tokens'
)
token_store = await AuthTokenStore.get_instance(
keycloak_user_id=user_id, idp=idp
)
@@ -275,9 +279,7 @@ class TokenManager:
self._check_expiration_and_refresh
)
if not token_info:
logger.error(
f'No tokens for user: {username}, identity provider: {idp}'
)
logger.info(f'No tokens for user: {username}, identity provider: {idp}')
raise ValueError(
f'No tokens for user: {username}, identity provider: {idp}'
)
@@ -441,8 +443,16 @@ class TokenManager:
0 if refresh_expires_in == 0 else current_time + refresh_expires_in
)
# Log detailed expiration info for debugging
access_expires_hours = expires_in / 3600 if expires_in > 0 else 'Never'
refresh_expires_days = (
refresh_expires_in / 86400 if refresh_expires_in > 0 else 'Never'
)
logger.info(
f'Token refresh successful. New access token expires at: {access_token_expires_at}, refresh token expires at: {refresh_token_expires_at}'
f'[TOKEN_DEBUG] Token refresh successful. Access token expires in: {access_expires_hours} hours, '
f'Refresh token expires in: {refresh_expires_days} days. '
f'Raw values - expires_in: {expires_in}s, refresh_expires_in: {refresh_expires_in}s'
)
return {
'access_token': access_token,
@@ -459,15 +469,32 @@ class TokenManager:
async def get_idp_token_from_offline_token(
self, offline_token: str, idp: ProviderType
) -> str:
logger.info('Getting IDP token from offline token')
logger.info(
f'[TOKEN_DEBUG] Getting {idp} token from offline token. '
f'Token preview: {offline_token[:20] if offline_token else "None"}...'
)
logger.info(
f'[TOKEN_SOURCE_DEBUG] Using OFFLINE token (from DB) to refresh {idp} token, '
f'token_length={len(offline_token) if offline_token else 0}'
)
try:
logger.info('[TOKEN_DEBUG] Calling Keycloak to refresh offline token...')
tokens = await get_keycloak_openid(self.external).a_refresh_token(
offline_token
)
logger.info('[TOKEN_DEBUG] Keycloak refresh successful!')
return await self.get_idp_token(tokens['access_token'], idp)
except KeycloakConnectionError:
logger.exception('KeycloakConnectionError when refreshing token')
except KeycloakConnectionError as e:
logger.error(
f'[TOKEN_DEBUG] KeycloakConnectionError when refreshing token: {e}'
)
raise
except Exception as e:
logger.error(
f'[TOKEN_DEBUG] Unexpected error refreshing Keycloak token: '
f'{type(e).__name__}: {e}'
)
raise
@retry(

View File

@@ -8,6 +8,7 @@ from server.clustered_conversation_manager import ClusteredConversationManager
from server.saas_nested_conversation_manager import SaasNestedConversationManager
from openhands.core.config import LLMConfig, OpenHandsConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import MessageAction
from openhands.server.config.server_config import ServerConfig
from openhands.server.conversation_manager.conversation_manager import (
@@ -33,8 +34,8 @@ class LegacyCacheEntry:
@dataclass
class LegacyConversationManager(ConversationManager):
"""
Conversation manager for use while migrating - since existing conversations are not nested!
"""Conversation manager for use while migrating - since existing conversations are not nested.
Separate class from SaasNestedConversationManager so it can be easliy removed in a few weeks.
(As of 2025-07-23)
"""
@@ -187,10 +188,33 @@ class LegacyConversationManager(ConversationManager):
initial_user_msg: MessageAction | None = None,
replay_json: str | None = None,
) -> AgentLoopInfo:
try:
has_tokens = bool(
settings
and hasattr(settings, 'provider_tokens')
and settings.provider_tokens
)
logger.info(
f'[TOKEN_DEBUG] LegacyManager.maybe_start_agent_loop ENTRY: '
f'sid={sid}, user_id={user_id}, has_provider_tokens={has_tokens}'
)
except Exception as e:
logger.error(f'[TOKEN_DEBUG] Error logging entry: {e}')
logger.info(
f'[TOKEN_DEBUG] LegacyManager.maybe_start_agent_loop ENTRY: sid={sid}, user_id={user_id}'
)
if await self.should_start_in_legacy_mode(sid):
logger.info(
f'[TOKEN_DEBUG] LegacyManager: Routing {sid} to ClusteredConversationManager (legacy mode)'
)
return await self.legacy_conversation_manager.maybe_start_agent_loop(
sid, settings, user_id, initial_user_msg, replay_json
)
logger.info(
f'[TOKEN_DEBUG] LegacyManager: Routing {sid} to SaasNestedConversationManager (new mode)'
)
return await self.conversation_manager.maybe_start_agent_loop(
sid, settings, user_id, initial_user_msg, replay_json
)
@@ -270,8 +294,8 @@ class LegacyConversationManager(ConversationManager):
del self._legacy_cache[key]
async def should_start_in_legacy_mode(self, conversation_id: str) -> bool:
"""
Check if a conversation should run in legacy mode by directly checking the runtime.
"""Check if a conversation should run in legacy mode by directly checking the runtime.
The /list method does not include stopped conversations even though the PVC for these
may not yet have been deleted, so we need to check /sessions/{session_id} directly.
"""
@@ -283,11 +307,32 @@ class LegacyConversationManager(ConversationManager):
cached_entry = self._legacy_cache[conversation_id]
# Check if the cached value is still valid
if time.time() - cached_entry.timestamp <= _LEGACY_ENTRY_TIMEOUT_SECONDS:
logger.info(
f'[TOKEN_DEBUG] LegacyManager: Using cached legacy status for {conversation_id}: '
f'is_legacy={cached_entry.is_legacy}'
)
return cached_entry.is_legacy
# If not in cache or expired, check the runtime directly
runtime = await self.conversation_manager._get_runtime(conversation_id)
# Log runtime details for debugging
if runtime:
logger.info(
f"[TOKEN_DEBUG] LegacyManager: Runtime check for {conversation_id}: "
f"status={runtime.get('status')}, has_command={bool(runtime.get('command'))}, "
f"command_preview={str(runtime.get('command', ''))[:100]}"
)
else:
logger.info(
f'[TOKEN_DEBUG] LegacyManager: No runtime found for {conversation_id}'
)
is_legacy = self.is_legacy_runtime(runtime)
logger.info(
f"[TOKEN_DEBUG] LegacyManager: Determined legacy status for {conversation_id}: "
f"is_legacy={is_legacy}, will use {'ClusteredConversationManager' if is_legacy else 'SaasNestedConversationManager'}"
)
# Cache the result with current timestamp
self._legacy_cache[conversation_id] = LegacyCacheEntry(is_legacy, time.time())
@@ -295,8 +340,7 @@ class LegacyConversationManager(ConversationManager):
return is_legacy
def is_legacy_runtime(self, runtime: dict | None) -> bool:
"""
Determine if a runtime is a legacy runtime based on its command.
"""Determine if a runtime is a legacy runtime based on its command.
Args:
runtime: The runtime dictionary or None if not found
@@ -304,9 +348,25 @@ class LegacyConversationManager(ConversationManager):
Returns:
bool: True if this is a legacy runtime, False otherwise
"""
if runtime is None:
# Ensure runtime is actually a dict (not None, mock, or other object)
if not isinstance(runtime, dict):
return False
return 'openhands.server' not in runtime['command']
# Handle case where command field might not exist (e.g., paused runtimes)
command = runtime.get('command', '')
if not command:
# If no command field, check if this is a paused runtime
# Paused runtimes should use the new conversation manager
if runtime.get('status', '').lower() == 'paused':
return False
# Unknown state - default to False (use new manager)
return False
# Ensure command is a string before checking substring
if not isinstance(command, str):
return False
return 'openhands.server' not in command
@classmethod
def get_instance(

View File

@@ -417,12 +417,35 @@ async def refresh_tokens(
x_session_api_key: Annotated[str | None, Header(alias='X-Session-API-Key')],
) -> TokenResponse:
"""Return the latest token for a given provider."""
logger.info(
f'[TOKEN_DEBUG] /api/refresh-tokens called: provider={provider}, sid={sid}, '
f'has_session_key={bool(x_session_api_key)}'
)
user_id = _get_user_id(sid)
logger.info(
f'[TOKEN_DEBUG] Got user_id: {user_id[:8]}...' if user_id else 'No user_id'
)
session_api_key = await _get_session_api_key(user_id, sid)
logger.info(
f'[TOKEN_DEBUG] Session key validation: '
f'expected={session_api_key[:8] if session_api_key else None}..., '
f'received={x_session_api_key[:8] if x_session_api_key else None}..., '
f'match={session_api_key == x_session_api_key}'
)
if session_api_key != x_session_api_key:
logger.error(
f'[TOKEN_DEBUG] Session key mismatch! Returning 403. '
f'Expected: {session_api_key[:8] if session_api_key else "None"}..., '
f'Got: {x_session_api_key[:8] if x_session_api_key else "None"}...'
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Forbidden')
logger.info(f'Refreshing token for conversation {sid}')
logger.info(
f'[TOKEN_DEBUG] Session validated. Refreshing {provider} token for {sid}'
)
provider_handler = ProviderHandler(
create_provider_tokens_object([provider]), external_auth_id=user_id
)

View File

@@ -17,11 +17,13 @@ from server.constants import (
STRIPE_API_KEY,
STRIPE_WEBHOOK_SECRET,
SUBSCRIPTION_PRICE_DATA,
get_default_litellm_model,
)
from server.logger import logger
from storage.billing_session import BillingSession
from storage.database import session_maker
from storage.subscription_access import SubscriptionAccess
from storage.user_settings import UserSettings
from openhands.server.user_auth import get_user_id
@@ -42,6 +44,8 @@ class SubscriptionAccessResponse(BaseModel):
start_at: datetime
end_at: datetime
created_at: datetime
cancelled_at: datetime | None = None
stripe_subscription_id: str | None = None
class CreateCheckoutSessionRequest(BaseModel):
@@ -85,7 +89,7 @@ async def get_credits(user_id: str = Depends(get_user_id)) -> GetCreditsResponse
async def get_subscription_access(
user_id: str = Depends(get_user_id),
) -> SubscriptionAccessResponse | None:
"""Get details of the currently valid subscription for the user"""
"""Get details of the currently valid subscription for the user."""
with session_maker() as session:
now = datetime.now(UTC)
subscription_access = (
@@ -102,6 +106,8 @@ async def get_subscription_access(
start_at=subscription_access.start_at,
end_at=subscription_access.end_at,
created_at=subscription_access.created_at,
cancelled_at=subscription_access.cancelled_at,
stripe_subscription_id=subscription_access.stripe_subscription_id,
)
@@ -113,6 +119,78 @@ async def has_payment_method(user_id: str = Depends(get_user_id)) -> bool:
return await stripe_service.has_payment_method(user_id)
# Endpoint to cancel user's subscription
@billing_router.post('/cancel-subscription')
async def cancel_subscription(user_id: str = Depends(get_user_id)) -> JSONResponse:
"""Cancel user's active subscription at the end of the current billing period."""
if not user_id:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
with session_maker() as session:
# Find the user's active subscription
now = datetime.now(UTC)
subscription_access = (
session.query(SubscriptionAccess)
.filter(SubscriptionAccess.status == 'ACTIVE')
.filter(SubscriptionAccess.user_id == user_id)
.filter(SubscriptionAccess.start_at <= now)
.filter(SubscriptionAccess.end_at >= now)
.filter(SubscriptionAccess.cancelled_at.is_(None)) # Not already cancelled
.first()
)
if not subscription_access:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='No active subscription found',
)
if not subscription_access.stripe_subscription_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Cannot cancel subscription: missing Stripe subscription ID',
)
try:
# Cancel the subscription in Stripe at period end
await stripe.Subscription.modify_async(
subscription_access.stripe_subscription_id, cancel_at_period_end=True
)
# Update local database
subscription_access.cancelled_at = datetime.now(UTC)
session.merge(subscription_access)
session.commit()
logger.info(
'subscription_cancelled',
extra={
'user_id': user_id,
'stripe_subscription_id': subscription_access.stripe_subscription_id,
'subscription_access_id': subscription_access.id,
'end_at': subscription_access.end_at,
},
)
return JSONResponse(
{'status': 'success', 'message': 'Subscription cancelled successfully'}
)
except stripe.StripeError as e:
logger.error(
'stripe_cancellation_failed',
extra={
'user_id': user_id,
'stripe_subscription_id': subscription_access.stripe_subscription_id,
'error': str(e),
},
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f'Failed to cancel subscription: {str(e)}',
)
# Endpoint to create a new setup intent in stripe
@billing_router.post('/create-customer-setup-session')
async def create_customer_setup_session(
@@ -190,9 +268,27 @@ async def create_subscription_checkout_session(
billing_session_type: BillingSessionType = BillingSessionType.MONTHLY_SUBSCRIPTION,
user_id: str = Depends(get_user_id),
) -> CreateBillingSessionResponse:
# Prevent duplicate subscriptions for the same user
with session_maker() as session:
now = datetime.now(UTC)
existing_active_subscription = (
session.query(SubscriptionAccess)
.filter(SubscriptionAccess.status == 'ACTIVE')
.filter(SubscriptionAccess.user_id == user_id)
.filter(SubscriptionAccess.start_at <= now)
.filter(SubscriptionAccess.end_at >= now)
.filter(SubscriptionAccess.cancelled_at.is_(None)) # Not cancelled
.first()
)
if existing_active_subscription:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Cannot create subscription: User already has an active subscription that has not been cancelled',
)
customer_id = await stripe_service.find_or_create_customer(user_id)
subscription_price_data = SUBSCRIPTION_PRICE_DATA[billing_session_type.value]
# TODO: Prevent duplicate subscriptions for the same user
checkout_session = await stripe.checkout.Session.create_async(
customer=customer_id,
line_items=[
@@ -246,7 +342,7 @@ async def create_subscription_checkout_session_via_get(
billing_session_type: BillingSessionType = BillingSessionType.MONTHLY_SUBSCRIPTION,
user_id: str = Depends(get_user_id),
) -> RedirectResponse:
"""Create a subscription checkout session using a GET request (For easier copy / paste to URL bar)"""
"""Create a subscription checkout session using a GET request (For easier copy / paste to URL bar)."""
response = await create_subscription_checkout_session(
request, billing_session_type, user_id
)
@@ -278,7 +374,7 @@ async def success_callback(session_id: str, request: Request):
!= BillingSessionType.DIRECT_PAYMENT.value
):
return RedirectResponse(
f'{request.base_url}settings/billing?checkout=success', status_code=302
f'{request.base_url}settings?checkout=success', status_code=302
)
stripe_session = stripe.checkout.Session.retrieve(session_id)
@@ -348,14 +444,29 @@ async def cancel_callback(session_id: str, request: Request):
session.merge(billing_session)
session.commit()
# Redirect credit purchases to billing screen, subscriptions to LLM settings
if (
billing_session.billing_session_type
== BillingSessionType.DIRECT_PAYMENT.value
):
return RedirectResponse(
f'{request.base_url}settings/billing?checkout=cancel',
status_code=302,
)
else:
return RedirectResponse(
f'{request.base_url}settings?checkout=cancel', status_code=302
)
# If no billing session found, default to LLM settings (subscription flow)
return RedirectResponse(
f'{request.base_url}settings/billing?checkout=cancel', status_code=302
f'{request.base_url}settings?checkout=cancel', status_code=302
)
@billing_router.post('/stripe-webhook')
async def stripe_webhook(request: Request) -> JSONResponse:
"""Endpoint for stripe webhooks"""
"""Endpoint for stripe webhooks."""
payload = await request.body()
sig_header = request.headers.get('stripe-signature')
@@ -397,15 +508,111 @@ async def stripe_webhook(request: Request) -> JSONResponse:
end_at=end_at,
amount_paid=amount_paid,
stripe_invoice_payment_id=invoice.payment_intent,
stripe_subscription_id=invoice.subscription, # Store Stripe subscription ID
)
session.add(subscription_access)
session.commit()
elif event_type == 'customer.subscription.updated':
subscription = event['data']['object']
subscription_id = subscription['id']
# Handle subscription cancellation
if subscription.get('cancel_at_period_end') is True:
with session_maker() as session:
subscription_access = (
session.query(SubscriptionAccess)
.filter(
SubscriptionAccess.stripe_subscription_id == subscription_id
)
.filter(SubscriptionAccess.status == 'ACTIVE')
.first()
)
if subscription_access and not subscription_access.cancelled_at:
subscription_access.cancelled_at = datetime.now(UTC)
session.merge(subscription_access)
session.commit()
logger.info(
'subscription_cancelled_via_webhook',
extra={
'stripe_subscription_id': subscription_id,
'user_id': subscription_access.user_id,
'subscription_access_id': subscription_access.id,
},
)
elif event_type == 'customer.subscription.deleted':
subscription = event['data']['object']
subscription_id = subscription['id']
with session_maker() as session:
subscription_access = (
session.query(SubscriptionAccess)
.filter(SubscriptionAccess.stripe_subscription_id == subscription_id)
.filter(SubscriptionAccess.status == 'ACTIVE')
.first()
)
if subscription_access:
subscription_access.status = 'DISABLED'
subscription_access.updated_at = datetime.now(UTC)
session.merge(subscription_access)
session.commit()
# Reset user settings to free tier defaults
reset_user_to_free_tier_settings(subscription_access.user_id)
logger.info(
'subscription_expired_reset_to_free_tier',
extra={
'stripe_subscription_id': subscription_id,
'user_id': subscription_access.user_id,
'subscription_access_id': subscription_access.id,
},
)
else:
logger.info('stripe_webhook_unhandled_event_type', extra={'type': event_type})
return JSONResponse({'status': 'success'})
def reset_user_to_free_tier_settings(user_id: str) -> None:
"""Reset user settings to free tier defaults when subscription ends."""
with session_maker() as session:
user_settings = (
session.query(UserSettings)
.filter(UserSettings.keycloak_user_id == user_id)
.first()
)
if user_settings:
user_settings.llm_model = get_default_litellm_model()
user_settings.llm_api_key = None
user_settings.llm_api_key_for_byor = None
user_settings.llm_base_url = LITE_LLM_API_URL
user_settings.max_budget_per_task = None
user_settings.confirmation_mode = False
user_settings.enable_solvability_analysis = False
user_settings.security_analyzer = 'llm'
user_settings.agent = 'CodeActAgent'
user_settings.language = 'en'
user_settings.enable_default_condenser = True
user_settings.enable_sound_notifications = False
user_settings.enable_proactive_conversation_starters = True
user_settings.user_consents_to_analytics = False
session.merge(user_settings)
session.commit()
logger.info(
'user_settings_reset_to_free_tier',
extra={
'user_id': user_id,
'reset_timestamp': datetime.now(UTC).isoformat(),
},
)
async def _get_litellm_user(client: httpx.AsyncClient, user_id: str) -> dict:
"""Get a user from litellm with the id matching that given.

View File

@@ -234,7 +234,7 @@ def _get_user_id(conversation_id: str) -> str:
return conversation_metadata.user_id
async def _get_session_api_key(user_id: str, conversation_id: str) -> str:
async def _get_session_api_key(user_id: str, conversation_id: str) -> str | None:
agent_loop_info = await conversation_manager.get_agent_loop_info(
user_id, filter_to_sids={conversation_id}
)

View File

@@ -174,37 +174,121 @@ class SaasNestedConversationManager(ConversationManager):
initial_user_msg: MessageAction | None = None,
replay_json: str | None = None,
) -> AgentLoopInfo:
logger.info(
f'[TOKEN_DEBUG] SaasNestedConversationManager.maybe_start_agent_loop ENTRY: '
f'sid={sid}, user_id={user_id}'
)
# First we check redis to see if we are already starting - or the runtime will tell us the session is stopped
redis = self._get_redis_client()
key = self._get_redis_conversation_key(user_id, sid)
starting = await redis.get(key)
logger.info(f'[TOKEN_DEBUG] Getting runtime for sid={sid}...')
runtime = await self._get_runtime(sid)
logger.info(
f'[TOKEN_DEBUG] Runtime info for {sid}: '
f'exists={runtime is not None}, '
f'runtime_id={runtime.get("runtime_id") if runtime else None}, '
f'status={runtime.get("status") if runtime else None}, '
f'session_api_key_exists={bool(runtime.get("session_api_key")) if runtime else False}'
)
# Get raw runtime status for branching decisions
raw_runtime_status = (runtime.get('status') or '').lower() if runtime else ''
# Use _parse_status() only to compute the UI-facing ConversationStatus
status = self._parse_status(runtime) if runtime else ConversationStatus.STOPPED
nested_url = None
session_api_key = None
status = ConversationStatus.STOPPED
event_store = EventStore(sid, self.file_store, user_id)
if runtime:
nested_url = self._get_nested_url_for_runtime(runtime['runtime_id'], sid)
session_api_key = runtime.get('session_api_key')
status_str = (runtime.get('status') or 'stopped').upper()
if status_str in ConversationStatus:
status = ConversationStatus[status_str]
if status is ConversationStatus.STOPPED and starting:
status = ConversationStatus.STARTING
if status is ConversationStatus.STOPPED:
# Mark the agentloop as starting in redis
await redis.set(key, 1, ex=_REDIS_ENTRY_TIMEOUT_SECONDS)
# Start the agent loop in the background
asyncio.create_task(
self._start_agent_loop(
sid, settings, user_id, initial_user_msg, replay_json
)
logger.info(
f'[TOKEN_DEBUG] Retrieved from runtime: '
f'key_preview={session_api_key[:10] if session_api_key else "None"}..., '
f'runtime_id={runtime.get("runtime_id")}, '
f'raw_status={raw_runtime_status}, '
f'parsed_status={status}'
)
# Determine if we need to start/resume the conversation
# Key insight: We should only skip starting if:
# 1. Runtime is running AND
# 2. We're already starting (redis flag) OR conversation already exists
should_schedule_work = False
is_resume = False
if raw_runtime_status == 'paused':
# Always resume paused conversations
should_schedule_work = True
is_resume = True
logger.info(f'[TOKEN_DEBUG] Will resume paused conversation {sid}')
elif raw_runtime_status in ('stopped', ''):
# Start new for stopped or non-existent runtimes
should_schedule_work = True
logger.info(f'[TOKEN_DEBUG] Will start new conversation {sid} (status={raw_runtime_status})')
elif raw_runtime_status == 'running':
# For running, only start if not already starting
if starting:
logger.info(f'[TOKEN_DEBUG] Already starting {sid} per Redis, returning STARTING')
return AgentLoopInfo(
conversation_id=sid,
url=nested_url,
session_api_key=session_api_key,
event_store=event_store,
status=ConversationStatus.STARTING,
)
else:
# Runtime is running but we're not starting - this means conversation should exist
# Return RUNNING status
logger.info(f'[TOKEN_DEBUG] Runtime running and not starting, returning RUNNING')
return AgentLoopInfo(
conversation_id=sid,
url=nested_url,
session_api_key=session_api_key,
event_store=event_store,
status=ConversationStatus.RUNNING,
)
if should_schedule_work:
if not starting:
logger.info(
f'[TOKEN_DEBUG] Scheduling {"resume" if is_resume else "start"} '
f'for sid={sid} (raw_status={raw_runtime_status})'
)
await redis.set(key, 1, ex=_REDIS_ENTRY_TIMEOUT_SECONDS)
asyncio.create_task(
self._start_agent_loop(
sid, settings, user_id, initial_user_msg, replay_json, is_resume
)
)
else:
logger.info(
f'[TOKEN_DEBUG] Already starting {sid} according to Redis, not scheduling again'
)
# Return STARTING when work is scheduled or in progress
return AgentLoopInfo(
conversation_id=sid,
url=nested_url,
session_api_key=session_api_key,
event_store=event_store,
status=ConversationStatus.STARTING,
)
logger.info(
f'[TOKEN_DEBUG] Returning from maybe_start_agent_loop: '
f'sid={sid}, status={status}, '
f'has_url={bool(nested_url)}, has_api_key={bool(session_api_key)}'
)
return AgentLoopInfo(
conversation_id=sid,
url=nested_url,
@@ -214,14 +298,29 @@ class SaasNestedConversationManager(ConversationManager):
)
async def _start_agent_loop(
self, sid, settings, user_id, initial_user_msg=None, replay_json=None
self,
sid,
settings,
user_id,
initial_user_msg=None,
replay_json=None,
is_resume=False,
):
try:
logger.info(f'starting_agent_loop:{sid}', extra={'session_id': sid})
logger.info(
f'[TOKEN_DEBUG] SaaS _start_agent_loop: sid={sid}, is_resume={is_resume}'
)
if is_resume:
logger.info(f'[RESUME_DEBUG] Resuming existing runtime for sid={sid}')
else:
logger.info(f'[RESUME_DEBUG] Creating new runtime for sid={sid}')
await self.ensure_num_conversations_below_limit(sid, user_id)
provider_handler = self._get_provider_handler(settings)
runtime = await self._create_runtime(
sid, user_id, settings, provider_handler
sid, user_id, settings, provider_handler, is_resume
)
await runtime.connect()
@@ -233,16 +332,51 @@ class SaasNestedConversationManager(ConversationManager):
)
session_api_key = runtime.session.headers['X-Session-API-Key']
await self._start_conversation(
sid,
user_id,
settings,
initial_user_msg,
replay_json,
runtime.runtime_url,
session_api_key,
logger.info(
f'[TOKEN_DEBUG] Got session_api_key from runtime: '
f'key_preview={session_api_key[:10] if session_api_key else "None"}...'
)
# Check if we should skip conversation creation on resume
if is_resume:
# Get the existing runtime to check if we already have a session_api_key
existing_runtime = await self._get_runtime(sid)
if existing_runtime and existing_runtime.get('session_api_key'):
logger.info(
'[RESUME_DEBUG] Skipping conversation creation for resume '
'(using existing session_api_key)'
)
# Use the EXISTING session_api_key for resume, not the new one
existing_session_key = existing_runtime.get('session_api_key')
# Just wait for the runtime to be ready
async with httpx.AsyncClient(
headers={'X-Session-API-Key': existing_session_key}
) as client:
await self._wait_for_conversation_ready(
client, runtime.runtime_url, sid
)
else:
# Resume but no existing session, create new conversation
await self._start_conversation(
sid,
user_id,
settings,
initial_user_msg,
replay_json,
runtime.runtime_url,
session_api_key,
)
else:
# Not a resume, normal start
await self._start_conversation(
sid,
user_id,
settings,
initial_user_msg,
replay_json,
runtime.runtime_url,
session_api_key,
)
finally:
# remove the starting entry from redis
redis = self._get_redis_client()
@@ -260,6 +394,11 @@ class SaasNestedConversationManager(ConversationManager):
session_api_key: str,
):
logger.info('starting_nested_conversation', extra={'sid': sid})
logger.info(
f'[TOKEN_DEBUG] _start_conversation with session_api_key: '
f'key_preview={session_api_key[:10] if session_api_key else "None"}..., '
f'api_url={api_url[:50] if api_url else "None"}...'
)
async with httpx.AsyncClient(
headers={
'X-Session-API-Key': session_api_key,
@@ -418,16 +557,35 @@ class SaasNestedConversationManager(ConversationManager):
):
"""Wait for the conversation to be ready by checking the events endpoint."""
# TODO: Find out why /api/conversations/{sid} returns RUNNING when events are not available
for _ in range(5):
logger.info(
f'[WEBSOCKET_DEBUG] Starting _wait_for_conversation_ready for sid={sid}, '
f'will check events endpoint up to 5 times'
)
for attempt in range(5):
try:
logger.info('checking_events_endpoint_running', extra={'sid': sid})
logger.info(
f'[WEBSOCKET_DEBUG] Attempt {attempt+1}/5: Checking {api_url}/api/conversations/{sid}/events'
)
response = await client.get(f'{api_url}/api/conversations/{sid}/events')
if response.is_success:
logger.info('events_endpoint_is_running', extra={'sid': sid})
logger.info(
f'[WEBSOCKET_DEBUG] Events endpoint ready after {attempt+1} attempts. '
f'Frontend should now be able to connect via websocket.'
)
break
except Exception:
except Exception as e:
logger.warning('events_endpoint_not_ready', extra={'sid': sid})
logger.warning(
f'[WEBSOCKET_DEBUG] Events endpoint not ready (attempt {attempt+1}/5): {e}'
)
await asyncio.sleep(5)
else:
logger.error(
f'[WEBSOCKET_DEBUG] CRITICAL: Events endpoint never became ready after 5 attempts! '
f'Frontend will not receive events for sid={sid}'
)
async def send_to_event_stream(self, connection_id: str, data: dict):
# Not supported - clients should connect directly to the nested server!
@@ -462,10 +620,17 @@ class SaasNestedConversationManager(ConversationManager):
async def close_session(self, sid: str):
logger.info('close_session', extra={'sid': sid})
logger.info(
f'[TOKEN_DEBUG] close_session called for {sid}, about to pause runtime'
)
runtime = await self._get_runtime(sid)
if runtime is None:
logger.info('no_session_to_close', extra={'sid': sid})
logger.info(f'[TOKEN_DEBUG] No runtime found to close for {sid}')
return
logger.info(
f'[TOKEN_DEBUG] Pausing runtime {runtime.get("runtime_id")} for session {sid}'
)
async with self._httpx_client() as client:
response = await client.post(
f'{self.remote_runtime_api_url}/pause',
@@ -691,6 +856,15 @@ class SaasNestedConversationManager(ConversationManager):
provider_tokens = None
if isinstance(settings, ConversationInitData):
provider_tokens = settings.git_provider_tokens
logger.info(
f'[TOKEN_DEBUG] Getting provider handler: '
f'has_settings={settings is not None}, '
f'is_ConversationInitData={isinstance(settings, ConversationInitData)}, '
f'has_provider_tokens={bool(provider_tokens)}, '
f'token_count={len(provider_tokens) if provider_tokens else 0}'
)
provider_handler = ProviderHandler(
provider_tokens=provider_tokens
or cast(PROVIDER_TOKEN_TYPE, MappingProxyType({}))
@@ -703,7 +877,40 @@ class SaasNestedConversationManager(ConversationManager):
user_id: str,
settings: Settings,
provider_handler: ProviderHandler,
is_resume: bool = False,
):
# Check if we have an existing runtime to understand the context
logger.info(
f'[RESUME_DEBUG] _create_runtime called for sid={sid}, is_resume={is_resume}'
)
existing_runtime = await self._get_runtime(sid)
# Determine if we should attach to existing runtime
attach_to_existing = False
if existing_runtime:
raw_status = (existing_runtime.get('status') or '').lower()
logger.info(
f'[RESUME_DEBUG] Found existing runtime: '
f'runtime_id={existing_runtime.get("runtime_id")}, '
f'status={raw_status}, '
f'has_api_key={bool(existing_runtime.get("session_api_key"))}'
)
# Attach to existing runtime if it's paused or running
if raw_status in ('paused', 'running'):
attach_to_existing = True
logger.info(
f'[RESUME_DEBUG] Will attach to existing {raw_status} runtime'
)
else:
logger.info(
f'[RESUME_DEBUG] Will create new runtime (existing is {raw_status})'
)
else:
logger.info(
'[RESUME_DEBUG] No existing runtime found, creating fresh runtime'
)
llm_registry, conversation_stats, config = (
create_registry_and_conversation_stats(self.config, sid, user_id, settings)
)
@@ -764,6 +971,29 @@ class SaasNestedConversationManager(ConversationManager):
if self._runtime_container_image:
config.sandbox.runtime_container_image = self._runtime_container_image
# Log the attach_to_existing decision
logger.info(
f'[ATTACH_DEBUG] Making attach_to_existing decision: '
f'sid={sid}, attach_to_existing={attach_to_existing}, '
f'reasoning={"attach to paused/running" if attach_to_existing else "create new"}'
)
logger.info(
f'[TOKEN_DEBUG] Creating RemoteRuntime: '
f'sid={sid}, attach_to_existing={attach_to_existing}, '
f'user_id={user_id}, '
f'has_provider_tokens={bool(provider_handler and provider_handler.provider_tokens)}'
)
# Log the state of tokens before runtime creation
if provider_handler and provider_handler.provider_tokens:
logger.info(
f'[TOKEN_DEBUG] Provider tokens before runtime creation: '
f'{list(provider_handler.provider_tokens.keys())}'
)
else:
logger.info('[TOKEN_DEBUG] No provider tokens before runtime creation')
runtime = RemoteRuntime(
config=config,
event_stream=None, # type: ignore[arg-type]
@@ -771,7 +1001,7 @@ class SaasNestedConversationManager(ConversationManager):
plugins=agent.sandbox_plugins,
# env_vars=env_vars,
# status_callback: Callable[..., None] | None = None,
attach_to_existing=False,
attach_to_existing=attach_to_existing,
headless_mode=False,
user_id=user_id,
# git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
@@ -779,6 +1009,10 @@ class SaasNestedConversationManager(ConversationManager):
llm_registry=llm_registry,
)
logger.info(
f'[TOKEN_DEBUG] RemoteRuntime created: runtime_id={runtime.runtime_id if hasattr(runtime, "runtime_id") else "N/A"}'
)
# TODO: This is a hack. The setup_initial_env method directly calls the methods on the action
# execution server, even though there are not any variables to set. In the nested env, there
# is currently no direct access to the action execution server, so we should either add a
@@ -826,10 +1060,27 @@ class SaasNestedConversationManager(ConversationManager):
async def _get_runtime(self, sid: str) -> dict | None:
async with self._httpx_client() as client:
response = await client.get(f'{self.remote_runtime_api_url}/sessions/{sid}')
url = f'{self.remote_runtime_api_url}/sessions/{sid}'
logger.info(f'[TOKEN_DEBUG] Fetching runtime from: {url}')
response = await client.get(url)
if not response.is_success:
logger.info(
f'[TOKEN_DEBUG] Runtime fetch failed: '
f'status_code={response.status_code}, '
f'sid={sid}'
)
return None
response_json = response.json()
logger.info(
f'[TOKEN_DEBUG] Runtime fetched successfully: '
f'sid={sid}, '
f'runtime_id={response_json.get("runtime_id")}, '
f'status={response_json.get("status")}, '
f'has_api_key={bool(response_json.get("session_api_key"))}'
)
# Hack: This endpoint doesn't return the session_id
response_json['session_id'] = sid
@@ -839,12 +1090,26 @@ class SaasNestedConversationManager(ConversationManager):
def _parse_status(self, runtime: dict):
# status is one of running, stoppped, paused, error, starting
status = (runtime.get('status') or '').upper()
logger.info(
f'[TOKEN_DEBUG] _parse_status: input_status="{runtime.get("status")}", '
f'uppercase_status="{status}", '
f'is_paused={status == "PAUSED"}, '
f'is_stopped={status == "STOPPED"}'
)
if status == 'PAUSED':
logger.info('[TOKEN_DEBUG] Mapping PAUSED -> ConversationStatus.STOPPED')
return ConversationStatus.STOPPED
elif status == 'STOPPED':
logger.info('[TOKEN_DEBUG] Mapping STOPPED -> ConversationStatus.ARCHIVED')
return ConversationStatus.ARCHIVED
if status in ConversationStatus:
logger.info(f'[TOKEN_DEBUG] Direct mapping to ConversationStatus.{status}')
return ConversationStatus[status]
logger.info(
f'[TOKEN_DEBUG] Unknown status "{status}", defaulting to ConversationStatus.STOPPED'
)
return ConversationStatus.STOPPED
def _get_nested_url_for_runtime(self, runtime_id: str, conversation_id: str):

View File

@@ -37,6 +37,14 @@ class ApiKeyStore:
"""
api_key = self.generate_api_key()
logger.info(
f'[TOKEN_DEBUG] Creating API key: '
f'user_id={user_id}, '
f'name={name}, '
f'expires_at={expires_at}, '
f'key_preview={api_key[:10] if api_key else "None"}...'
)
with self.session_maker() as session:
key_record = ApiKey(
key=api_key, user_id=user_id, name=name, expires_at=expires_at
@@ -44,21 +52,43 @@ class ApiKeyStore:
session.add(key_record)
session.commit()
logger.info(
f'[TOKEN_DEBUG] API key created successfully: '
f'key_id={key_record.id}, '
f'user_id={user_id}, '
f'name={name}'
)
return api_key
def validate_api_key(self, api_key: str) -> str | None:
"""Validate an API key and return the associated user_id if valid."""
now = datetime.now(UTC)
logger.info(
f'[TOKEN_DEBUG] Validating API key: '
f'key_preview={api_key[:10] if api_key else "None"}...'
)
with self.session_maker() as session:
key_record = session.query(ApiKey).filter(ApiKey.key == api_key).first()
if not key_record:
logger.info(
f'[TOKEN_DEBUG] API key not found in database: '
f'key_preview={api_key[:10] if api_key else "None"}...'
)
return None
# Check if the key has expired
if key_record.expires_at and key_record.expires_at < now:
logger.info(f'API key has expired: {key_record.id}')
logger.info(
f'[TOKEN_DEBUG] API key expired: '
f'key_id={key_record.id}, '
f'expires_at={key_record.expires_at}, '
f'now={now}'
)
return None
# Update last_used_at timestamp
@@ -69,6 +99,13 @@ class ApiKeyStore:
)
session.commit()
logger.info(
f'[TOKEN_DEBUG] API key validated successfully: '
f'key_id={key_record.id}, '
f'user_id={key_record.user_id}, '
f'name={key_record.name}'
)
return key_record.user_id
def delete_api_key(self, api_key: str) -> bool:

View File

@@ -7,7 +7,7 @@ from storage.base import Base
class SubscriptionAccess(Base): # type: ignore
"""
Represents a user's subscription access record.
Tracks subscription status, duration, and payment information.
Tracks subscription status, duration, payment information, and cancellation status.
"""
__tablename__ = 'subscription_access'
@@ -27,6 +27,8 @@ class SubscriptionAccess(Base): # type: ignore
end_at = Column(DateTime(timezone=True), nullable=True)
amount_paid = Column(DECIMAL(19, 4), nullable=True)
stripe_invoice_payment_id = Column(String, nullable=False)
cancelled_at = Column(DateTime(timezone=True), nullable=True)
stripe_subscription_id = Column(String, nullable=True, index=True)
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(UTC), # type: ignore[attr-defined]

View File

@@ -276,12 +276,12 @@ class VerifyWebhookStatus:
webhook
)
gitlab_service = GitLabServiceImpl(external_auth_id=user_id)
gitlab_service_impl = GitLabServiceImpl(external_auth_id=user_id)
if not isinstance(gitlab_service, SaaSGitLabService):
if not isinstance(gitlab_service_impl, SaaSGitLabService):
raise Exception('Only SaaSGitLabService is supported')
# Cast needed when mypy can see OpenHands
gitlab_service = cast(type[SaaSGitLabService], gitlab_service)
gitlab_service = cast(type[SaaSGitLabService], gitlab_service_impl)
await self.verify_conditions_are_met(
gitlab_service=gitlab_service,

View File

@@ -0,0 +1,159 @@
"""Tests for enterprise integrations utils module."""
import pytest
from integrations.utils import get_summary_for_agent_state
from openhands.core.schema.agent import AgentState
from openhands.events.observation.agent import AgentStateChangedObservation
class TestGetSummaryForAgentState:
"""Test cases for get_summary_for_agent_state function."""
def setup_method(self):
"""Set up test fixtures."""
self.conversation_link = 'https://example.com/conversation/123'
def test_empty_observations_list(self):
"""Test handling of empty observations list."""
result = get_summary_for_agent_state([], self.conversation_link)
assert 'unknown error' in result.lower()
assert self.conversation_link in result
@pytest.mark.parametrize(
'state,expected_text,includes_link',
[
(AgentState.RATE_LIMITED, 'rate limited', False),
(AgentState.AWAITING_USER_INPUT, 'waiting for your input', True),
],
)
def test_handled_agent_states(self, state, expected_text, includes_link):
"""Test handling of states with specific behavior."""
observation = AgentStateChangedObservation(
content=f'Agent state: {state.value}', agent_state=state
)
result = get_summary_for_agent_state([observation], self.conversation_link)
assert expected_text in result.lower()
if includes_link:
assert self.conversation_link in result
else:
assert self.conversation_link not in result
@pytest.mark.parametrize(
'state',
[
AgentState.FINISHED,
AgentState.PAUSED,
AgentState.STOPPED,
AgentState.AWAITING_USER_CONFIRMATION,
],
)
def test_unhandled_agent_states(self, state):
"""Test handling of unhandled states (should all return unknown error)."""
observation = AgentStateChangedObservation(
content=f'Agent state: {state.value}', agent_state=state
)
result = get_summary_for_agent_state([observation], self.conversation_link)
assert 'unknown error' in result.lower()
assert self.conversation_link in result
@pytest.mark.parametrize(
'error_code,expected_text',
[
(
'STATUS$ERROR_LLM_AUTHENTICATION',
'authentication with the llm provider failed',
),
(
'STATUS$ERROR_LLM_SERVICE_UNAVAILABLE',
'llm service is temporarily unavailable',
),
(
'STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR',
'llm provider encountered an internal error',
),
('STATUS$ERROR_LLM_OUT_OF_CREDITS', "you've run out of credits"),
('STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION', 'content policy violation'),
],
)
def test_error_state_readable_reasons(self, error_code, expected_text):
"""Test all readable error reason mappings."""
observation = AgentStateChangedObservation(
content=f'Agent encountered error: {error_code}',
agent_state=AgentState.ERROR,
reason=error_code,
)
result = get_summary_for_agent_state([observation], self.conversation_link)
assert 'encountered an error' in result.lower()
assert expected_text in result.lower()
assert self.conversation_link in result
def test_error_state_with_custom_reason(self):
"""Test handling of ERROR state with a custom reason."""
observation = AgentStateChangedObservation(
content='Agent encountered an error',
agent_state=AgentState.ERROR,
reason='Test error message',
)
result = get_summary_for_agent_state([observation], self.conversation_link)
assert 'encountered an error' in result.lower()
assert 'test error message' in result.lower()
assert self.conversation_link in result
def test_multiple_observations_uses_first(self):
"""Test that when multiple observations are provided, only the first is used."""
observation1 = AgentStateChangedObservation(
content='Agent is awaiting user input',
agent_state=AgentState.AWAITING_USER_INPUT,
)
observation2 = AgentStateChangedObservation(
content='Agent encountered an error',
agent_state=AgentState.ERROR,
reason='Should not be used',
)
result = get_summary_for_agent_state(
[observation1, observation2], self.conversation_link
)
# Should handle the first observation (AWAITING_USER_INPUT), not the second (ERROR)
assert 'waiting for your input' in result.lower()
assert 'error' not in result.lower()
def test_awaiting_user_input_specific_message(self):
"""Test that AWAITING_USER_INPUT returns the specific expected message."""
observation = AgentStateChangedObservation(
content='Agent is awaiting user input',
agent_state=AgentState.AWAITING_USER_INPUT,
)
result = get_summary_for_agent_state([observation], self.conversation_link)
# Test the exact message format
assert 'waiting for your input' in result.lower()
assert 'continue the conversation' in result.lower()
assert self.conversation_link in result
assert 'unknown error' not in result.lower()
def test_rate_limited_specific_message(self):
"""Test that RATE_LIMITED returns the specific expected message."""
observation = AgentStateChangedObservation(
content='Agent was rate limited', agent_state=AgentState.RATE_LIMITED
)
result = get_summary_for_agent_state([observation], self.conversation_link)
# Test the exact message format
assert 'rate limited' in result.lower()
assert 'try again later' in result.lower()
# RATE_LIMITED doesn't include conversation link in response
assert self.conversation_link not in result

View File

@@ -5,16 +5,16 @@ import pytest
import stripe
from fastapi import HTTPException, Request, status
from httpx import HTTPStatusError, Response
from server.routes import billing
from integrations.stripe_service import has_payment_method
from server.routes.billing import (
CreateBillingSessionResponse,
CreateCheckoutSessionRequest,
GetCreditsResponse,
cancel_callback,
cancel_subscription,
create_checkout_session,
create_customer_setup_session,
create_subscription_checkout_session,
get_credits,
has_payment_method,
success_callback,
)
from sqlalchemy import create_engine
@@ -362,8 +362,7 @@ async def test_cancel_callback_session_not_found():
response = await cancel_callback('test_session_id', mock_request)
assert response.status_code == 302
assert (
response.headers['location']
== 'http://test.com/settings/billing?checkout=cancel'
response.headers['location'] == 'http://test.com/settings?checkout=cancel'
)
# Verify no database updates occurred
@@ -389,8 +388,7 @@ async def test_cancel_callback_success():
assert response.status_code == 302
assert (
response.headers['location']
== 'http://test.com/settings/billing?checkout=cancel'
response.headers['location'] == 'http://test.com/settings?checkout=cancel'
)
# Verify database updates
@@ -402,51 +400,312 @@ async def test_cancel_callback_success():
@pytest.mark.asyncio
async def test_has_payment_method_with_payment_method():
"""Test has_payment_method returns True when user has a payment method."""
mock_has_payment_method = AsyncMock(return_value=True)
with patch(
'integrations.stripe_service.has_payment_method', mock_has_payment_method
with (
patch('integrations.stripe_service.session_maker') as mock_session_maker,
patch(
'stripe.Customer.list_payment_methods_async',
AsyncMock(return_value=MagicMock(data=[MagicMock()])),
) as mock_list_payment_methods,
):
# Setup mock session
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.first.return_value = (
MagicMock(stripe_customer_id='cus_test123')
)
result = await has_payment_method('mock_user')
assert result is True
mock_has_payment_method.assert_called_once_with('mock_user')
mock_list_payment_methods.assert_called_once_with('cus_test123')
@pytest.mark.asyncio
async def test_has_payment_method_without_payment_method():
"""Test has_payment_method returns False when user has no payment method."""
mock_has_payment_method = AsyncMock(return_value=False)
with patch(
'integrations.stripe_service.has_payment_method', mock_has_payment_method
with (
patch('integrations.stripe_service.session_maker') as mock_session_maker,
patch(
'stripe.Customer.list_payment_methods_async',
AsyncMock(return_value=MagicMock(data=[])),
) as mock_list_payment_methods,
):
mock_has_payment_method.return_value = False
# Setup mock session
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.first.return_value = (
MagicMock(stripe_customer_id='cus_test123')
)
result = await has_payment_method('mock_user')
assert result is False
mock_has_payment_method.assert_called_once_with('mock_user')
mock_list_payment_methods.assert_called_once_with('cus_test123')
@pytest.mark.asyncio
async def test_create_customer_setup_session_success():
"""Test successful creation of customer setup session."""
mock_request = Request(
scope={'type': 'http', 'state': {'user_id': 'mock_user'}, 'headers': []}
async def test_cancel_subscription_success():
"""Test successful subscription cancellation."""
from datetime import UTC, datetime
from storage.subscription_access import SubscriptionAccess
# Mock active subscription
mock_subscription_access = SubscriptionAccess(
id=1,
status='ACTIVE',
user_id='test_user',
start_at=datetime.now(UTC),
end_at=datetime.now(UTC),
amount_paid=2000,
stripe_invoice_payment_id='pi_test',
stripe_subscription_id='sub_test123',
cancelled_at=None,
)
mock_customer = stripe.Customer(
id='mock-customer', metadata={'user_id': 'mock-user'}
)
mock_session = MagicMock()
mock_session.url = 'https://checkout.stripe.com/test-session'
mock_create = AsyncMock(return_value=mock_session)
# Mock Stripe subscription response
mock_stripe_subscription = MagicMock()
mock_stripe_subscription.cancel_at_period_end = True
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
patch(
'stripe.Subscription.modify_async',
AsyncMock(return_value=mock_stripe_subscription),
) as mock_stripe_modify,
):
# Setup mock session
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = mock_subscription_access
# Call the function
result = await cancel_subscription('test_user')
# Verify Stripe API was called
mock_stripe_modify.assert_called_once_with(
'sub_test123', cancel_at_period_end=True
)
# Verify database was updated
assert mock_subscription_access.cancelled_at is not None
mock_session.merge.assert_called_once_with(mock_subscription_access)
mock_session.commit.assert_called_once()
# Verify response
assert result.status_code == 200
@pytest.mark.asyncio
async def test_cancel_subscription_no_active_subscription():
"""Test cancellation when no active subscription exists."""
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
):
# Setup mock session with no subscription found
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = None
# Call the function and expect HTTPException
with pytest.raises(HTTPException) as exc_info:
await cancel_subscription('test_user')
assert exc_info.value.status_code == 404
assert 'No active subscription found' in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_cancel_subscription_missing_stripe_id():
"""Test cancellation when subscription has no Stripe ID."""
from datetime import UTC, datetime
from storage.subscription_access import SubscriptionAccess
# Mock subscription without Stripe ID
mock_subscription_access = SubscriptionAccess(
id=1,
status='ACTIVE',
user_id='test_user',
start_at=datetime.now(UTC),
end_at=datetime.now(UTC),
amount_paid=2000,
stripe_invoice_payment_id='pi_test',
stripe_subscription_id=None, # Missing Stripe ID
cancelled_at=None,
)
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
):
# Setup mock session
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = mock_subscription_access
# Call the function and expect HTTPException
with pytest.raises(HTTPException) as exc_info:
await cancel_subscription('test_user')
assert exc_info.value.status_code == 400
assert 'missing Stripe subscription ID' in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_cancel_subscription_stripe_error():
"""Test cancellation when Stripe API fails."""
from datetime import UTC, datetime
from storage.subscription_access import SubscriptionAccess
# Mock active subscription
mock_subscription_access = SubscriptionAccess(
id=1,
status='ACTIVE',
user_id='test_user',
start_at=datetime.now(UTC),
end_at=datetime.now(UTC),
amount_paid=2000,
stripe_invoice_payment_id='pi_test',
stripe_subscription_id='sub_test123',
cancelled_at=None,
)
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
patch(
'stripe.Subscription.modify_async',
AsyncMock(side_effect=stripe.StripeError('API Error')),
),
):
# Setup mock session
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = mock_subscription_access
# Call the function and expect HTTPException
with pytest.raises(HTTPException) as exc_info:
await cancel_subscription('test_user')
assert exc_info.value.status_code == 500
assert 'Failed to cancel subscription' in str(exc_info.value.detail)
@pytest.mark.asyncio
async def test_create_subscription_checkout_session_duplicate_prevention():
"""Test that creating a subscription when user already has active subscription raises error."""
from datetime import UTC, datetime
from storage.subscription_access import SubscriptionAccess
# Mock active subscription
mock_subscription_access = SubscriptionAccess(
id=1,
status='ACTIVE',
user_id='test_user',
start_at=datetime.now(UTC),
end_at=datetime.now(UTC),
amount_paid=2000,
stripe_invoice_payment_id='pi_test',
stripe_subscription_id='sub_test123',
cancelled_at=None,
)
mock_request = Request(scope={'type': 'http'})
mock_request._base_url = URL('http://test.com/')
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
):
# Setup mock session to return existing active subscription
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = mock_subscription_access
# Call the function and expect HTTPException
with pytest.raises(HTTPException) as exc_info:
await create_subscription_checkout_session(
mock_request, user_id='test_user'
)
assert exc_info.value.status_code == 400
assert (
'user already has an active subscription'
in str(exc_info.value.detail).lower()
)
@pytest.mark.asyncio
async def test_create_subscription_checkout_session_allows_after_cancellation():
"""Test that creating a subscription is allowed when previous subscription was cancelled."""
mock_request = Request(scope={'type': 'http'})
mock_request._base_url = URL('http://test.com/')
mock_session_obj = MagicMock()
mock_session_obj.url = 'https://checkout.stripe.com/test-session'
mock_session_obj.id = 'test_session_id'
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
patch(
'integrations.stripe_service.find_or_create_customer',
AsyncMock(return_value=mock_customer),
AsyncMock(return_value='cus_test123'),
),
patch(
'stripe.checkout.Session.create_async',
AsyncMock(return_value=mock_session_obj),
),
patch(
'server.routes.billing.SUBSCRIPTION_PRICE_DATA',
{'MONTHLY_SUBSCRIPTION': {'unit_amount': 2000}},
),
patch('stripe.checkout.Session.create_async', mock_create),
):
result = await create_customer_setup_session(mock_request)
# Setup mock session - the query should return None because cancelled subscriptions are filtered out
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = None
assert isinstance(result, billing.CreateBillingSessionResponse)
# Should succeed
result = await create_subscription_checkout_session(
mock_request, user_id='test_user'
)
assert isinstance(result, CreateBillingSessionResponse)
assert result.redirect_url == 'https://checkout.stripe.com/test-session'
@pytest.mark.asyncio
async def test_create_subscription_checkout_session_success_no_existing():
"""Test successful subscription creation when no existing subscription."""
mock_request = Request(scope={'type': 'http'})
mock_request._base_url = URL('http://test.com/')
mock_session_obj = MagicMock()
mock_session_obj.url = 'https://checkout.stripe.com/test-session'
mock_session_obj.id = 'test_session_id'
with (
patch('server.routes.billing.session_maker') as mock_session_maker,
patch(
'integrations.stripe_service.find_or_create_customer',
AsyncMock(return_value='cus_test123'),
),
patch(
'stripe.checkout.Session.create_async',
AsyncMock(return_value=mock_session_obj),
),
patch(
'server.routes.billing.SUBSCRIPTION_PRICE_DATA',
{'MONTHLY_SUBSCRIPTION': {'unit_amount': 2000}},
),
):
# Setup mock session to return no existing subscription
mock_session = MagicMock()
mock_session_maker.return_value.__enter__.return_value = mock_session
mock_session.query.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.filter.return_value.first.return_value = None
# Should succeed
result = await create_subscription_checkout_session(
mock_request, user_id='test_user'
)
assert isinstance(result, CreateBillingSessionResponse)
assert result.redirect_url == 'https://checkout.stripe.com/test-session'

View File

@@ -175,17 +175,17 @@ class TestIsLegacyRuntime:
assert result is True
def test_is_legacy_runtime_empty_command(self, legacy_manager):
"""Test with empty command."""
"""Test with empty command - should use new manager."""
runtime = {'command': ''}
result = legacy_manager.is_legacy_runtime(runtime)
assert result is True
assert result is False # Empty command means use new manager
def test_is_legacy_runtime_missing_command_key(self, legacy_manager):
"""Test with runtime missing command key."""
"""Test with runtime missing command key - should use new manager."""
runtime = {'other_key': 'value'}
# This should raise a KeyError
with pytest.raises(KeyError):
legacy_manager.is_legacy_runtime(runtime)
# Should not raise KeyError, returns False (use new manager)
result = legacy_manager.is_legacy_runtime(runtime)
assert result is False
class TestShouldStartInLegacyMode:

View File

@@ -28,6 +28,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -36,7 +37,11 @@ from openhands.core.config import (
get_llm_config_arg,
load_from_toml,
)
from openhands.core.config.utils import get_agent_config_arg
from openhands.core.config.utils import (
get_agent_config_arg,
get_llms_for_routing_config,
get_model_routing_config_arg,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import AgentFinishAction, CmdRunAction, MessageAction
@@ -57,6 +62,7 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
@@ -66,13 +72,24 @@ def get_config(
sandbox_config=sandbox_config,
runtime='docker',
)
config.set_llm_config(metadata.llm_config)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
)
model_routing_config = get_model_routing_config_arg()
model_routing_config.llms_for_routing = (
get_llms_for_routing_config()
) # Populate with LLMs for routing from config.toml file
if metadata.agent_config:
metadata.agent_config.model_routing = model_routing_config
config.set_agent_config(metadata.agent_config, metadata.agent_class)
else:
logger.info('Agent config not provided, using default settings')
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.enable_prompt_extensions = False
agent_config.model_routing = model_routing_config
config_copy = copy.deepcopy(config)
load_from_toml(config_copy)
@@ -145,7 +162,7 @@ def process_instance(
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
config = get_config(metadata)
config = get_config(instance, metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:

View File

@@ -0,0 +1,152 @@
<h1 align="center"> Training Software Engineering Agents and Verifiers with SWE-Gym </h1>
A Multi-SWE-bench implementation of SWE-Gym.
<p align="center">
<a href="https://www.jiayipan.com/" style="text-decoration: none;">Jiayi Pan<sup>*,1</sup></a>,
<a href="https://xwang.dev/" style="text-decoration: none;">Xingyao Wang<sup>*,2</sup></a>,
<a href="https://www.phontron.com/" style="text-decoration: none;">Graham Neubig<sup>3</sup></a>,
<a href="https://www.cs.toronto.edu/~ndjaitly/" style="text-decoration: none;">Navdeep Jaitly<sup>4</sup></a>,
<a href="https://blender.cs.illinois.edu/hengji.html" style="text-decoration: none;">Heng Ji<sup>2</sup></a>,
<a href="https://www.alanesuhr.com/" style="text-decoration: none;">Alane Suhr<sup>^,1</sup></a>,
<a href="https://dreasysnail.github.io/" style="text-decoration: none;">Yizhe Zhang<sup>^,4</sup></a>
</p>
<p align="center">
<sup>1</sup>UC Berkeley, <sup>2</sup>UIUC, <sup>3</sup>CMU, <sup>4</sup>Apple </br>
<sub><sup>*</sup>Equal contribution, <sup>^</sup>Equal supervision</sub>
</p>
<p align="center">
<a href="https://arxiv.org/abs/2412.21139">📃 Paper</a>
<a href="https://huggingface.co/SWE-Gym" >🤗 Data & Models</a>
</p>
We present **SWE-Gym**, the first environment for training real-world software engineering agents.
We use it to train strong LM agents that achieve state-of-the-art open results on SWE-Bench, with early, promising scaling characteristics as we increase training and inference-time compute.
<p align="center">
<img src="https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/teaser.jpg?raw=true" width="100%" alt="teaser">
</p>
---
# Run SWE-Gym with OpenHands
The process of running SWE-Gym is very similar to how you'd run SWE-Bench evaluation.
1. First, clone OpenHands repo `git clone https://github.com/All-Hands-AI/OpenHands.git`
2. Then setup the repo following [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)
3. Then you can simply serve your own model as an OpenAI compatible endpoint, put those info in config.toml. You can do this by following instruction [here](../../README.md#setup).
4. And then simply do the following to sample for 16x parallelism:
```bash
export ALLHANDS_API_KEY=ah-yourkey # You don't need to set this when running these in local docker container
./evaluation/benchmarks/multi_swe_bench/scripts/rollout_swegym.sh llm.mymodel-temp05 'train-t05' 16
```
NOTE: SWE-Gym sampling with parallelism is currently only tested with AllHands RemoteRuntime (limited beta). Fill [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply for access.
5. When `rollout_swegym.sh` finishes, you will get a file called `output.with_completions.jsonl.gz`. Then you can use [`./scripts/swegym/convert_data.ipynb`](./scripts/swegym/convert_data.ipynb) to convert them into SFT data format.
## Running the Jupyter Notebook
To run the data conversion notebook, follow these steps:
1. Navigate to the OpenHands repository root:
```bash
cd openhands_repo
```
2. Set the PYTHONPATH and start Jupyter notebook:
```bash
PYTHONPATH=$(pwd) jupyter notebook
```
3. In the Jupyter interface, navigate to `evaluation/benchmarks/swe_bench/scripts/swegym/convert_data.ipynb`
4. Update the file paths in the notebook:
- Set `FILE_PATHS` to point to your `output.with_completions.jsonl.gz` files
- Set `YOUR_OUTPUT_FOLDER` to your desired output directory
5. Run the notebook cells sequentially to process your data and generate the SFT training format.
---
# More info about SWE-Gym
Progress in agents for software engineering has been limited by the lack of training environments that both include rigorous verification for reinforcement learning and cover the expansive tasks encountered in real-world repository-level engineering.
We introduce SWE-Gym: An Open Environment for Training Software Engineering Agents & Verifiers.
Our baselines achieve new open SOTA - 32%/26% on SWE-Bench Verified/Lite, with promising scaling trends.
![SWE-Gym Scaling](https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/scaling.jpg?raw=true)
*SWE-Gym enables scalable improvements for software engineering agents at both training and inference time. Our current results is primarily bottlenecked by training and inference compute, rather than the size of our environment.*
## SWE-Gym Environment
We create SWE-Gym, the first environment for training SWE agents, with **2.4K real tasks from 11 Python repos** & a Lite split of 234 instances. SWE-Gym combines real-world Python tasks, repository context, executable environments, and test verification to train agents for solving software engineering problems.
![SWE-Gym Repo Distribution](https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/swe-gym.jpg?raw=true)
## SWE-Gym trains LMs as agents
When fine-tuned on less than 500 agent-environment interaction trajectories sampled from it from GPT-4o and Claude 3.5 Sonnet, we achieve **+14%** absolute gains on SWE-Bench Verified with an 32B LM-powered OpenHands agent.
![OpenHands Performance diff before and after training](https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/oh-agent.jpg?raw=true)
## SWE-Gym enables self-improvement
SWE-Gym is also effective across agent scaffolds. With rejection sampling fine-tuning and MoatlessTools scaffold, our 32B and 7B models achieve 20% and 10% respectively on SWE-Bench Lite through self-improvement.
<p align="center">
<img src="https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/ml-agent.jpg?raw=true" width="80%" alt="Moatless self-improvement">
</p>
## SWE-Gym enables inference-time scaling
SWE-Gym enables inference-time scaling through verifiers trained on agent trajectories.
These verifiers identify most promising solutions via best-of-n selection, together with our learned agents, they achieve 32%/26% on SWE-Bench Verified/Lite, a new open SoTA.
![Inference Time Scaling for Moatless Agent](https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/inference-ml.jpg?raw=true)
*Inference Time Scaling for Moatless Agent*
![Inference Time Scaling for OpenHands Agent](https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/inference-oh.jpg?raw=true)
*Inference Time Scaling for OpenHands Agent*
## Our baselines on SWE-Gym shows strong scaling trends
Lastly, our ablations reveal strong scaling trends - performance is now bottlenecked by train and inference compute, rather than the size of our dataset. Pushing and improving these scaling trends further is an exciting direction for future work.
![](https://github.com/SWE-Gym/SWE-Gym/blob/main/assets/images/scaling.jpg?raw=true)
## Reproducing Results
**The Dataset**
To access SWE-Gym dataset, checkout our huggingface hub page [SWE-Gym](https://huggingface.co/SWE-Gym)
The environment constants are currently saved at [SWE-Bench-Fork](https://github.com/SWE-Gym/SWE-Bench-Fork)
We also have pre-built docker images for each instance under [xingyaoww/sweb.eval.x86_64](https://hub.docker.com/search?q=xingyaoww%2Fsweb.eval.x86_64.) prefix at docker hub.
## 📚 Citation
```bibtex
@misc{pan2024trainingsoftwareengineeringagents,
title={Training Software Engineering Agents and Verifiers with SWE-Gym},
author={Jiayi Pan and Xingyao Wang and Graham Neubig and Navdeep Jaitly and Heng Ji and Alane Suhr and Yizhe Zhang},
year={2024},
eprint={2412.21139},
archivePrefix={arXiv},
primaryClass={cs.SE},
url={https://arxiv.org/abs/2412.21139},
}
```

View File

@@ -51,8 +51,8 @@ RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'tru
# TODO: migrate all swe-bench docker to ghcr.io/openhands
# TODO: 适应所有的语言
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', '')
LANGUAGE = os.environ.get('LANGUAGE', 'python')
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'mswebench')
LANGUAGE = os.environ.get('LANGUAGE', 'java')
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
@@ -305,31 +305,19 @@ def get_instance_docker_image(instance: pd.Series):
instance_id = instance.get('instance_id', '')
tag_suffix = instance_id.split('-')[-1] if instance_id else ''
container_tag = f'pr-{tag_suffix}'
# pdb.set_trace()
return f'mswebench/{container_name}:{container_tag}'
# return "kong/insomnia:pr-8284"
# return "'sweb.eval.x86_64.local_insomnia"
# return "local_insomnia_why"
# return "local/kong-insomnia:pr-8117"
return f'{DOCKER_IMAGE_PREFIX}/{container_name}:{container_tag}'
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> OpenHandsConfig:
SWE_BENCH_CONTAINER_IMAGE = 'ghcr.io/opendevin/eval-swe-bench:full-v1.2.1'
if USE_INSTANCE_IMAGE:
# We use a different instance image for the each instance of swe-bench eval
# base_container_image = get_instance_docker_image(instance['instance_id'])
base_container_image = get_instance_docker_image(instance)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
else:
base_container_image = SWE_BENCH_CONTAINER_IMAGE
logger.info(f'Using swe-bench container image: {base_container_image}')
base_container_image = get_instance_docker_image(instance)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
@@ -772,7 +760,6 @@ if __name__ == '__main__':
parser.add_argument(
'--dataset',
type=str,
default='princeton-nlp/SWE-bench',
help='data set to evaluate on, either full-test or lite-test',
)
parser.add_argument(
@@ -787,6 +774,7 @@ if __name__ == '__main__':
# so we don't need to manage file uploading to OpenHands's repo
# dataset = load_dataset(args.dataset, split=args.split)
# dataset = load_dataset(args.dataset)
logger.info(f'Loading dataset {args.dataset} with split {args.split} ')
dataset = load_dataset('json', data_files=args.dataset)
dataset = dataset[args.split]
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
@@ -839,7 +827,7 @@ if __name__ == '__main__':
args.eval_num_workers,
process_instance,
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
max_retries=5,
max_retries=3,
)
# Check if any instances reached maximum retries
check_maximum_retries_exceeded(metadata.eval_output_dir)

View File

@@ -1,37 +1,54 @@
import argparse
import json
input_file = 'XXX.jsonl'
output_file = 'YYY.jsonl'
with (
open(input_file, 'r', encoding='utf-8') as fin,
open(output_file, 'w', encoding='utf-8') as fout,
):
for line in fin:
line = line.strip()
if not line:
continue
def main(input_file, output_file):
with (
open(input_file, 'r', encoding='utf-8') as fin,
open(output_file, 'w', encoding='utf-8') as fout,
):
for line in fin:
line = line.strip()
if not line:
continue
data = json.loads(line)
item = data
data = json.loads(line)
item = data
# 提取原始数据
org = item.get('org', '')
repo = item.get('repo', '')
number = str(item.get('number', ''))
# Skip instances that don't have resolved_issues or have empty resolved_issues
if not item.get('resolved_issues') or len(item['resolved_issues']) == 0:
print(
f'Skipping instance {item.get("org", "")}/{item.get("repo", "")}-{item.get("number", "")} - no resolved_issues'
)
continue
new_item = {}
new_item['repo'] = f'{org}/{repo}'
new_item['instance_id'] = f'{org}__{repo}-{number}'
new_item['problem_statement'] = (
item['resolved_issues'][0].get('title', '')
+ '\n'
+ item['resolved_issues'][0].get('body', '')
)
new_item['FAIL_TO_PASS'] = []
new_item['PASS_TO_PASS'] = []
new_item['base_commit'] = item['base'].get('sha', '')
new_item['version'] = '0.1' # depends
# 提取原始数据
org = item.get('org', '')
repo = item.get('repo', '')
number = str(item.get('number', ''))
output_data = new_item
fout.write(json.dumps(output_data, ensure_ascii=False) + '\n')
new_item = {}
new_item['repo'] = f'{org}/{repo}'
new_item['instance_id'] = f'{org}__{repo}-{number}'
# Get the first resolved issue
resolved_issue = item['resolved_issues'][0]
title = resolved_issue.get('title') or ''
body = resolved_issue.get('body') or ''
new_item['problem_statement'] = title + '\n' + body
new_item['FAIL_TO_PASS'] = []
new_item['PASS_TO_PASS'] = []
new_item['base_commit'] = item['base'].get('sha', '')
new_item['version'] = '0.1' # depends
output_data = new_item
fout.write(json.dumps(output_data, ensure_ascii=False) + '\n')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='Input .jsonl file path')
parser.add_argument('--output', required=True, help='Output .jsonl file path')
args = parser.parse_args()
main(args.input, args.output)

View File

@@ -0,0 +1,69 @@
import argparse
import gzip
import json
import os
from glob import glob
from tqdm import tqdm
tqdm.pandas()
# Load trajectories for resolved instances
def load_completions(output_dir: str, instance_id: str):
glob_path = os.path.join(output_dir, 'llm_completions', instance_id, '*.json')
files = sorted(glob(glob_path)) # this is ascending order
# pick the last file (last turn)
try:
file_path = files[-1]
except IndexError:
# print(f'No files found for instance {instance_id}: files={files}')
return None
with open(file_path, 'r') as f:
result = json.load(f)
# create messages
messages = result['messages']
messages.append(result['response']['choices'][0]['message'])
tools = result['kwargs'].get('tools', [])
return {
'messages': messages,
'tools': tools,
}
parser = argparse.ArgumentParser()
parser.add_argument('jsonl_path', type=str)
args = parser.parse_args()
output_dir = os.path.dirname(args.jsonl_path)
output_path = os.path.join(output_dir, 'output.with_completions.jsonl.gz')
# Check if output would be different from input
needs_update = False
with open(args.jsonl_path, 'r') as f_in:
for line in tqdm(f_in, desc='Checking for changes'):
data = json.loads(line)
new_completions = load_completions(output_dir, data['instance_id'])
current_completions = data.get('raw_completions')
if current_completions != new_completions:
needs_update = True
break
if not needs_update:
print('No updates required. Skipping file update.')
exit(0)
if os.path.exists(output_path):
print(f'Output file already exists at {output_path}, overwriting? (y/n)')
if input() != 'y':
print('Exiting...')
exit(0)
# Process line by line
with open(args.jsonl_path, 'r') as f_in, gzip.open(output_path, 'wt') as f_out:
for line in tqdm(f_in):
data = json.loads(line)
data['raw_completions'] = load_completions(output_dir, data['instance_id'])
f_out.write(json.dumps(data) + '\n')
print(f'Saved compressed output to {output_path}')

View File

@@ -1,13 +1,11 @@
import argparse
import json
import re
IN_FILE = 'output.jsonl'
OUT_FILE = 'patch.jsonl'
def main():
with open(IN_FILE, 'r') as fin:
with open(OUT_FILE, 'w') as fout:
def main(input_file, output_file):
with open(input_file, 'r') as fin:
with open(output_file, 'w') as fout:
for line in fin:
data = json.loads(line)
groups = re.match(r'(.*)__(.*)-(.*)', data['instance_id'])
@@ -15,10 +13,14 @@ def main():
'org': groups.group(1),
'repo': groups.group(2),
'number': groups.group(3),
'fix_patch': data['test_result']['git_patch'],
'fix_patch': data.get('test_result', {}).get('git_patch', '') or '',
}
fout.write(json.dumps(patch) + '\n')
if __name__ == '__main__':
main()
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='Input .jsonl file path')
parser.add_argument('--output', required=True, help='Output .jsonl file path')
args = parser.parse_args()
main(args.input, args.output)

View File

@@ -0,0 +1,70 @@
import argparse
import json
import os
import subprocess
def update_multi_swe_config(output_jsonl_path, config_path, dataset):
path_to_parent = os.path.dirname(os.path.abspath(output_jsonl_path))
converted_path = os.path.join(path_to_parent, 'output_converted.jsonl')
# Run the conversion script
subprocess.run(
[
'python3',
'./evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py',
'--input',
output_jsonl_path,
'--output',
converted_path,
],
check=True,
)
# Create required directories
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'dataset'), exist_ok=True)
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'workdir'), exist_ok=True)
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'repos'), exist_ok=True)
os.makedirs(os.path.join(path_to_parent, 'eval_files', 'logs'), exist_ok=True)
# Prepare config dict
config = {
'mode': 'evaluation',
'workdir': os.path.join(path_to_parent, 'eval_files', 'workdir'),
'patch_files': [converted_path],
'dataset_files': [dataset],
'force_build': True,
'output_dir': os.path.join(path_to_parent, 'eval_files', 'dataset'),
'specifics': [],
'skips': [],
'repo_dir': os.path.join(path_to_parent, 'eval_files', 'repos'),
'need_clone': True,
'global_env': [],
'clear_env': True,
'stop_on_error': False,
'max_workers': 5,
'max_workers_build_image': 5,
'max_workers_run_instance': 5,
'log_dir': os.path.join(path_to_parent, 'eval_files', 'logs'),
'log_level': 'DEBUG',
'fix_patch_run_cmd': (
'bash -c "apt update ; apt install -y patch ; '
"sed -i 's@git apply.*@patch --batch --fuzz=5 -p1 -i /home/test.patch;"
'patch --batch --fuzz=5 -p1 -i /home/fix.patch@g\' /home/fix-run.sh ; chmod +x /home/*.sh ; /home/fix-run.sh"'
),
}
# Save to multibench.config
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
json.dump(config, f, indent=4)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--input', required=True, help='Path to input file')
parser.add_argument('--output', required=True, help='Path to create config')
parser.add_argument('--dataset', required=True, help='Path to dataset')
args = parser.parse_args()
update_multi_swe_config(args.input, args.output, args.dataset)

View File

@@ -0,0 +1,176 @@
import argparse
import json
import os
from collections import defaultdict
from tqdm import tqdm
parser = argparse.ArgumentParser()
parser.add_argument('input_file', type=str)
parser.add_argument(
'--force',
action='store_true',
help='Force update all reports even if no changes are detected',
)
parser.add_argument(
'--overwrite-backup',
action='store_true',
help='Automatically overwrite existing backup files without prompting',
)
args = parser.parse_args()
dirname = os.path.dirname(args.input_file)
# Initialize counters and data structures
instance_id_to_status = defaultdict(
lambda: {
'empty_generation': False,
'resolved': False,
'failed_apply_patch': False,
'error_eval': False,
'test_timeout': False,
}
)
# Process official report if it exists
swebench_official_report_json = os.path.join(
dirname, 'eval_files/dataset/final_report.json'
)
openhands_remote_report_jsonl = args.input_file.replace(
'.jsonl', '.swebench_eval.jsonl'
)
if os.path.exists(swebench_official_report_json):
output_md_filepath = os.path.join(dirname, 'README.md')
with open(swebench_official_report_json, 'r') as f:
report = json.load(f)
# Convert instance IDs from "repo/name:pr-123" format to "repo__name-123" format
def convert_instance_id(instance_id):
"""Convert instance ID from slash/colon-pr format to double underscore/dash format."""
if '/' in instance_id and ':pr-' in instance_id:
# Split on '/' and ':pr-'
parts = instance_id.split('/')
if len(parts) == 2:
repo_part = parts[0]
name_and_pr = parts[1]
if ':pr-' in name_and_pr:
name, pr_number = name_and_pr.split(':pr-')
return f'{repo_part}__{name}-{pr_number}'
return instance_id
# Convert all instance ID lists in the report
for key in [
'resolved_ids',
'unresolved_ids',
'error_ids',
'empty_patch_ids',
'incomplete_ids',
]:
if key in report:
report[key] = [
convert_instance_id(instance_id) for instance_id in report[key]
]
output_md = (
'# Multi-SWE-bench Report\n'
'This folder contains the evaluation results of the SWE-bench using the [official evaluation docker containerization](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level).\n\n'
'## Summary\n'
f'- total instances: {report["total_instances"]}\n'
f'- submitted instances: {report["submitted_instances"]}\n'
f'- completed instances: {report["completed_instances"]}\n'
f'- empty patch instances: {report["empty_patch_instances"]}\n'
f'- resolved instances: {report["resolved_instances"]}\n'
f'- unresolved instances: {report["unresolved_instances"]}\n'
f'- error instances: {report["error_instances"]}\n'
)
output_md += '\n## Resolved Instances\n'
# instance_id to status
for instance_id in report['resolved_ids']:
instance_id_to_status[instance_id]['resolved'] = True
output_md += (
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
)
output_md += '\n## Unresolved Instances\n'
for instance_id in report['unresolved_ids']:
output_md += (
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
)
output_md += '\n## Error Instances\n'
for instance_id in report['error_ids']:
instance_id_to_status[instance_id]['error_eval'] = True
output_md += (
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
)
output_md += '\n## Empty Patch Instances\n'
for instance_id in report['empty_patch_ids']:
instance_id_to_status[instance_id]['empty_generation'] = True
output_md += (
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
)
output_md += '\n## Incomplete Instances\n'
for instance_id in report['incomplete_ids']:
output_md += (
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
)
with open(output_md_filepath, 'w') as f:
f.write(output_md)
else:
print(
f'No report file found: Both {swebench_official_report_json} and {openhands_remote_report_jsonl} do not exist.'
)
exit()
# Before backup and update, check if any changes would be made (unless --force is used)
if not args.force:
needs_update = False
with open(args.input_file, 'r') as infile:
for line in tqdm(infile, desc='Checking for changes'):
data = json.loads(line)
instance_id = data['instance_id']
current_report = data.get('report', {})
new_report = instance_id_to_status[
instance_id
] # if no report, it's not resolved
if current_report != new_report:
needs_update = True
break
if not needs_update:
print('No updates detected. Skipping file update.')
exit()
else:
print('Force flag enabled. Updating all reports regardless of changes.')
# Backup and update the original file row by row
if os.path.exists(args.input_file + '.bak'):
if args.overwrite_backup:
print(
'Existing backup file found. Overwriting automatically due to --overwrite-backup flag.'
)
os.remove(args.input_file + '.bak')
else:
conf = input('Existing backup file found. Do you want to overwrite it? (y/n)')
if conf != 'y':
exit()
os.remove(args.input_file + '.bak')
os.rename(args.input_file, args.input_file + '.bak')
# Process and write file row by row
with (
open(args.input_file + '.bak', 'r') as infile,
open(args.input_file, 'w') as outfile,
):
for line in tqdm(infile, desc='Updating output file'):
data = json.loads(line)
instance_id = data['instance_id']
data['report'] = instance_id_to_status[instance_id]
outfile.write(json.dumps(data) + '\n')

View File

@@ -0,0 +1,146 @@
#!/bin/bash
# NOTE: this script is for rolling out the Multi-SWE-Gym dataset for **TRAINING**
# For more information, please refer to
# 1. the Github Repo: https://github.com/SWE-Gym/SWE-Gym
# 2. the paper: https://arxiv.org/abs/2412.21139
MODEL=$1 # eg your llm config name in config.toml (eg: "llm.claude-3-5-sonnet-20241022-t05")
EXP_NAME=$2 # "train-t05"
EVAL_DATASET=$3 # path to original dataset (jsonl file)
N_WORKERS=${4:-64}
N_RUNS=${5:-1}
export EXP_NAME=$EXP_NAME
# use 2x resources for rollout since some codebases are pretty resource-intensive
export DEFAULT_RUNTIME_RESOURCE_FACTOR=2
echo "MODEL: $MODEL"
echo "EXP_NAME: $EXP_NAME"
echo "EVAL_DATASET: $EVAL_DATASET"
# Generate DATASET path by adding _with_runtime_ before .jsonl extension
DATASET="${EVAL_DATASET%.jsonl}_with_runtime_.jsonl" # path to converted dataset
# Create the converted dataset file
echo "Creating converted dataset at: $DATASET"
poetry run python ./evaluation/benchmarks/multi_swe_bench/scripts/data/data_change.py --input "$EVAL_DATASET" --output "$DATASET"
SPLIT="train"
export LANGUAGE=java
if [ -z "$ALLHANDS_API_KEY" ] || [ "$RUNTIME" != "remote" ]; then
echo "ALLHANDS_API_KEY is not set or RUNTIME is not set to remote. Will rollout and evaluate locally using Docker. WARNING: A large value of N_WORKERS will result in a large number of Docker containers being spun up and may crash your machine."
export RUNTIME=docker
else
echo "ALLHANDS_API_KEY is set and RUNTIME is set to remote. Continuing rollout and evaluation with remote runtime..."
export SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev"
fi
#EVAL_LIMIT=3000
MAX_ITER=100
# ===== Run inference =====
source "evaluation/utils/version_control.sh"
get_openhands_version
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
echo "DATASET: $DATASET"
echo "EVAL_DOCKER_IMAGE_PREFIX: $EVAL_DOCKER_IMAGE_PREFIX"
# Default to NOT use Hint
export USE_INSTANCE_IMAGE=true
export USE_HINT_TEXT=false
export RUN_WITH_BROWSING=false
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
EVAL_NOTE="$OPENHANDS_VERSION-no-hint-$EXP_NAME"
function run_eval() {
local eval_note=$1
export LANGUAGE=java
echo "About to run command"
COMMAND="EVAL_DOCKER_IMAGE_PREFIX=$EVAL_DOCKER_IMAGE_PREFIX; LANGUAGE=java;
poetry run python evaluation/benchmarks/multi_swe_bench/run_infer.py \
--agent-cls CodeActAgent \
--llm-config $MODEL \
--max-iterations $MAX_ITER \
--eval-num-workers $N_WORKERS \
--eval-note $eval_note \
--dataset $DATASET \
--split $SPLIT"
echo "Running command: $COMMAND"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
}
for run_idx in $(seq 1 $N_RUNS); do
while true; do
echo "### Running inference... ###"
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
current_eval_note="$EVAL_NOTE-run_$run_idx"
echo "EVAL_NOTE: $current_eval_note"
echo "DATASET command: $DATASET"
#INFER_OUTPUT=$(run_eval $current_eval_note)
INFER_OUTPUT=$(run_eval $current_eval_note | tee /dev/stderr)
INFER_STATUS=$? # Capture the exit status of run_infer.sh
echo "INFER_STATUS: $INFER_STATUS"
echo "### Cleaning up remote runtime... ###"
./evaluation/utils/scripts/cleanup_remote_runtime.sh
if [ $INFER_STATUS -eq 0 ]; then
echo "### Inference completed successfully. ###"
break
else
echo "### Inference failed with exit code $INFER_STATUS. Retrying... ###"
fi
done
# Extract the output directory using the special delimiters
OUTPUT_FILE=$(echo "$INFER_OUTPUT" | grep -o '### OUTPUT FILE:.* ###' | sed 's/### OUTPUT FILE: \(.*\) ###/\1/')
echo "Got OUTPUT_FILE: $OUTPUT_FILE"
while true; do
echo "### Evaluating on $OUTPUT_FILE ... ###"
OUTPUT_CONFIG_FILE="${OUTPUT_FILE%.jsonl}_config.json"
export EVAL_SKIP_BUILD_ERRORS=true
pip install multi-swe-bench --quiet --disable-pip-version-check > /dev/null 2>&1
COMMAND="poetry run python ./evaluation/benchmarks/multi_swe_bench/scripts/eval/update_multi_swe_bench_config.py --input $OUTPUT_FILE --output $OUTPUT_CONFIG_FILE --dataset $EVAL_DATASET;
python -m multi_swe_bench.harness.run_evaluation --config $OUTPUT_CONFIG_FILE
"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
echo "Running command: $COMMAND"
# Run the command
eval $COMMAND
EVAL_STATUS=$?
if [ $EVAL_STATUS -eq 0 ]; then
echo "### Evaluation completed successfully. ###"
break
else
echo "### Evaluation failed with exit code $EVAL_STATUS. Retrying... ###"
fi
./evaluation/utils/scripts/cleanup_remote_runtime.sh
done
# update the output with evaluation results
echo "### Updating the output with evaluation results... ###"
poetry run python evaluation/benchmarks/multi_swe_bench/scripts/eval/update_output_with_eval.py $OUTPUT_FILE
echo "### Combining the final completions... ###"
poetry run python evaluation/benchmarks/multi_swe_bench/scripts/eval/combine_final_completions.py $OUTPUT_FILE
echo "### DONE for run $run_idx! ###"
echo "You can find the final output at $(dirname $OUTPUT_FILE)/$FINAL_OUTPUT_FILE"
done

View File

@@ -47,8 +47,8 @@ if [ -z "$DATASET" ]; then
fi
if [ -z "$LANGUAGE" ]; then
echo "LANUGUAGE not specified, use default python"
LANGUAGE="python"
echo "LANGUAGE not specified, use default python"
LANGUAGE="java"
fi
if [ -z "$SPLIT" ]; then
@@ -69,10 +69,10 @@ fi
if [ -z "$EVAL_DOCKER_IMAGE_PREFIX" ]; then
if [ "$LANGUAGE" = "python" ]; then
echo "EVAL_DOCKER_IMAGE_PREFIX is docker.io/xingyaoww/ as default as LANUGUAGE is python"
echo "EVAL_DOCKER_IMAGE_PREFIX is docker.io/xingyaoww/ as default as LANGUAGE is python"
EVAL_DOCKER_IMAGE_PREFIX="docker.io/xingyaoww/"
elif [ "$LANGUAGE" = "java" ]; then
echo "EVAL_DOCKER_IMAGE_PREFIX is java_verified as LANUGUAGE is java"
echo "EVAL_DOCKER_IMAGE_PREFIX is empty as LANGUAGE is java"
EVAL_DOCKER_IMAGE_PREFIX=""
fi
fi

View File

@@ -0,0 +1,344 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"import pandas as pd\n",
"from tqdm import tqdm\n",
"\n",
"tqdm.pandas()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 1. Load raw data and convert to training data"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import gzip\n",
"import json\n",
"\n",
"from tqdm import tqdm\n",
"\n",
"FILE_PATHS = [\n",
" 'YOURPATH-no-hint-train-t05-run_1/output.with_completions.jsonl.gz',\n",
" 'YOURPATH-no-hint-train-t05-run_2/output.with_completions.jsonl.gz',\n",
"]\n",
"\n",
"# More memory efficient for large files\n",
"# Initialize lists to store the data\n",
"data = []\n",
"\n",
"\n",
"# Read file line by line\n",
"for FILE_PATH in FILE_PATHS:\n",
" with gzip.open(FILE_PATH, 'rb') as f: # Use 'rb' for gzipped files\n",
" for i, line in tqdm(\n",
" enumerate(f), desc=f'Processing {FILE_PATH.split(\"/\")[-1]}'\n",
" ):\n",
" # Parse only the fields we need\n",
" raw_data = json.loads(line)\n",
" data.append(\n",
" {\n",
" 'resolved': raw_data['report']['resolved'],\n",
" 'messages': raw_data['raw_completions']['messages']\n",
" if raw_data['raw_completions'] is not None\n",
" else None,\n",
" 'git_patch': raw_data['test_result'].get('git_patch', ''),\n",
" 'tools': raw_data['raw_completions']['tools']\n",
" if raw_data['raw_completions'] is not None\n",
" and 'tools' in raw_data['raw_completions']\n",
" else None,\n",
" }\n",
" )\n",
"\n",
"# Convert to DataFrame after collecting all data\n",
"df = pd.DataFrame(data)\n",
"print(f'#total amount of data={len(df)}')\n",
"df = df[~df['messages'].isna()]\n",
"print(f'#total amount of data after removing nan={len(df)}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Filter"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def _contains_multiple_tool_calls(messages: list[dict]) -> bool:\n",
" return any(\n",
" message.get('tool_calls') and len(message['tool_calls']) > 1\n",
" for message in messages\n",
" )\n",
"\n",
"\n",
"df['contains_multiple_tool_calls'] = df['messages'].apply(_contains_multiple_tool_calls)\n",
"display(df.groupby(['contains_multiple_tool_calls'])['resolved'].sum())"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"import copy\n",
"\n",
"# Convert function calling messages to non-function calling messages\n",
"from openhands.llm.fn_call_converter import (\n",
" FunctionCallConversionError,\n",
" convert_fncall_messages_to_non_fncall_messages,\n",
" convert_from_multiple_tool_calls_to_single_tool_call_messages,\n",
")\n",
"\n",
"total_failed = 0\n",
"\n",
"\n",
"def _convert_messages(messages: list[dict], tools: list[dict]) -> list[dict]:\n",
" global total_failed\n",
" message_copy = copy.deepcopy(messages)\n",
" for message in message_copy:\n",
" if message['content'] is None:\n",
" message['content'] = ''\n",
" try:\n",
" return convert_fncall_messages_to_non_fncall_messages(\n",
" message_copy, tools, add_in_context_learning_example=False\n",
" )\n",
" except FunctionCallConversionError:\n",
" total_failed += 1\n",
" # print(f'Failed to convert messages: {messages}\\nTools: {tools}')\n",
" # traceback.print_exc()\n",
" return None\n",
"\n",
"\n",
"df['converted_messages'] = df.apply(\n",
" lambda row: convert_from_multiple_tool_calls_to_single_tool_call_messages(\n",
" row['messages'], ignore_final_tool_result=True\n",
" ),\n",
" axis=1,\n",
")\n",
"df['nonfncall_messages'] = df.apply(\n",
" lambda row: _convert_messages(row['converted_messages'], row['tools']), axis=1\n",
")\n",
"print('total nan', df['nonfncall_messages'].isna().sum())\n",
"df = df[~df['nonfncall_messages'].isna()]\n",
"print(df['nonfncall_messages'].iloc[0])\n",
"\n",
"print(f'Total failed: {total_failed}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Tokenization"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from pandarallel import pandarallel\n",
"from transformers import AutoTokenizer\n",
"\n",
"os.environ['TOKENIZERS_PARALLELISM'] = 'false'\n",
"pandarallel.initialize(progress_bar=True, verbose=1, nb_workers=16)\n",
"tokenizer = AutoTokenizer.from_pretrained('Qwen/Qwen2.5-7B-Instruct')\n",
"\n",
"\n",
"def clean_messages(messages):\n",
" clean = []\n",
" for msg in messages:\n",
" if not isinstance(msg, dict):\n",
" continue\n",
" role = msg.get('role')\n",
" content = msg.get('content')\n",
" if isinstance(content, str):\n",
" text = content\n",
" elif isinstance(content, dict):\n",
" text = content.get('text')\n",
" elif (\n",
" isinstance(content, list)\n",
" and len(content) == 1\n",
" and isinstance(content[0], dict)\n",
" ):\n",
" text = content[0].get('text')\n",
" else:\n",
" print(f'Format not accepted {content}')\n",
" clean.append({'role': role, 'content': text})\n",
" return clean\n",
"\n",
"\n",
"# Step 1: Clean the messages\n",
"df['nonfncall_messages'] = df['nonfncall_messages'].apply(clean_messages)\n",
"\n",
"# Step 2: Compute token count\n",
"df['n_tokens'] = df['nonfncall_messages'].parallel_apply(\n",
" lambda x: len(tokenizer.apply_chat_template(x))\n",
")\n",
"\n",
"# print(df['nonfncall_messages'])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(f'BEFORE: #total={len(df)}')\n",
"df_selected = df[df['n_tokens'] < 131072]\n",
"print(f'AFTER(truncated to 128k): #total={len(df_selected)}')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df_selected['n_tokens'].describe()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# ecdf of n_tokens\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"\n",
"display(df.groupby(['resolved'])['n_tokens'].describe())\n",
"sns.ecdfplot(x='n_tokens', data=df, hue='resolved')\n",
"plt.show()\n",
"\n",
"print(f'#total={len(df)}')\n",
"df_selected = df[df['n_tokens'] < 131072]\n",
"print(f'#selected={len(df_selected)}')\n",
"display(df_selected.groupby(['resolved'])['n_tokens'].describe())\n",
"sns.ecdfplot(x='n_tokens', data=df_selected, hue='resolved')\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df_selected[~df_selected['resolved']]['n_tokens'].describe()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df_selected['resolved'].value_counts()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df_selected.groupby(['resolved'])['n_tokens'].describe()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Save Resolved Messages for SFT"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Flatten messages and change format to {\"content\": \"\", \"role\": \"\"}\n",
"df_selected[df_selected['resolved']][['nonfncall_messages']].rename(\n",
" columns={'nonfncall_messages': 'messages'}\n",
").to_json(\n",
" os.path.join(\n",
" 'PATH_TO_FILE',\n",
" f'policy_traj_128k_swegym_{df_selected[\"resolved\"].value_counts()[True]}i.jsonl',\n",
" ),\n",
" lines=True,\n",
" orient='records',\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.11"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -47,6 +47,8 @@ from openhands.core.config import (
get_agent_config_arg,
get_evaluation_parser,
get_llm_config_arg,
get_llms_for_routing_config,
get_model_routing_config_arg,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
@@ -244,6 +246,11 @@ def get_config(
# get 'draft_editor' config if exists
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
model_routing_config = get_model_routing_config_arg()
model_routing_config.llms_for_routing = (
get_llms_for_routing_config()
) # Populate with LLMs for routing from config.toml file
agent_config = AgentConfig(
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
@@ -251,8 +258,10 @@ def get_config(
enable_mcp=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
model_routing=model_routing_config,
)
config.set_agent_config(agent_config)
return config

View File

@@ -0,0 +1,81 @@
# SWE-Perf Evaluation
This folder contains the OpenHands inference generation of the [SWE-Perf benchmark](https://swe-perf.github.io/) ([paper](https://arxiv.org/pdf/2507.12415v1)).
The evaluation consists of three steps:
1. Environment setup: [install python environment](../../README.md#development-environment) and [configure LLM config](../../README.md#configure-openhands-and-your-llm).
2. [Run inference](#running-inference-locally-with-docker): Generate a edit patch for each Github issue
3. [Evaluate patches](#evaluate-generated-patches)
## Setup Environment and LLM Configuration
Please follow instruction [here](../../README.md#setup) to setup your local development environment and LLM.
## Running inference Locally with Docker
Make sure your Docker daemon is running, and you have ample disk space (at least 200-500GB, depends on the SWE-PErf set you are running on) for the instance-level docker image.
When the `run_infer.sh` script is started, it will automatically pull the relevant SWE-Perf images.
For example, for instance ID `scikit-learn_scikit-learn-11674`, it will try to pull our pre-build docker image `betty1202/sweb.eval.x86_64.scikit-learn_s_scikit-learn-11674` from DockerHub.
This image will be used create an OpenHands runtime image where the agent will operate on.
```bash
./evaluation/benchmarks/swe_perf/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split] [n_runs] [mode]
# Example
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 500 100 1 SWE-Perf/SWE-Perf test
```
where `model_config` is mandatory, and the rest are optional.
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
LLM settings, as defined in your `config.toml`.
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version you would
like to evaluate. It could also be a release tag like `0.6.2`.
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
to `CodeActAgent`.
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances. By
default, the script evaluates the entire SWE-Perf test set (140 issues). Note:
in order to use `eval_limit`, you must also set `agent`.
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
default, it is set to 100.
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
default, it is set to 1.
- `dataset`, a huggingface dataset name. e.g. `SWE-Perf/SWE-Perf`, specifies which dataset to evaluate on.
- `dataset_split`, split for the huggingface dataset. e.g., `test`, `dev`. Default to `test`.
- `n_runs`, e.g. `3`, is the number of times to run the evaluation. Default is 1.
- `mode`, e.g. `swt`, `swt-ci`, or `swe`, specifies the evaluation mode. Default is `swe`.
> [!CAUTION]
> Setting `num_workers` larger than 1 is not officially tested, YMMV.
Let's say you'd like to run 10 instances using `llm.eval_gpt4_1106_preview` and CodeActAgent,
then your command would be:
```bash
./evaluation/benchmarks/swe_bench/scripts/run_infer.sh llm.eval_gpt4_1106_preview HEAD CodeActAgent 10
```
## Evaluate Generated Patches
To evaluate the generated patch, follow these steps:
### 1. Convert output to the evaluation standard format
Run the following command:
```bash
python -m evaluation.benchmarks.swe_perf.format_conversion \
--input_path [input_path] \
--output_path [output_path]
```
* `input_path`: Path to the raw generated patch file.
* `output_path`: Path where the converted file will be saved.
### 2. Run the SWE-Perf benchmark official evaluation
Once the output is converted, use the [official SWE-Perf benchmark evaluation](https://github.com/SWE-Perf/SWE-Perf/tree/main/evaluation) to evaluate it.

View File

@@ -0,0 +1,52 @@
"""
Utilities for handling binary files and patch generation in SWE-Perf evaluation.
"""
def remove_binary_diffs(patch_text):
"""
Remove binary file diffs from a git patch.
Args:
patch_text (str): The git patch text
Returns:
str: The cleaned patch text with binary diffs removed
"""
lines = patch_text.splitlines()
cleaned_lines = []
block = []
is_binary_block = False
for line in lines:
if line.startswith('diff --git '):
if block and not is_binary_block:
cleaned_lines.extend(block)
block = [line]
is_binary_block = False
elif 'Binary files' in line:
is_binary_block = True
block.append(line)
else:
block.append(line)
if block and not is_binary_block:
cleaned_lines.extend(block)
return '\n'.join(cleaned_lines)
def remove_binary_files_from_git():
"""
Generate a bash command to remove binary files from git staging.
Returns:
str: A bash command that removes binary files from git staging
"""
return """
for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do
if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then
git rm -f "$file" 2>/dev/null || rm -f "$file"
echo "Removed: $file"
fi
done
""".strip()

View File

@@ -0,0 +1,45 @@
import json
import os
from argparse import ArgumentParser
parser = ArgumentParser()
parser.add_argument('--input_path', type=str, help='Name of input path to JSON file.')
parser.add_argument('--output_path', type=str, help='Name of output path to JSON file.')
args = parser.parse_args()
input_path = args.input_path
output_path = args.output_path
os.makedirs(output_path, exist_ok=True)
def load_jsonl(file_path):
"""Load JSONL file into a list of dictionaries."""
data = []
with open(file_path, 'r') as f:
for line in f:
data.append(json.loads(line))
return data
dataset = load_jsonl(input_path)
ooutput_dataset = []
for data in dataset:
instance_id = data['instance_id']
model_name_or_path = 'openhands'
model_patch = (
data['test_result']['git_patch']
if 'test_result' in data and 'git_patch' in data['test_result']
else None
)
ooutput_dataset.append(
{
'instance_id': instance_id,
'model_name_or_path': model_name_or_path,
'model_patch': model_patch,
}
)
with open(os.path.join(output_path, 'output.jsonl'), 'w') as f:
for item in ooutput_dataset:
json_line = json.dumps(item, ensure_ascii=False)
f.write(json_line + '\n')

View File

@@ -0,0 +1,39 @@
"""Mapping instance_id to resource_factor.
Different instances may have different resource requirements.
e.g., some instances may require more memory/CPU to run inference.
This file tracks the resource requirements of different instances.
"""
import json
import os
from openhands.core.logger import openhands_logger as logger
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_RUNTIME_RESOURCE_FACTOR = int(
os.environ.get('DEFAULT_RUNTIME_RESOURCE_FACTOR', 1)
)
# dataset to resource mapping
_global_resource_mapping: dict[str, dict[str, float]] = {}
def get_resource_mapping(dataset_name: str) -> dict[str, float]:
if dataset_name not in _global_resource_mapping:
file_path = os.path.join(CUR_DIR, f'{dataset_name}.json')
if not os.path.exists(file_path):
logger.info(f'Resource mapping for {dataset_name} not found.')
return None
with open(file_path, 'r') as f:
_global_resource_mapping[dataset_name] = json.load(f)
logger.debug(f'Loaded resource mapping for {dataset_name}')
return _global_resource_mapping[dataset_name]
def get_instance_resource_factor(dataset_name: str, instance_id: str) -> int:
resource_mapping = get_resource_mapping(dataset_name)
if resource_mapping is None:
return DEFAULT_RUNTIME_RESOURCE_FACTOR
return int(resource_mapping.get(instance_id, DEFAULT_RUNTIME_RESOURCE_FACTOR))

View File

@@ -0,0 +1,842 @@
# Based on https://github.com/logic-star-ai/swt-bench/blob/master/src/constants.py
# Constants - Installation Specifications
MAP_VERSION_TO_INSTALL_SKLEARN = {
k: {
'python': '3.6',
'packages': 'numpy scipy cython pytest pandas matplotlib',
'install': 'python -m pip install -v --no-use-pep517 --no-build-isolation -e .',
'pip_packages': [
'cython',
'numpy==1.19.2',
'setuptools',
'scipy==1.5.2',
],
}
for k in ['0.20', '0.21', '0.22']
}
MAP_VERSION_TO_INSTALL_SKLEARN.update(
{
k: {
'python': '3.9',
'packages': "'numpy==1.19.2' 'scipy==1.5.2' 'cython==3.0.10' pytest 'pandas<2.0.0' 'matplotlib<3.9.0' setuptools pytest joblib threadpoolctl",
'install': 'python -m pip install -v --no-use-pep517 --no-build-isolation -e .',
'pip_packages': ['cython', 'setuptools', 'numpy', 'scipy'],
}
for k in ['1.3', '1.4']
}
)
MAP_VERSION_TO_INSTALL_FLASK = {
'2.0': {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pip_packages': [
'setuptools==70.0.0',
'Werkzeug==2.3.7',
'Jinja2==3.0.1',
'itsdangerous==2.1.2',
'click==8.0.1',
'MarkupSafe==2.1.3',
],
},
'2.1': {
'python': '3.10',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pip_packages': [
'click==8.1.3',
'itsdangerous==2.1.2',
'Jinja2==3.1.2',
'MarkupSafe==2.1.1',
'Werkzeug==2.3.7',
],
},
}
MAP_VERSION_TO_INSTALL_FLASK.update(
{
k: {
'python': '3.11',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pip_packages': [
'click==8.1.3',
'itsdangerous==2.1.2',
'Jinja2==3.1.2',
'MarkupSafe==2.1.1',
'Werkzeug==2.3.7',
],
}
for k in ['2.2', '2.3']
}
)
MAP_VERSION_TO_INSTALL_DJANGO = {
k: {
'python': '3.5',
'packages': 'requirements.txt',
'pre_install': [
'apt-get update && apt-get install -y locales',
"echo 'en_US UTF-8' > /etc/locale.gen",
'locale-gen en_US.UTF-8',
],
'install': 'python setup.py install',
'pip_packages': ['setuptools'],
'eval_commands': [
'export LANG=en_US.UTF-8',
'export LC_ALL=en_US.UTF-8',
'export PYTHONIOENCODING=utf8',
'export LANGUAGE=en_US:en',
],
}
for k in ['1.7', '1.8', '1.9', '1.10', '1.11', '2.0', '2.1', '2.2']
}
MAP_VERSION_TO_INSTALL_DJANGO.update(
{
k: {'python': '3.5', 'install': 'python setup.py install'}
for k in ['1.4', '1.5', '1.6']
}
)
MAP_VERSION_TO_INSTALL_DJANGO.update(
{
k: {
'python': '3.6',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'eval_commands': [
"sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen",
'export LANG=en_US.UTF-8',
'export LANGUAGE=en_US:en',
'export LC_ALL=en_US.UTF-8',
],
}
for k in ['3.0', '3.1', '3.2']
}
)
MAP_VERSION_TO_INSTALL_DJANGO.update(
{
k: {
'python': '3.8',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
}
for k in ['4.0']
}
)
MAP_VERSION_TO_INSTALL_DJANGO.update(
{
k: {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
}
for k in ['4.1', '4.2']
}
)
MAP_VERSION_TO_INSTALL_DJANGO.update(
{
k: {
'python': '3.11',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
}
for k in ['5.0']
}
)
MAP_VERSION_TO_INSTALL_REQUESTS = {
k: {'python': '3.9', 'packages': 'pytest', 'install': 'python -m pip install .'}
for k in ['0.7', '0.8', '0.9', '0.11', '0.13', '0.14', '1.1', '1.2', '2.0', '2.2']
+ ['2.3', '2.4', '2.5', '2.7', '2.8', '2.9', '2.10', '2.11', '2.12', '2.17']
+ ['2.18', '2.19', '2.22', '2.26', '2.25', '2.27', '3.0']
}
MAP_VERSION_TO_INSTALL_SEABORN = {
k: {
'python': '3.9',
'install': 'python -m pip install -e .',
'pip_packages': [
'contourpy==1.1.0',
'cycler==0.11.0',
'fonttools==4.42.1',
'importlib-resources==6.0.1',
'kiwisolver==1.4.5',
'matplotlib==3.7.2',
'numpy==1.25.2',
'packaging==23.1',
'pandas==1.3.5', # 2.0.3
'pillow==10.0.0',
'pyparsing==3.0.9',
'pytest',
'python-dateutil==2.8.2',
'pytz==2023.3.post1',
'scipy==1.11.2',
'six==1.16.0',
'tzdata==2023.1',
'zipp==3.16.2',
],
}
for k in ['0.11']
}
MAP_VERSION_TO_INSTALL_SEABORN.update(
{
k: {
'python': '3.9',
'install': 'python -m pip install -e .[dev]',
'pip_packages': [
'contourpy==1.1.0',
'cycler==0.11.0',
'fonttools==4.42.1',
'importlib-resources==6.0.1',
'kiwisolver==1.4.5',
'matplotlib==3.7.2',
'numpy==1.25.2',
'packaging==23.1',
'pandas==2.0.0',
'pillow==10.0.0',
'pyparsing==3.0.9',
'pytest',
'python-dateutil==2.8.2',
'pytz==2023.3.post1',
'scipy==1.11.2',
'six==1.16.0',
'tzdata==2023.1',
'zipp==3.16.2',
],
}
for k in ['0.12', '0.13']
}
)
MAP_VERSION_TO_INSTALL_PYTEST = {
k: {'python': '3.9', 'install': 'python -m pip install -e .'}
for k in [
'4.4',
'4.5',
'4.6',
'5.0',
'5.1',
'5.2',
'5.3',
'5.4',
'6.0',
'6.2',
'6.3',
'7.0',
'7.1',
'7.2',
'7.4',
'8.0',
]
}
MAP_VERSION_TO_INSTALL_PYTEST['4.4']['pip_packages'] = [
'atomicwrites==1.4.1',
'attrs==23.1.0',
'more-itertools==10.1.0',
'pluggy==0.13.1',
'py==1.11.0',
'setuptools==68.0.0',
'six==1.16.0',
]
MAP_VERSION_TO_INSTALL_PYTEST['4.5']['pip_packages'] = [
'atomicwrites==1.4.1',
'attrs==23.1.0',
'more-itertools==10.1.0',
'pluggy==0.11.0',
'py==1.11.0',
'setuptools==68.0.0',
'six==1.16.0',
'wcwidth==0.2.6',
]
MAP_VERSION_TO_INSTALL_PYTEST['4.6']['pip_packages'] = [
'atomicwrites==1.4.1',
'attrs==23.1.0',
'more-itertools==10.1.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
'six==1.16.0',
'wcwidth==0.2.6',
]
for k in ['5.0', '5.1', '5.2']:
MAP_VERSION_TO_INSTALL_PYTEST[k]['pip_packages'] = [
'atomicwrites==1.4.1',
'attrs==23.1.0',
'more-itertools==10.1.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
'wcwidth==0.2.6',
]
MAP_VERSION_TO_INSTALL_PYTEST['5.3']['pip_packages'] = [
'attrs==23.1.0',
'more-itertools==10.1.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
'wcwidth==0.2.6',
]
MAP_VERSION_TO_INSTALL_PYTEST['5.4']['pip_packages'] = [
'py==1.11.0',
'packaging==23.1',
'attrs==23.1.0',
'more-itertools==10.1.0',
'pluggy==0.13.1',
]
MAP_VERSION_TO_INSTALL_PYTEST['6.0']['pip_packages'] = [
'attrs==23.1.0',
'iniconfig==2.0.0',
'more-itertools==10.1.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
'toml==0.10.2',
]
for k in ['6.2', '6.3']:
MAP_VERSION_TO_INSTALL_PYTEST[k]['pip_packages'] = [
'attrs==23.1.0',
'iniconfig==2.0.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
'toml==0.10.2',
]
MAP_VERSION_TO_INSTALL_PYTEST['7.0']['pip_packages'] = [
'attrs==23.1.0',
'iniconfig==2.0.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
]
for k in ['7.1', '7.2']:
MAP_VERSION_TO_INSTALL_PYTEST[k]['pip_packages'] = [
'attrs==23.1.0',
'iniconfig==2.0.0',
'packaging==23.1',
'pluggy==0.13.1',
'py==1.11.0',
'tomli==2.0.1',
]
MAP_VERSION_TO_INSTALL_PYTEST['7.4']['pip_packages'] = [
'iniconfig==2.0.0',
'packaging==23.1',
'pluggy==1.3.0',
'exceptiongroup==1.1.3',
'tomli==2.0.1',
]
MAP_VERSION_TO_INSTALL_PYTEST['8.0']['pip_packages'] = [
'iniconfig==2.0.0',
'packaging==23.1',
'pluggy==1.3.0',
'exceptiongroup==1.1.3',
'tomli==2.0.1',
]
MAP_VERSION_TO_INSTALL_MATPLOTLIB = {
k: {
'python': '3.11',
'packages': 'environment.yml',
'install': 'python -m pip install -e .',
'pre_install': [
'apt-get -y update && apt-get -y upgrade && apt-get install -y imagemagick ffmpeg texlive texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-luatex cm-super dvipng'
],
'pip_packages': [
'contourpy==1.1.0',
'cycler==0.11.0',
'fonttools==4.42.1',
'ghostscript',
'kiwisolver==1.4.5',
'numpy==1.25.2',
'packaging==23.1',
'pillow==10.0.0',
'pikepdf',
'pyparsing==3.0.9',
'python-dateutil==2.8.2',
'six==1.16.0',
'setuptools==68.1.2',
'setuptools-scm==7.1.0',
'typing-extensions==4.7.1',
],
}
for k in ['3.5', '3.6', '3.7']
}
MAP_VERSION_TO_INSTALL_MATPLOTLIB.update(
{
k: {
'python': '3.8',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pre_install': [
'apt-get -y update && apt-get -y upgrade && apt-get install -y imagemagick ffmpeg libfreetype6-dev pkg-config texlive texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-luatex cm-super'
],
'pip_packages': ['pytest', 'ipython'],
}
for k in ['3.1', '3.2', '3.3', '3.4']
}
)
MAP_VERSION_TO_INSTALL_MATPLOTLIB.update(
{
k: {
'python': '3.7',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pre_install': [
'apt-get -y update && apt-get -y upgrade && apt-get install -y imagemagick ffmpeg libfreetype6-dev pkg-config'
],
'pip_packages': ['pytest'],
}
for k in ['3.0']
}
)
MAP_VERSION_TO_INSTALL_MATPLOTLIB.update(
{
k: {
'python': '3.5',
'install': 'python setup.py build; python setup.py install',
'pre_install': [
'apt-get -y update && apt-get -y upgrade && && apt-get install -y imagemagick ffmpeg'
],
'pip_packages': ['pytest'],
'execute_test_as_nonroot': True,
}
for k in ['2.0', '2.1', '2.2', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5']
}
)
MAP_VERSION_TO_INSTALL_SPHINX = {
k: {
'python': '3.9',
'pip_packages': ['tox==4.16.0', 'tox-current-env==0.0.11'],
'install': 'python -m pip install -e .[test]',
'pre_install': ["sed -i 's/pytest/pytest -rA/' tox.ini"],
}
for k in ['1.5', '1.6', '1.7', '1.8', '2.0', '2.1', '2.2', '2.3', '2.4', '3.0']
+ ['3.1', '3.2', '3.3', '3.4', '3.5', '4.0', '4.1', '4.2', '4.3', '4.4']
+ ['4.5', '5.0', '5.1', '5.2', '5.3', '6.0', '6.2', '7.0', '7.1', '7.2']
}
for k in ['3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '4.0', '4.1', '4.2', '4.3', '4.4']:
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
[
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
]
)
if k in ['4.2', '4.3', '4.4']:
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
[
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py",
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py",
]
)
elif k == '4.1':
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
[
(
"grep -q 'sphinxcontrib-htmlhelp>=2.0.0' setup.py && "
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py || "
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py"
),
(
"grep -q 'sphinxcontrib-serializinghtml>=1.1.5' setup.py && "
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py || "
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py"
),
]
)
else:
MAP_VERSION_TO_INSTALL_SPHINX[k]['pre_install'].extend(
[
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py",
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py",
]
)
MAP_VERSION_TO_INSTALL_SPHINX['7.2']['pre_install'] += [
'apt-get update && apt-get install -y graphviz'
]
MAP_VERSION_TO_INSTALL_ASTROPY = {
k: {
'python': '3.9',
'install': 'python -m pip install -e .[test] --verbose',
'pip_packages': [
'attrs==23.1.0',
'exceptiongroup==1.1.3',
'execnet==2.0.2',
'hypothesis==6.82.6',
'iniconfig==2.0.0',
'numpy==1.25.2',
'packaging==23.1',
'pluggy==1.3.0',
'psutil==5.9.5',
'pyerfa==2.0.0.3',
'pytest-arraydiff==0.5.0',
'pytest-astropy-header==0.2.2',
'pytest-astropy==0.10.0',
'pytest-cov==4.1.0',
'pytest-doctestplus==1.0.0',
'pytest-filter-subpackage==0.1.2',
'pytest-mock==3.11.1',
'pytest-openfiles==0.5.0',
'pytest-remotedata==0.4.0',
'pytest-xdist==3.3.1',
'pytest==7.4.0',
'PyYAML==6.0.1',
'setuptools==68.0.0',
'sortedcontainers==2.4.0',
'tomli==2.0.1',
],
}
for k in ['0.1', '0.2', '0.3', '0.4', '1.1', '1.2', '1.3', '3.0', '3.1', '3.2']
+ ['4.1', '4.2', '4.3', '5.0', '5.1', '5.2']
}
for k in ['4.1', '4.2', '4.3', '5.0', '5.1', '5.2']:
MAP_VERSION_TO_INSTALL_ASTROPY[k]['pre_install'] = [
'sed -i \'s/requires = \\["setuptools",/requires = \\["setuptools==68.0.0",/\' pyproject.toml'
]
MAP_VERSION_TO_INSTALL_SYMPY = {
k: {
'python': '3.9',
'packages': 'mpmath flake8',
'pip_packages': ['mpmath==1.3.0', 'flake8-comprehensions'],
'install': 'python -m pip install -e .',
}
for k in ['0.7', '1.0', '1.1', '1.10', '1.11', '1.12', '1.2', '1.4', '1.5', '1.6']
+ ['1.7', '1.8', '1.9']
}
MAP_VERSION_TO_INSTALL_SYMPY.update(
{
k: {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pip_packages': ['mpmath==1.3.0'],
}
for k in ['1.13']
}
)
MAP_VERSION_TO_INSTALL_PYLINT = {
k: {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
}
for k in [
'2.10',
'2.11',
'2.13',
'2.14',
'2.15',
'2.16',
'2.17',
'2.8',
'2.9',
'3.0',
]
}
MAP_VERSION_TO_INSTALL_PYLINT['2.8']['pip_packages'] = ['pyenchant==3.2']
MAP_VERSION_TO_INSTALL_PYLINT['2.8']['pre_install'] = [
'apt-get update && apt-get install -y libenchant-2-dev hunspell-en-us'
]
MAP_VERSION_TO_INSTALL_PYLINT.update(
{
k: {
**MAP_VERSION_TO_INSTALL_PYLINT[k],
'pip_packages': ['astroid==3.0.0a6', 'setuptools'],
}
for k in ['3.0']
}
)
MAP_VERSION_TO_INSTALL_XARRAY = {
k: {
'python': '3.10',
'packages': 'environment.yml',
'install': 'python -m pip install -e .',
'pip_packages': [
'numpy==1.23.0',
'packaging==23.1',
'pandas==1.5.3',
'pytest==7.4.0',
'python-dateutil==2.8.2',
'pytz==2023.3',
'six==1.16.0',
'scipy==1.11.1',
'setuptools==68.0.0',
],
'no_use_env': True,
}
for k in ['0.12', '0.18', '0.19', '0.20', '2022.03', '2022.06', '2022.09']
}
MAP_VERSION_TO_INSTALL_SQLFLUFF = {
k: {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
}
for k in [
'0.10',
'0.11',
'0.12',
'0.13',
'0.4',
'0.5',
'0.6',
'0.8',
'0.9',
'1.0',
'1.1',
'1.2',
'1.3',
'1.4',
'2.0',
'2.1',
'2.2',
]
}
MAP_VERSION_TO_INSTALL_DBT_CORE = {
k: {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
}
for k in [
'0.13',
'0.14',
'0.15',
'0.16',
'0.17',
'0.18',
'0.19',
'0.20',
'0.21',
'1.0',
'1.1',
'1.2',
'1.3',
'1.4',
'1.5',
'1.6',
'1.7',
]
}
MAP_VERSION_TO_INSTALL_PYVISTA = {
k: {
'python': '3.9',
'install': 'python -m pip install -e .',
'pip_packages': ['pytest'],
}
for k in ['0.20', '0.21', '0.22', '0.23']
}
MAP_VERSION_TO_INSTALL_PYVISTA.update(
{
k: {
'python': '3.9',
'packages': 'requirements.txt',
'install': 'python -m pip install -e .',
'pip_packages': ['pytest'],
}
for k in [
'0.24',
'0.25',
'0.26',
'0.27',
'0.28',
'0.29',
'0.30',
'0.31',
'0.32',
'0.33',
'0.34',
'0.35',
'0.36',
'0.37',
'0.38',
'0.39',
'0.40',
'0.41',
'0.42',
'0.43',
]
}
)
MAP_VERSION_TO_INSTALL_ASTROID = {
k: {
'python': '3.9',
'install': 'python -m pip install -e .',
'pip_packages': ['pytest'],
}
for k in [
'2.10',
'2.12',
'2.13',
'2.14',
'2.15',
'2.16',
'2.5',
'2.6',
'2.7',
'2.8',
'2.9',
'3.0',
]
}
MAP_VERSION_TO_INSTALL_MARSHMALLOW = {
k: {
'python': '3.9',
'install': "python -m pip install -e '.[dev]'",
}
for k in [
'2.18',
'2.19',
'2.20',
'3.0',
'3.1',
'3.10',
'3.11',
'3.12',
'3.13',
'3.15',
'3.16',
'3.19',
'3.2',
'3.4',
'3.8',
'3.9',
]
}
MAP_VERSION_TO_INSTALL_PVLIB = {
k: {
'python': '3.9',
'install': 'python -m pip install -e .[all]',
'packages': 'pandas scipy',
'pip_packages': ['jupyter', 'ipython', 'matplotlib', 'pytest', 'flake8'],
}
for k in ['0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9']
}
MAP_VERSION_TO_INSTALL_PYDICOM = {
k: {'python': '3.6', 'install': 'python -m pip install -e .', 'packages': 'numpy'}
for k in [
'1.0',
'1.1',
'1.2',
'1.3',
'1.4',
'2.0',
'2.1',
'2.2',
'2.3',
'2.4',
'3.0',
]
}
MAP_VERSION_TO_INSTALL_PYDICOM.update(
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.8'} for k in ['1.4', '2.0']}
)
MAP_VERSION_TO_INSTALL_PYDICOM.update(
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.9'} for k in ['2.1', '2.2']}
)
MAP_VERSION_TO_INSTALL_PYDICOM.update(
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.10'} for k in ['2.3']}
)
MAP_VERSION_TO_INSTALL_PYDICOM.update(
{k: {**MAP_VERSION_TO_INSTALL_PYDICOM[k], 'python': '3.11'} for k in ['2.4', '3.0']}
)
MAP_VERSION_TO_INSTALL_HUMANEVAL = {k: {'python': '3.9'} for k in ['1.0']}
MAP_VERSION_TO_INSTALL_HUMANEVAL_FIX = {
k: {'python': '3.10', 'packages': 'pytest'} for k in ['0.0.1']
}
# Constants - Task Instance Instllation Environment
MAP_VERSION_TO_INSTALL = {
'astropy/astropy': MAP_VERSION_TO_INSTALL_ASTROPY,
'dbt-labs/dbt-core': MAP_VERSION_TO_INSTALL_DBT_CORE,
'django/django': MAP_VERSION_TO_INSTALL_DJANGO,
'matplotlib/matplotlib': MAP_VERSION_TO_INSTALL_MATPLOTLIB,
'marshmallow-code/marshmallow': MAP_VERSION_TO_INSTALL_MARSHMALLOW,
'mwaskom/seaborn': MAP_VERSION_TO_INSTALL_SEABORN,
'pallets/flask': MAP_VERSION_TO_INSTALL_FLASK,
'psf/requests': MAP_VERSION_TO_INSTALL_REQUESTS,
'pvlib/pvlib-python': MAP_VERSION_TO_INSTALL_PVLIB,
'pydata/xarray': MAP_VERSION_TO_INSTALL_XARRAY,
'pydicom/pydicom': MAP_VERSION_TO_INSTALL_PYDICOM,
'pylint-dev/astroid': MAP_VERSION_TO_INSTALL_ASTROID,
'pylint-dev/pylint': MAP_VERSION_TO_INSTALL_PYLINT,
'pytest-dev/pytest': MAP_VERSION_TO_INSTALL_PYTEST,
'pyvista/pyvista': MAP_VERSION_TO_INSTALL_PYVISTA,
'scikit-learn/scikit-learn': MAP_VERSION_TO_INSTALL_SKLEARN,
'sphinx-doc/sphinx': MAP_VERSION_TO_INSTALL_SPHINX,
'sqlfluff/sqlfluff': MAP_VERSION_TO_INSTALL_SQLFLUFF,
'swe-bench/humaneval': MAP_VERSION_TO_INSTALL_HUMANEVAL,
'nielstron/humaneval_fix': MAP_VERSION_TO_INSTALL_HUMANEVAL_FIX,
'sympy/sympy': MAP_VERSION_TO_INSTALL_SYMPY,
}
# Constants - Repository Specific Installation Instructions
MAP_REPO_TO_INSTALL = {}
# Constants - Task Instance Test Frameworks
TEST_PYTEST_VERBOSE = 'pytest -rA --tb=long -p no:cacheprovider'
MAP_REPO_TO_TEST_FRAMEWORK_VERBOSE = {
'astropy/astropy': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_ASTROPY.keys()
},
'django/django': {
k: './tests/runtests.py --verbosity 2 --settings=test_sqlite --parallel 1'
for k in MAP_VERSION_TO_INSTALL_DJANGO.keys()
},
'marshmallow-code/marshmallow': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_MARSHMALLOW.keys()
},
'matplotlib/matplotlib': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_MATPLOTLIB.keys()
},
'mwaskom/seaborn': {
k: 'pytest -rA --tb=long' for k in MAP_VERSION_TO_INSTALL_SEABORN.keys()
},
'pallets/flask': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_FLASK.keys()
},
'psf/requests': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_REQUESTS.keys()
},
'pvlib/pvlib-python': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PVLIB.keys()
},
'pydata/xarray': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_XARRAY.keys()
},
'pydicom/pydicom': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PYDICOM.keys()
},
'pylint-dev/astroid': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_ASTROID.keys()
},
'pylint-dev/pylint': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PYLINT.keys()
},
'pytest-dev/pytest': {
k: 'pytest -rA --tb=long' for k in MAP_VERSION_TO_INSTALL_PYTEST.keys()
},
'pyvista/pyvista': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_PYVISTA.keys()
},
'scikit-learn/scikit-learn': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_SKLEARN.keys()
},
'sphinx-doc/sphinx': {
k: 'tox -epy39 -v --' for k in MAP_VERSION_TO_INSTALL_SPHINX.keys()
},
'sqlfluff/sqlfluff': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_SQLFLUFF.keys()
},
'swe-bench/humaneval': {
k: 'python' for k in MAP_VERSION_TO_INSTALL_HUMANEVAL.keys()
},
'nielstron/humaneval_fix': {
k: TEST_PYTEST_VERBOSE for k in MAP_VERSION_TO_INSTALL_HUMANEVAL.keys()
},
'sympy/sympy': {
k: 'bin/test -C --verbose' for k in MAP_VERSION_TO_INSTALL_SYMPY.keys()
},
}
MAP_REPO_TO_TEST_FRAMEWORK_VERBOSE['django/django']['1.9'] = (
'./tests/runtests.py --verbosity 2'
)

View File

@@ -0,0 +1,978 @@
import asyncio
import copy
import json
import os
import tempfile
from typing import Any, Literal
import pandas as pd
import toml
from datasets import load_dataset
import openhands.agenthub
from evaluation.benchmarks.swe_perf.binary_patch_utils import (
remove_binary_diffs,
remove_binary_files_from_git,
)
from evaluation.benchmarks.swe_perf.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.benchmarks.swe_perf.resource.swt_bench_constants import (
MAP_REPO_TO_INSTALL,
MAP_VERSION_TO_INSTALL,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
assert_and_raise,
check_maximum_retries_exceeded,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.critic import AgentFinishedCritic
from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileReadObservation,
)
from openhands.events.serialization.event import event_from_dict, event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
ENABLE_LLM_EDITOR = os.environ.get('ENABLE_LLM_EDITOR', 'false').lower() == 'true'
BenchMode = Literal['swe', 'swt', 'swt-ci']
# Global variable to track dataset type
DATASET_TYPE = 'SWE-Perf'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
def _get_sweperf_workspace_dir_name(instance: pd.Series) -> str:
return f'{instance.repo}__{instance.version}'.replace('/', '__')
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
workspace_dir_name = _get_sweperf_workspace_dir_name(instance)
# The instruction
instruction = f"""
<uploaded_files>
/workspace/{workspace_dir_name}
</uploaded_files>
I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:
<issue_description>
{instance.problem_statement_realistic}
</issue_description>
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{workspace_dir_name} directory to ensure the <issue_description> is satisfied.
Follow these phases to resolve the issue:
## ⚙️ Phase 1: Understand the Problem & Test Reuse
**1.1. Install the package locally:**
```bash
python -m pip install pyinstrument
python -m pip install -e .
```
> Only proceed to README-based install if the above fails.
**1.2. Identify relevant modules and logic:**
* Use test cases mentioned in `<issue_description>` to locate the functions and files involved.
* Focus on potential performance bottlenecks: loops, I/O, locks, cache access, data structures, etc.
**1.3. Run initial benchmark:**
```bash
pytest -rA --durations=0 --disable-warnings -p no:warnings --tb=no <test_case>
```
## 📊 Phase 2: Localization (Hierarchical Bottleneck Detection)
**2.1. Global profiling using `pyinstrument`:**
```bash
pyinstrument -m pytest -rA --durations=0 --disable-warnings --tb=no --continue-on-collection-errors -p no:warnings <test_case>
```
**2.2. Analyze performance stack if necessary:**
* 🔍 **Module level**: Identify hot files and methods.
* 🔬 **Function level**: Focus on top-consuming classes/functions.
* 🧬 **Line level**: Add fine-grained sampling/logging if needed.
**2.3. Output a layered summary** showing where time is spent and why.
## 🧠 Phase 3: Repair (Design Candidate Fixes)
**3.1. Propose multiple optimization ideas:**
* Algorithm refinement
* Data structure improvement
* Parallelism / async
* Caching / batching
**3.2. For each candidate:**
* Describe the idea using pseudocode or `diff`
* Evaluate expected gain vs implementation complexity
---
## 🔬 Phase 4: Patch Validation (Quantitative Benchmarking)
**4.1. Apply each patch separately**
**4.2. Re-run benchmark using the same test case**
**4.3. Record improvements in pytest `call` phase**
**4.4. Build a comparison table:**
| Patch | Call Time (s) | % Improvement | Notes |
| ----- | ------------- | ------------- | ----- |
**4.5. Select the best-performing, low-risk patch**
---
## 📌 Phase 5: Fix Analysis (Explain the Fix)
**5.1.** Clearly describe the **problem**
**5.2.** Identify the **exact location** in the code
**5.3.** Describe **how the test reveals** the problem
**5.4.** State **best practices** relevant to the fix
**5.5.** Explain **how the fix resolves** the issue
---
## 🧩 Phase 6: Fix Implementation
**6.1.** Implement the selected fix in the non-test source files
**6.2.** Ensure the changes are minimal and performance-focused
---
## ✅ Phase 7: Final Verification & Review
**7.1.** Run **full regression tests** to ensure correctness
**7.2.** Re-benchmark performance and compare with base commit `{instance['base_commit']}`
**7.3.** Ensure `call` time is significantly improved in `pytest` output
**7.4.** If gain is limited, consider deeper optimization
**7.5.** Write a concise final report including:
* Bottleneck path
* Fix alternatives considered
* Final solution and metrics
* Code diff summary
## Important ##
1. Please do not optimize specifically for certain test cases, as this may be considered cheating.
2. Delete all files unrelated to optimization, including temporary files, auxiliary test code, and so on.
3. Avoid excessive optimization and unnecessary divergence; if the improvement is not significant, stop promptly to maintain efficiency and focus.
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
"""
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\nYou SHOULD NEVER attempt to browse the web. </IMPORTANT!>\n'
)
if 'image_assets' in instance:
assets = json.loads(instance['image_assets'])
assert 'problem_statement' in assets, (
'problem_statement is required in image_assets'
)
image_urls = assets['problem_statement']
return MessageAction(content=instruction, image_urls=image_urls)
return MessageAction(content=instruction)
def get_instance_docker_image(
instance_id: str,
) -> str:
docker_image_prefix = 'docker.io/betty1202/'
image_name = 'sweb.eval.x86_64.' + instance_id
image_name = image_name.replace(
'__', '_s_'
) # to comply with docker image naming convention
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> OpenHandsConfig:
base_container_image = get_instance_docker_image(
instance['instance_id'],
)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
sandbox_config.use_host_network = False
# Add platform to the sandbox config to solve issue 4401
sandbox_config.platform = 'linux/amd64'
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
)
# get 'draft_editor' config if exists
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
agent_config = AgentConfig(
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=ENABLE_LLM_EDITOR,
enable_mcp=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
metadata: EvalMetadata,
):
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Initialization Fn')
logger.info('-' * 30)
workspace_dir_name = _get_sweperf_workspace_dir_name(instance)
obs: CmdOutputObservation
# Set instance id and git configuration
action = CmdRunAction(
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc && git config --global core.pager "" && git config --global diff.binary false"""
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to export SWE_INSTANCE_ID and configure git: {str(obs)}',
)
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to export USER: {str(obs)}')
# inject the init script
script_dir = os.path.dirname(__file__)
# inject the instance info
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to create /swe_util/eval_data/instances: {str(obs)}',
)
swe_instance_json_name = 'swe-perf-instance.json'
with tempfile.TemporaryDirectory() as temp_dir:
# Construct the full path for the desired file name within the temporary directory
temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
# Write to the file with the desired name within the temporary directory
with open(temp_file_path, 'w') as f:
if not isinstance(instance, dict):
json.dump([instance.to_dict()], f)
else:
json.dump([instance], f)
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
# inject the instance swe entry
entry_script_path = 'instance_swe_entry.sh'
runtime.copy_to(
str(os.path.join(script_dir, f'scripts/setup/{entry_script_path}')),
'/swe_util/',
)
action = CmdRunAction(command='cat ~/.bashrc')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source ~/.bashrc')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(obs, ErrorObservation):
logger.error(f'Failed to source ~/.bashrc: {str(obs)}')
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
action = CmdRunAction(command=f'source /swe_util/{entry_script_path}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to source /swe_util/{entry_script_path}: {str(obs)}',
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git reset --hard')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to git reset --hard: {str(obs)}')
action = CmdRunAction(
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
if metadata.details['mode'] == 'swt-ci':
# set up repo
setup_commands = []
if instance['repo'] in MAP_REPO_TO_INSTALL:
setup_commands.append(MAP_REPO_TO_INSTALL[instance['repo']])
# Run pre-install set up if provided
install = MAP_VERSION_TO_INSTALL.get(instance['repo'], {}).get(
instance['version'], []
)
if 'pre_install' in install:
for pre_install in install['pre_install']:
setup_commands.append(pre_install)
if 'install' in install:
setup_commands.append(install['install'])
for command in setup_commands:
action = CmdRunAction(command=command)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
action = CmdRunAction(command='which python')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0 and 'testbed' in obs.content,
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
)
logger.info('-' * 30)
logger.info('END Runtime Initialization Fn')
logger.info('-' * 30)
def complete_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
) -> dict[str, Any]:
"""Complete the runtime for the agent.
This function is called before the runtime is used to run the agent.
If you need to do something in the sandbox to get the correctness metric after
the agent has run, modify this function.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Completion Fn')
logger.info('-' * 30)
obs: CmdOutputObservation
workspace_dir_name = _get_sweperf_workspace_dir_name(instance)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to kill it...')
action = CmdRunAction(command='C-c')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to ctrl+z it...')
action = CmdRunAction(command='C-z')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git config --global core.pager ""')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git config --global core.pager "": {str(obs)}',
)
# First check for any git repositories in subdirectories
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to find git repositories: {str(obs)}',
)
git_dirs = [p for p in obs.content.strip().split('\n') if p]
if git_dirs:
# Remove all .git directories in subdirectories
for git_dir in git_dirs:
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove git directory {git_dir}: {str(obs)}',
)
# add all files
action = CmdRunAction(command='git add -A')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git add -A: {str(obs)}',
)
# Remove binary files from git staging
action = CmdRunAction(command=remove_binary_files_from_git())
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove binary files: {str(obs)}',
)
n_retries = 0
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]} > patch.diff'
)
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
n_retries += 1
if isinstance(obs, CmdOutputObservation):
if obs.exit_code == 0:
# Read the patch file
action = FileReadAction(path='patch.diff')
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(obs, FileReadObservation):
git_patch = obs.content
break
elif isinstance(obs, ErrorObservation):
# Fall back to cat "patch.diff" to get the patch
assert 'File could not be decoded as utf-8' in obs.content
action = CmdRunAction(command='cat patch.diff')
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation) and obs.exit_code == 0
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
git_patch = obs.content
break
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
else:
logger.info('Failed to get git diff, retrying...')
sleep_if_should_continue(10)
elif isinstance(obs, ErrorObservation):
logger.error(f'Error occurred: {obs.content}. Retrying...')
sleep_if_should_continue(10)
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
# Remove binary diffs from the patch
git_patch = remove_binary_diffs(git_patch)
logger.info('-' * 30)
logger.info('END Runtime Completion Fn')
logger.info('-' * 30)
return {'git_patch': git_patch}
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
runtime_failure_count: int = 0,
) -> EvalOutput:
config = get_config(instance, metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
# Increase resource_factor with increasing attempt_id
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
8,
)
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance, metadata)
message_action = get_instruction(instance, metadata)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=message_action,
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
# if fatal error, throw EvalError to trigger re-run
if is_fatal_evaluation_error(state.last_error):
raise EvalException('Fatal error detected: ' + state.last_error)
# Get git patch
complete_runtime_fn = complete_runtime
return_val = complete_runtime_fn(runtime, instance)
git_patch = return_val['git_patch']
logger.info(
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
)
finally:
runtime.close()
# ==========================================
# ======= Attempt to evaluate the agent's edits =======
# we use eval_infer.sh to evaluate the agent's edits, not here
# because the agent may alter the environment / testcases
test_result = {
'git_patch': git_patch,
}
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = get_metrics(state)
# Save the output
instruction = message_action.content
if message_action.image_urls:
instruction += (
'\n\n<image_urls>' + '\n'.join(message_action.image_urls) + '</image_urls>'
)
output = EvalOutput(
instance_id=instance.instance_id,
instruction=instruction,
instance=instance.to_dict(), # SWE Bench specific
test_result=test_result,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
)
return output
def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
if os.path.exists(file_path):
with open(file_path, 'r') as file:
data = toml.load(file)
if 'selected_ids' in data:
selected_ids = data['selected_ids']
logger.info(
f'Filtering {len(selected_ids)} tasks from "selected_ids"...'
)
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
if 'selected_repos' in data:
selected_repos = data['selected_repos']
if isinstance(selected_repos, str):
selected_repos = [selected_repos]
assert isinstance(selected_repos, list)
logger.info(
f'Filtering {selected_repos} tasks from "selected_repos"...'
)
subset = dataset[dataset['repo'].isin(selected_repos)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
return dataset[~dataset[filter_column].isin(skip_ids)]
return dataset
if __name__ == '__main__':
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
default='SWE-Perf/SWE-Perf',
help='data set to evaluate on, either full-test or lite-test',
)
parser.add_argument(
'--split',
type=str,
default='test',
help='split to evaluate on',
)
parser.add_argument(
'--mode',
type=str,
default='swe',
choices=['swe', 'swt', 'swt-ci'],
help="mode to run the evaluation, either 'swe', 'swt', or 'swt-ci'",
)
args, _ = parser.parse_known_args()
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
# so we don't need to manage file uploading to OpenHands's repo
dataset = load_dataset(args.dataset, split=args.split)
swe_perf_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
logger.info(
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_perf_tests)} tasks'
)
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
llm_config.log_completions = True
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
llm_config.modify_params = False
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
# Get condenser config from environment variable
condenser_name = os.environ.get('EVAL_CONDENSER')
if condenser_name:
condenser_config = get_condenser_config_arg(condenser_name)
if condenser_config is None:
raise ValueError(
f'Could not find Condenser config: EVAL_CONDENSER={condenser_name}'
)
else:
# If no specific condenser config is provided via env var, default to NoOpCondenser
condenser_config = NoOpCondenserConfig()
logger.debug(
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
)
details = {'mode': args.mode}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
dataset_descrption = (
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
)
metadata = make_metadata(
llm_config,
dataset_descrption,
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
details=details,
condenser_config=condenser_config,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
# Run evaluation in iterative mode:
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
ITERATIVE_EVAL_MODE = (
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
)
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
)
if not ITERATIVE_EVAL_MODE:
# load the dataset
instances = prepare_dataset(swe_perf_tests, output_file, args.eval_n_limit)
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
else:
critic = AgentFinishedCritic()
def get_cur_output_file_path(attempt: int) -> str:
return (
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
)
eval_ids = None
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
cur_output_file = get_cur_output_file_path(attempt)
logger.info(
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
)
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
# so hopefully we get slightly different results
if attempt > 1 and metadata.llm_config.temperature == 0:
logger.info(
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
)
metadata.llm_config.temperature = 0.1
# Load instances - at first attempt, we evaluate all instances
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
instances = prepare_dataset(
swe_perf_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
)
# Run evaluation - but save them to cur_output_file
logger.info(
f'Evaluating {len(instances)} instances for attempt {attempt}...'
)
run_evaluation(
instances,
metadata,
cur_output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
# When eval is done, we update eval_ids to the instances that failed the current attempt
instances_failed = []
logger.info(
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
)
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
try:
history = [
event_from_dict(event) for event in instance['history']
]
critic_result = critic.evaluate(
history, instance['test_result'].get('git_patch', '')
)
if not critic_result.success:
instances_failed.append(instance['instance_id'])
except Exception as e:
logger.error(
f'Error loading history for instance {instance["instance_id"]}: {e}'
)
instances_failed.append(instance['instance_id'])
logger.info(
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
)
eval_ids = instances_failed
# If no instances failed, we break
if len(instances_failed) == 0:
break
# Then we should aggregate the results from all attempts into the original output file
# and remove the intermediate files
logger.info(
'Aggregating results from all attempts into the original output file...'
)
fout = open(output_file, 'w')
added_instance_ids = set()
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
cur_output_file = get_cur_output_file_path(attempt)
if not os.path.exists(cur_output_file):
logger.warning(
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
)
continue
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
if (
instance['instance_id'] not in added_instance_ids
and instance['test_result'].get('git_patch', '').strip()
):
fout.write(line)
added_instance_ids.add(instance['instance_id'])
logger.info(
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
)
fout.close()
logger.info(
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
)
# Check if any instances reached maximum retries
check_maximum_retries_exceeded(metadata.eval_output_dir)

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
MAX_ITER=$5
NUM_WORKERS=$6
DATASET=$7
SPLIT=$8
N_RUNS=$9
MODE=${10}
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
fi
if [ -z "$RUN_WITH_BROWSING" ]; then
echo "RUN_WITH_BROWSING not specified, use default false"
RUN_WITH_BROWSING=false
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default SWE-Perf/SWE-Perf"
DATASET="SWE-Perf/SWE-Perf"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
fi
if [ -z "$MODE" ]; then
MODE="swe"
echo "MODE not specified, use default $MODE"
fi
if [ -n "$EVAL_CONDENSER" ]; then
echo "Using Condenser Config: $EVAL_CONDENSER"
else
echo "No Condenser Config provided via EVAL_CONDENSER, use default (NoOpCondenser)."
fi
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
get_openhands_version
echo "AGENT: $AGENT"
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
echo "DATASET: $DATASET"
echo "SPLIT: $SPLIT"
echo "MAX_ITER: $MAX_ITER"
echo "NUM_WORKERS: $NUM_WORKERS"
echo "COMMIT_HASH: $COMMIT_HASH"
echo "MODE: $MODE"
echo "EVAL_CONDENSER: $EVAL_CONDENSER"
# Default to NOT use Hint
if [ -z "$USE_HINT_TEXT" ]; then
export USE_HINT_TEXT=false
fi
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
EVAL_NOTE="$OPENHANDS_VERSION"
# if not using Hint, add -no-hint to the eval note
if [ "$USE_HINT_TEXT" = false ]; then
EVAL_NOTE="$EVAL_NOTE-no-hint"
fi
if [ "$RUN_WITH_BROWSING" = true ]; then
EVAL_NOTE="$EVAL_NOTE-with-browsing"
fi
if [ -n "$EXP_NAME" ]; then
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
fi
# if mode != swe, add mode to the eval note
if [ "$MODE" != "swe" ]; then
EVAL_NOTE="${EVAL_NOTE}-${MODE}"
fi
# Add condenser config to eval note if provided
if [ -n "$EVAL_CONDENSER" ]; then
EVAL_NOTE="${EVAL_NOTE}-${EVAL_CONDENSER}"
fi
function run_eval() {
local eval_note="${1}"
COMMAND="poetry run python evaluation/benchmarks/swe_perf/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations $MAX_ITER \
--eval-num-workers $NUM_WORKERS \
--eval-note $eval_note \
--dataset $DATASET \
--split $SPLIT \
--mode $MODE"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
}
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
if [ -z "$N_RUNS" ]; then
N_RUNS=1
echo "N_RUNS not specified, use default $N_RUNS"
fi
# Skip runs if the run number is in the SKIP_RUNS list
# read from env variable SKIP_RUNS as a comma separated list of run numbers
SKIP_RUNS=(${SKIP_RUNS//,/ })
for i in $(seq 1 $N_RUNS); do
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
echo "Skipping run $i"
continue
fi
current_eval_note="$EVAL_NOTE-run_$i"
echo "EVAL_NOTE: $current_eval_note"
run_eval $current_eval_note
done
checkout_original_branch

View File

@@ -0,0 +1,54 @@
"""This script compares gold patches with OpenHands-generated patches and check whether
OpenHands found the right (set of) files to modify.
"""
import argparse
import json
import re
def extract_modified_files(patch):
modified_files = set()
file_pattern = re.compile(r'^diff --git a/(.*?) b/')
for line in patch.split('\n'):
match = file_pattern.match(line)
if match:
modified_files.add(match.group(1))
return modified_files
def process_report(oh_output_file):
succ = 0
fail = 0
for line in open(oh_output_file):
line = json.loads(line)
instance_id = line['instance_id']
gold_patch = line['swe_instance']['patch']
generated_patch = line['git_patch']
gold_modified_files = extract_modified_files(gold_patch)
# swe-bench lite only: a gold patch always contains exactly one file
assert len(gold_modified_files) == 1
generated_modified_files = extract_modified_files(generated_patch)
# Check if all files in gold_patch are also in generated_patch
all_files_in_generated = gold_modified_files.issubset(generated_modified_files)
if all_files_in_generated:
succ += 1
else:
fail += 1
print(
f'{instance_id}: file mismatch, gold = {gold_modified_files}, generated = {generated_modified_files}'
)
print(
f'\nSUMMARY: {succ} out of {succ + fail} instances found correct files to edit, success rate = {succ / float(succ + fail)}'
)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--oh_output_file', help='Path to the OH output file')
args = parser.parse_args()
process_report(args.oh_output_file)

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
source ~/.bashrc
SWEUTIL_DIR=/swe_util
# FIXME: Cannot read SWE_INSTANCE_ID from the environment variable
# SWE_INSTANCE_ID=django__django-11099
if [ -z "$SWE_INSTANCE_ID" ]; then
echo "Error: SWE_INSTANCE_ID is not set." >&2
exit 1
fi
# Read the swe-bench-test-lite.json file and extract the required item based on instance_id
item=$(jq --arg INSTANCE_ID "$SWE_INSTANCE_ID" '.[] | select(.instance_id == $INSTANCE_ID)' $SWEUTIL_DIR/eval_data/instances/swe-bench-instance.json)
if [[ -z "$item" ]]; then
echo "No item found for the provided instance ID."
exit 1
fi
WORKSPACE_NAME=$(echo "$item" | jq -r '(.repo | tostring) + "__" + (.version | tostring) | gsub("/"; "__")')
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
# Clear the workspace
if [ -d /workspace ]; then
rm -rf /workspace/*
else
mkdir /workspace
fi
# Copy repo to workspace
if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
fi
mkdir -p /workspace
cp -r /testbed /workspace/$WORKSPACE_NAME
# Activate instance-specific environment
if [ -d /opt/miniconda3 ]; then
. /opt/miniconda3/etc/profile.d/conda.sh
conda activate testbed
fi

View File

@@ -1,8 +1,6 @@
# Run frontend checks
echo "Running frontend checks..."
cd frontend
npm run lint
npm run check-translation-completeness
npx lint-staged
# Run backend pre-commit

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import OpenHands from "#/api/open-hands";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import {
FILE_VARIANTS_1,
FILE_VARIANTS_2,
@@ -10,20 +10,20 @@ import {
* You can find the mock handlers in `frontend/src/mocks/file-service-handlers.ts`.
*/
describe("OpenHands File API", () => {
describe("ConversationService File API", () => {
it("should get a list of files", async () => {
await expect(OpenHands.getFiles("test-conversation-id")).resolves.toEqual(
FILE_VARIANTS_1,
);
await expect(
ConversationService.getFiles("test-conversation-id"),
).resolves.toEqual(FILE_VARIANTS_1);
await expect(
OpenHands.getFiles("test-conversation-id-2"),
ConversationService.getFiles("test-conversation-id-2"),
).resolves.toEqual(FILE_VARIANTS_2);
});
it("should get content of a file", async () => {
await expect(
OpenHands.getFile("test-conversation-id", "file1.txt"),
ConversationService.getFile("test-conversation-id", "file1.txt"),
).resolves.toEqual("Content of file1.txt");
});
});

View File

@@ -13,7 +13,8 @@ vi.mock("react-router", async () => {
vi.mock("#/context/conversation-context", () => ({
useConversation: () => ({ conversationId: "test-conversation-id" }),
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
ConversationProvider: ({ children }: { children: React.ReactNode }) =>
children,
}));
vi.mock("react-i18next", async () => {
@@ -29,21 +30,18 @@ vi.mock("react-i18next", async () => {
};
});
// Mock redux
const mockDispatch = vi.fn();
// Mock Zustand browser store
let mockBrowserState = {
url: "https://example.com",
screenshotSrc: "",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
vi.mock("react-redux", async () => {
const actual = await vi.importActual("react-redux");
return {
...actual,
useDispatch: () => mockDispatch,
useSelector: () => mockBrowserState,
};
});
vi.mock("#/stores/browser-store", () => ({
useBrowserStore: () => mockBrowserState,
}));
// Import the component after all mocks are set up
import { BrowserPanel } from "#/components/features/browser/browser";
@@ -55,6 +53,9 @@ describe("Browser", () => {
mockBrowserState = {
url: "https://example.com",
screenshotSrc: "",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
});
@@ -63,6 +64,9 @@ describe("Browser", () => {
mockBrowserState = {
url: "https://example.com",
screenshotSrc: "",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
render(<BrowserPanel />);
@@ -75,7 +79,11 @@ describe("Browser", () => {
// Set the mock state for this test
mockBrowserState = {
url: "https://example.com",
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
screenshotSrc:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
setUrl: vi.fn(),
setScreenshotSrc: vi.fn(),
reset: vi.fn(),
};
render(<BrowserPanel />);

View File

@@ -1,287 +0,0 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
import OpenHands from "#/api/open-hands";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
// Mock dependencies
vi.mock("posthog-js", () => ({
default: {
capture: vi.fn(),
},
}));
const { useSelectorMock } = vi.hoisted(() => ({
useSelectorMock: vi.fn(),
}));
vi.mock("react-redux", () => ({
useSelector: useSelectorMock,
}));
vi.mock("#/context/auth-context", () => ({
useAuth: vi.fn(),
}));
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
ACTION$PUSH_TO_BRANCH: "Push to Branch",
ACTION$PUSH_CREATE_PR: "Push & Create PR",
ACTION$PUSH_CHANGES_TO_PR: "Push Changes to PR",
};
return translations[key] || key;
},
}),
}));
vi.mock("react-router", () => ({
useParams: () => ({
conversationId: "test-conversation-id",
}),
}));
const renderActionSuggestions = () =>
render(<ActionSuggestions onSuggestionsClick={() => {}} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
describe("ActionSuggestions", () => {
// Setup mocks for each test
beforeEach(() => {
vi.clearAllMocks();
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
github: "some-token",
},
});
useSelectorMock.mockReturnValue({
selectedRepository: "test-repo",
});
});
it("should render both GitHub buttons when GitHub token is set and repository is selected", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
// @ts-expect-error - only required for testing
getConversationSpy.mockResolvedValue({
selected_repository: "test-repo",
});
renderActionSuggestions();
// Find all buttons with data-testid="suggestion"
const buttons = await screen.findAllByTestId("suggestion");
// Check if we have at least 2 buttons
expect(buttons.length).toBeGreaterThanOrEqual(2);
// Check if the buttons contain the expected text
const pushButton = buttons.find((button) =>
button.textContent?.includes("Push to Branch"),
);
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
expect(pushButton).toBeInTheDocument();
expect(prButton).toBeInTheDocument();
});
it("should not render buttons when GitHub token is not set", () => {
renderActionSuggestions();
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
});
it("should not render buttons when no repository is selected", () => {
useSelectorMock.mockReturnValue({
selectedRepository: null,
});
renderActionSuggestions();
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
});
it("should have different prompts for 'Push to Branch' and 'Push & Create PR' buttons", () => {
// This test verifies that the prompts are different in the component
renderActionSuggestions();
// Get the component instance to access the internal values
const pushBranchPrompt =
"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.";
const createPRPrompt =
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes. If a pull request template exists in the repository, please follow it when creating the PR description.";
// Verify the prompts are different
expect(pushBranchPrompt).not.toEqual(createPRPrompt);
// Verify the PR prompt mentions creating a meaningful branch name
expect(createPRPrompt).toContain("meaningful branch name");
expect(createPRPrompt).not.toContain("SAME branch name");
});
it("should use correct provider name based on conversation git_provider, not user authenticated providers", async () => {
// Test case for GitHub repository
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({
conversation_id: "test-github",
title: "GitHub Test",
selected_repository: "test-repo",
git_provider: "github",
selected_branch: "main",
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
});
// Mock user having both GitHub and Bitbucket tokens
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
github: "github-token",
bitbucket: "bitbucket-token",
},
});
const onSuggestionsClick = vi.fn();
render(<ActionSuggestions onSuggestionsClick={onSuggestionsClick} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const buttons = await screen.findAllByTestId("suggestion");
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
expect(prButton).toBeInTheDocument();
if (prButton) {
prButton.click();
}
// The suggestion should mention GitHub, not Bitbucket
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("GitHub")
);
expect(onSuggestionsClick).not.toHaveBeenCalledWith(
expect.stringContaining("Bitbucket")
);
});
it("should use GitLab terminology when git_provider is gitlab", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({
conversation_id: "test-gitlab",
title: "GitLab Test",
selected_repository: "test-repo",
git_provider: "gitlab",
selected_branch: "main",
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
});
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
gitlab: "gitlab-token",
},
});
const onSuggestionsClick = vi.fn();
render(<ActionSuggestions onSuggestionsClick={onSuggestionsClick} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const buttons = await screen.findAllByTestId("suggestion");
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
if (prButton) {
prButton.click();
}
// Should mention GitLab and "merge request" instead of "pull request"
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("GitLab")
);
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("merge request")
);
});
it("should use Bitbucket terminology when git_provider is bitbucket", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({
conversation_id: "test-bitbucket",
title: "Bitbucket Test",
selected_repository: "test-repo",
git_provider: "bitbucket",
selected_branch: "main",
last_updated_at: new Date().toISOString(),
created_at: new Date().toISOString(),
status: "RUNNING",
runtime_status: "STATUS$READY",
url: null,
session_api_key: null,
});
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: {
bitbucket: "bitbucket-token",
},
});
const onSuggestionsClick = vi.fn();
render(<ActionSuggestions onSuggestionsClick={onSuggestionsClick} />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
const buttons = await screen.findAllByTestId("suggestion");
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
if (prButton) {
prButton.click();
}
// Should mention Bitbucket
expect(onSuggestionsClick).toHaveBeenCalledWith(
expect.stringContaining("Bitbucket")
);
});
});

Some files were not shown because too many files have changed in this diff Show More