feat: sync Computer Use outputs into Open WebUI files#83
feat: sync Computer Use outputs into Open WebUI files#83belugaming wants to merge 1 commit intoYambr:mainfrom
Conversation
Sync final outputs into Open WebUI's native file store and surface relative /api/v1/files/<id>/content paths so public Open WebUI deployments no longer depend on Computer Use file URLs. Make the host data path and bind host configurable for compose users, and add coverage for sync behavior and Dockerfile packaging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis PR integrates Open WebUI-native output synchronization for the computer-use server. It replaces external token provider configuration with new OWUI settings, introduces a new sync module that uploads outputs to Open WebUI with MD5-based manifest caching, integrates sync calls into MCP tools via background threading, and updates system prompts to guide agent file path preferences. Docker and environment configurations are updated to support the feature. Changes
Sequence DiagramsequenceDiagram
participant Tool as MCP Tool<br/>(bash_tool, etc.)
participant Sync as Sync Module<br/>(owui_sync.py)
participant FS as File System<br/>(outputs/, manifest)
participant OWUI as Open WebUI<br/>Server
Tool->>Sync: _sync_outputs_if_configured(chat_id)
Sync->>FS: Check if outputs dir exists
FS-->>Sync: Directory found
Sync->>FS: Walk files, compute MD5
FS-->>Sync: Files + hashes
Sync->>FS: Load .owui_sync_manifest.json
FS-->>Sync: Manifest data
Sync->>Sync: Check MD5 against manifest
alt File unchanged
Sync-->>Sync: Reuse cached file_id
else File new or changed
Sync->>OWUI: POST /api/v1/files/ (multipart)
OWUI-->>Sync: file_id, content_url
Sync->>FS: Update manifest with new entry
end
Sync->>FS: Write updated manifest
Sync-->>Tool: Return synced_files metadata
Tool->>Tool: format_sync_summary(output, synced_files)
Tool-->>User: Formatted result + file links
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
computer-use-server/system_prompt.py (1)
707-712: Avoid exact full-line text matching for prompt patching.Line 709’s literal match is fragile; small wording/whitespace edits will silently drop the OWUI guidance. Prefer inserting the note at a structural anchor (for example before
</sharing_files>) with a single replacement.♻️ Suggested robust insertion pattern
- if OWUI_INTERNAL_URL: - result = result.replace( - " - Action: Copy completed files here and share as HTTP links", - " - Action: Copy completed files here and share as HTTP links\n" - " - If tool results include Open WebUI file paths like /api/v1/files/<id>/content, prefer the Open WebUI file paths returned in tool results for final deliverables", - ) + if OWUI_INTERNAL_URL: + marker = "</sharing_files>" + owui_note = ( + "- If tool results include Open WebUI file paths like /api/v1/files/<id>/content, " + "prefer the Open WebUI file paths returned in tool results for final deliverables\n" + ) + result = result.replace(marker, f"{owui_note}{marker}", 1)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@computer-use-server/system_prompt.py` around lines 707 - 712, The current fragile replacement uses a full-line literal match inside the result.replace call when OWUI_INTERNAL_URL is set; change it to perform a single, structural insertion before the closing sharing_files tag (e.g., locate the string "</sharing_files>" and insert the OWUI guidance immediately before it) instead of matching the exact line; update the code that references OWUI_INTERNAL_URL and result.replace to build the guidance text and call a single replace or insertion using the "</sharing_files>" anchor so minor wording/whitespace changes won't break the patch.computer-use-server/owui_sync.py (1)
100-103: Don’t swallow upload failures silently.Catching
Exceptionhere makes sync failures invisible, so partial or complete upload outages look like a successful run. At minimum, log the file path and exception before continuing.🪵 Suggested fix
+import logging + +logger = logging.getLogger(__name__) + def _upload_file(owui_url: str, owui_api_key: str, file_path: Path) -> tuple[str, str]: @@ try: file_id, url = _upload_file(owui_url, owui_api_key, file_path) - except Exception: + except Exception: + logger.exception("Failed to sync %s to Open WebUI", file_path) continue🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@computer-use-server/owui_sync.py` around lines 100 - 103, The try/except around the call to _upload_file is swallowing failures; change the except Exception: block to except Exception as e: and log the failure (including file_path and the exception) before continuing — e.g., use the module logger (logging.getLogger(__name__)) or the existing logger in this module to call logger.exception or logger.error with file_path and e; keep the continue after logging so the loop behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@computer-use-server/owui_sync.py`:
- Around line 19-20: The manifest path helper _manifest_path currently ignores
chat_id so manifests are shared across chats; update _manifest_path to accept a
chat_id (e.g., def _manifest_path(outputs_dir: Path, chat_id: str) -> Path) and
use chat_id to scope the manifest filename or subdirectory (for example include
chat_id in the filename or create outputs_dir / chat_id / _MANIFEST_NAME) to
ensure per-chat isolation; also apply the same change to the other locations
referenced (the cached file-ID read/write logic around the block noted "69-76")
so all file-ID manifests are created/loaded per chat_id rather than globally.
- Around line 84-112: The manifest and results currently use file_path.name,
which conflates files with the same basename; change to use the relative path
from outputs_dir as the manifest key and as the filename in results (e.g. key =
file_path.relative_to(outputs_dir).as_posix()). Update the lookup of manifest
(replace manifest.get(file_path.name) with manifest.get(key)), store the entry
under that key, and append results entries using that relative path; keep the
existing use of _md5(file_path) and _upload_file(owui_url, owui_api_key,
file_path) and preserve the rest of the logic (updated flag, synced_at, file_id,
url).
In `@tests/orchestrator/test_owui_sync.py`:
- Around line 210-217: The test may import a previously cached system_prompt
module so the patched fake_skill_manager/fake_docker_manager get ignored; before
importing, remove "system_prompt" from sys.modules (or use importlib.reload) to
force a fresh import, then import and clear system_prompt_module._render_cache
and call system_prompt_module.render_system_prompt_sync("abc123", None); update
the test around system_prompt_module to ensure the patched modules are used
deterministically.
---
Nitpick comments:
In `@computer-use-server/owui_sync.py`:
- Around line 100-103: The try/except around the call to _upload_file is
swallowing failures; change the except Exception: block to except Exception as
e: and log the failure (including file_path and the exception) before continuing
— e.g., use the module logger (logging.getLogger(__name__)) or the existing
logger in this module to call logger.exception or logger.error with file_path
and e; keep the continue after logging so the loop behavior is unchanged.
In `@computer-use-server/system_prompt.py`:
- Around line 707-712: The current fragile replacement uses a full-line literal
match inside the result.replace call when OWUI_INTERNAL_URL is set; change it to
perform a single, structural insertion before the closing sharing_files tag
(e.g., locate the string "</sharing_files>" and insert the OWUI guidance
immediately before it) instead of matching the exact line; update the code that
references OWUI_INTERNAL_URL and result.replace to build the guidance text and
call a single replace or insertion using the "</sharing_files>" anchor so minor
wording/whitespace changes won't break the patch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8a166b79-5b0e-4660-bb14-6ee973c2c628
📒 Files selected for processing (9)
.env.examplecomputer-use-server/Dockerfilecomputer-use-server/docker_manager.pycomputer-use-server/mcp_tools.pycomputer-use-server/owui_sync.pycomputer-use-server/system_prompt.pydocker-compose.ymltests/orchestrator/test_mcp_tools.pytests/orchestrator/test_owui_sync.py
| def _manifest_path(outputs_dir: Path) -> Path: | ||
| return outputs_dir / _MANIFEST_NAME |
There was a problem hiding this comment.
Keep the manifest scoped to a chat.
chat_id is currently discarded, so every chat sharing the same outputs_dir will reuse the same manifest and cached file IDs. That breaks the per-chat isolation described in the PR and can surface the wrong Open WebUI file in a later chat.
🔧 Suggested fix
-def _manifest_path(outputs_dir: Path) -> Path:
- return outputs_dir / _MANIFEST_NAME
+def _manifest_path(outputs_dir: Path, chat_id: str) -> Path:
+ return outputs_dir / chat_id / _MANIFEST_NAME
@@
def sync_outputs_to_owui(
chat_id: str,
@@
) -> List[dict]:
- del chat_id
outputs_dir = Path(outputs_dir)Also applies to: 69-76
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@computer-use-server/owui_sync.py` around lines 19 - 20, The manifest path
helper _manifest_path currently ignores chat_id so manifests are shared across
chats; update _manifest_path to accept a chat_id (e.g., def
_manifest_path(outputs_dir: Path, chat_id: str) -> Path) and use chat_id to
scope the manifest filename or subdirectory (for example include chat_id in the
filename or create outputs_dir / chat_id / _MANIFEST_NAME) to ensure per-chat
isolation; also apply the same change to the other locations referenced (the
cached file-ID read/write logic around the block noted "69-76") so all file-ID
manifests are created/loaded per chat_id rather than globally.
| for file_path in sorted(outputs_dir.rglob("*")): | ||
| if not file_path.is_file() or file_path.name.startswith("."): | ||
| continue | ||
|
|
||
| digest = _md5(file_path) | ||
| entry = manifest.get(file_path.name) | ||
| if entry and entry.get("md5") == digest and entry.get("file_id"): | ||
| results.append( | ||
| { | ||
| "filename": file_path.name, | ||
| "file_id": entry["file_id"], | ||
| "url": entry["url"], | ||
| } | ||
| ) | ||
| continue | ||
|
|
||
| try: | ||
| file_id, url = _upload_file(owui_url, owui_api_key, file_path) | ||
| except Exception: | ||
| continue | ||
|
|
||
| manifest[file_path.name] = { | ||
| "md5": digest, | ||
| "file_id": file_id, | ||
| "url": url, | ||
| "synced_at": time.time(), | ||
| } | ||
| updated = True | ||
| results.append({"filename": file_path.name, "file_id": file_id, "url": url}) |
There was a problem hiding this comment.
Use the relative path as the manifest key.
Keying the manifest and result list by file_path.name collapses distinct files that share a basename in different subdirectories. That can reuse the wrong file_id/content URL for a different file.
🔧 Suggested fix
for file_path in sorted(outputs_dir.rglob("*")):
if not file_path.is_file() or file_path.name.startswith("."):
continue
digest = _md5(file_path)
- entry = manifest.get(file_path.name)
+ relative_name = str(file_path.relative_to(outputs_dir))
+ entry = manifest.get(relative_name)
if entry and entry.get("md5") == digest and entry.get("file_id"):
results.append(
{
- "filename": file_path.name,
+ "filename": relative_name,
"file_id": entry["file_id"],
"url": entry["url"],
}
)
continue
@@
- manifest[file_path.name] = {
+ manifest[relative_name] = {
"md5": digest,
"file_id": file_id,
"url": url,
"synced_at": time.time(),
}
updated = True
- results.append({"filename": file_path.name, "file_id": file_id, "url": url})
+ results.append({"filename": relative_name, "file_id": file_id, "url": url})🧰 Tools
🪛 Ruff (0.15.12)
[error] 102-103: try-except-continue detected, consider logging the exception
(S112)
[warning] 102-102: Do not catch blind exception: Exception
(BLE001)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@computer-use-server/owui_sync.py` around lines 84 - 112, The manifest and
results currently use file_path.name, which conflates files with the same
basename; change to use the relative path from outputs_dir as the manifest key
and as the filename in results (e.g. key =
file_path.relative_to(outputs_dir).as_posix()). Update the lookup of manifest
(replace manifest.get(file_path.name) with manifest.get(key)), store the entry
under that key, and append results entries using that relative path; keep the
existing use of _md5(file_path) and _upload_file(owui_url, owui_api_key,
file_path) and preserve the rest of the logic (updated flag, synced_at, file_id,
url).
| with patch.dict(sys.modules, { | ||
| "skill_manager": fake_skill_manager, | ||
| "docker_manager": fake_docker_manager, | ||
| }): | ||
| import importlib | ||
| system_prompt_module = importlib.import_module("system_prompt") | ||
| system_prompt_module._render_cache.clear() | ||
| body = system_prompt_module.render_system_prompt_sync("abc123", None) |
There was a problem hiding this comment.
Make this prompt test deterministic by forcing a fresh system_prompt import.
Line 215 can return a previously cached system_prompt module, so your patched docker_manager/skill_manager may be ignored depending on test order.
✅ Deterministic import fix
with patch.dict(sys.modules, {
"skill_manager": fake_skill_manager,
"docker_manager": fake_docker_manager,
}):
import importlib
+ sys.modules.pop("system_prompt", None)
system_prompt_module = importlib.import_module("system_prompt")
- system_prompt_module._render_cache.clear()
body = system_prompt_module.render_system_prompt_sync("abc123", None)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/orchestrator/test_owui_sync.py` around lines 210 - 217, The test may
import a previously cached system_prompt module so the patched
fake_skill_manager/fake_docker_manager get ignored; before importing, remove
"system_prompt" from sys.modules (or use importlib.reload) to force a fresh
import, then import and clear system_prompt_module._render_cache and call
system_prompt_module.render_system_prompt_sync("abc123", None); update the test
around system_prompt_module to ensure the patched modules are used
deterministically.
|
Thanks for the contribution! Before I can review this seriously I need a bit more context, and I see a number of concrete issues that need to be addressed. Could you walk me through the following? 1. Motivation — what problem does this actually solve?The orchestrator already exposes every output file via This PR introduces a second, parallel path where the same files are also re-uploaded into Open WebUI's native file store and surfaced as
A short "before/after" example in the PR description (what the model emits today vs. with sync enabled, and what the user sees in OWUI) would go a long way. 2. Bugs / regressions
3. ScopeThe compose changes ( 4. Evidence I'd like to see in the PR before mergingThe current Test Plan is just a list of commands without output. Please attach:
Once the rationale is documented and the bugs above are addressed I'm happy to do a proper line-level review. Thanks! Generated by Claude Code |
|
Following up on §1 of my previous comment with a clearer position on direction. We can't accept anything that breaks compatibility or inverts the dependency toward Open WebUI, even when the underlying idea is reasonable. This project is positioned as model- and frontend-agnostic ("pluggable into any model"); the orchestrator must not know about OWUI's API, hold an OWUI API key, or act as an OWUI client. Two concrete consequences of the current design that we won't ship as-is:
If you still want this feature, the right shape is to mirror the existing OWUI → workspace sync, in reverse, on the OWUI side. We already do exactly this pattern for the inbound direction:
Key properties of that pattern that the reverse version should preserve:
Concretely, what we'd accept:
The compose-side changes ( Happy to review v2 in this shape. The bug list from my previous comment still applies to whatever survives the reshape (especially the Generated by Claude Code |
Summary
/api/v1/files/<id>/contentreferences in tool resultsTest plan
python3 -m pytest /Users/beluga/open-computer-use/tests/orchestrator/test_owui_sync.py -vpython3 -m py_compile /Users/beluga/open-computer-use/computer-use-server/owui_sync.py /Users/beluga/open-computer-use/computer-use-server/system_prompt.py /Users/beluga/open-computer-use/computer-use-server/mcp_tools.py /Users/beluga/open-computer-use/computer-use-server/docker_manager.pydocker compose -f /Users/beluga/open-computer-use/docker-compose.yml up -d --force-recreate computer-use-server cleanupcurl -fsS http://127.0.0.1:8081/health/api/v1/files/<id>/content🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests