# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import asyncio import os from agents import Runner from agents.run import RunConfig from agents.sandbox import SandboxAgent, SandboxRunConfig from dotenv import load_dotenv from openai import AsyncOpenAI from mirage import MountMode, Workspace from mirage.agents.openai_agents import MirageRunner, MirageSandboxClient from mirage.resource.slack import SlackConfig, SlackResource load_dotenv(".env.development") slack = SlackResource(config=SlackConfig( token=os.environ["SLACK_BOT_TOKEN"], search_token=os.environ.get("SLACK_USER_TOKEN"), )) ws = Workspace({"/slack": (slack, MountMode.READ)}, mode=MountMode.READ) client = MirageSandboxClient(ws) navigator = SandboxAgent( name="path-resolver", model="gpt-5.4-mini", instructions=(f"{ws.file_prompt}\n\n" "Use shell tools (ls, find) to locate files. " "Reply with absolute paths only, one per line."), ) analyst = SandboxAgent( name="analyst", model="gpt-5.4-mini", instructions=( f"{ws.file_prompt}\n\n" "You have shell tools (ls, find, cat, grep, ...) and view_image. " "Some files may already be attached to this message — read them " "directly. For images you discover later, call view_image. " "Answer using only attachments and confirmed file contents."), ) async def mirage_run(task: str) -> str: """Run a task with both pre-attached multimodal context and live tools. Pipeline: 1. Navigator agent uses shell tools to resolve which paths the task needs. 2. MirageRunner pre-attaches every resolved path as a multimodal block — input_image (PNG/JPEG/GIF, base64 data URI), input_file (PDF, uploaded via OpenAI Files API), or input_text otherwise. 3. The analyst SandboxAgent receives those blocks AND retains all shell tools + native view_image. It can read the pre-attached content directly OR call view_image / cat for anything else it discovers mid-run. Args: task (str): Natural-language task referring to files in the VFS. Returns: str: The analyst's final output. """ nav = await Runner.run( navigator, f"Find every file this request refers to: {task}", run_config=RunConfig(sandbox=SandboxRunConfig(client=client)), max_turns=20, ) print(" navigator raw output:") for line in nav.final_output.strip().splitlines(): print(f" {line!r}") paths = [ line.strip().strip("`").strip() for line in nav.final_output.strip().splitlines() if line.strip().startswith("/") ] print(f" resolved paths: {paths}") runner = MirageRunner(ws, client=AsyncOpenAI()) blocks = await runner.build_blocks(task, paths) out = await Runner.run( analyst, [{ "role": "user", "content": blocks }], run_config=RunConfig(sandbox=SandboxRunConfig(client=client)), max_turns=20, ) return out.final_output async def main(): task = "Summarize the latest PNG and PDF in the slack general channel." print(f"=== Task: {task} ===") print() result = await mirage_run(task) print() print("=== Analyst output ===") print(result) # Why both pre-attach AND view_image? # # - PNG/JPEG/GIF: either path works. view_image is convenient for images # the agent discovers mid-run; pre-attach is convenient for images # already known up front. # - PDF: ONLY pre-attach works. The OpenAI Agents SDK has no view_file # builtin for tool outputs (issue #341). PDFs must be uploaded to the # Files API and added as input_file blocks in a user message. We do # that before the agent run so the model receives full PDF text and # rendered pages. # - input_text: any non-binary content the agent might want pre-loaded. # # Resource-agnostic: ws.ops.read(path) routes via the workspace mount # registry, so the same flow works for /s3/...png, /disk/...pdf, # /slack/.../files/..., etc. if __name__ == "__main__": asyncio.run(main())