Files
2026-07-13 12:39:17 +08:00

260 lines
8.7 KiB
Python

from __future__ import annotations
import argparse
import asyncio
import sys
from pathlib import Path
from typing import cast
from openai.types.responses import ResponseTextDeltaEvent
from agents import Runner
from agents.items import TResponseInputItem
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Capabilities, Skills
from agents.sandbox.entries import Dir, GitRepo, LocalFile
if __package__ is None or __package__ == "":
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
DATA_PATH = Path(__file__).resolve().parent / "data"
W2_PATH = DATA_PATH / "sample_w2.pdf"
FORM_1040_PATH = DATA_PATH / "f1040.pdf"
DEFAULT_IMAGE = "tax-prep:latest"
DEFAULT_SKILLS_REPO = "sdcoffey/tax-prep-skills"
DEFAULT_SKILLS_REF = "main"
DEFAULT_QUESTION = "Please generate a 1040 for filing year 2025."
INSTRUCTIONS = """
You are a federal tax filing agent. Your job is to compute year-end taxes and
produce a filled-out Form 1040 for the specified tax year using the user's
provided documents. Use only the information in the supplied files. If required
data is missing or unclear, ask follow-up questions or note explicit
assumptions. Save the finalized, filled PDF in the `output/` directory and
provide a short summary of key amounts such as income, deductions, tax, and
refund or amount due.
This is a demo, so assume the following unless the workspace says otherwise:
1. Filing status is single.
2. SSN is 123-45-6789.
3. Date of birth is 1991-01-01.
4. There are no other income documents.
5. If a minor data point is still needed, make up a clearly synthetic test value.
Use the `federal-tax-prep` skill to accomplish this task.
""".strip()
def _require_docker_dependency():
try:
from docker import from_env as docker_from_env # type: ignore[import-untyped]
except Exception as exc: # pragma: no cover - import path depends on local Docker setup
raise SystemExit(
"Docker-backed runs require the Docker SDK.\n"
"Install the repo dependencies with: make sync"
) from exc
from agents.sandbox.sandboxes.docker import DockerSandboxClient, DockerSandboxClientOptions
return docker_from_env, DockerSandboxClient, DockerSandboxClientOptions
def _build_manifest() -> Manifest:
return Manifest(
entries={
"taxpayer_data": Dir(
children={"sample_w2.pdf": LocalFile(src=W2_PATH)},
description="Taxpayer income documents such as W-2s and 1099s.",
),
"reference_forms": Dir(
children={"f1040.pdf": LocalFile(src=FORM_1040_PATH)},
description="Blank tax forms the agent can use as templates.",
),
"output": Dir(description="Write finalized tax documents here."),
}
)
def _build_agent(*, model: str, skills_repo: str, skills_ref: str) -> SandboxAgent:
return SandboxAgent(
name="Tax Prep Assistant",
model=model,
instructions=(
INSTRUCTIONS + "\n\n"
"Inspect the workspace before answering. Keep final explanations concise, and make "
"sure the final filled files are actually written into `output/`."
),
default_manifest=_build_manifest(),
capabilities=Capabilities.default()
+ [
Skills(
from_=GitRepo(repo=skills_repo, ref=skills_ref),
),
],
)
async def _copy_output_dir(
*,
session,
destination_root: Path,
) -> list[Path]:
destination_root.mkdir(parents=True, exist_ok=True)
remote_output_root = session.normalize_path("output")
pending_dirs = [remote_output_root]
copied_files: list[Path] = []
while pending_dirs:
current_dir = pending_dirs.pop()
for entry in await session.ls(current_dir):
entry_path = Path(entry.path)
if entry.is_dir():
pending_dirs.append(entry_path)
continue
relative_path = entry_path.relative_to(remote_output_root)
local_path = destination_root / relative_path
local_path.parent.mkdir(parents=True, exist_ok=True)
handle = await session.read(entry_path)
try:
payload = handle.read()
finally:
handle.close()
if isinstance(payload, str):
local_path.write_text(payload, encoding="utf-8")
else:
local_path.write_bytes(bytes(payload))
copied_files.append(local_path)
return copied_files
async def _run_turn(
*,
agent: SandboxAgent,
input_items: list[TResponseInputItem],
run_config: RunConfig,
) -> list[TResponseInputItem]:
stream_result = Runner.run_streamed(agent, input_items, run_config=run_config)
saw_text_delta = False
async for event in stream_result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
if not saw_text_delta:
print("assistant> ", end="", flush=True)
saw_text_delta = True
print(event.data.delta, end="", flush=True)
continue
if event.type == "run_item_stream_event" and event.name == "tool_called":
raw_item = getattr(event.item, "raw_item", None)
tool_name = ""
if isinstance(raw_item, dict):
tool_name = cast(str, raw_item.get("name") or raw_item.get("type") or "")
else:
tool_name = cast(
str,
getattr(raw_item, "name", None) or getattr(raw_item, "type", None) or "",
)
if tool_name:
if saw_text_delta:
print()
saw_text_delta = False
print(f"[tool call] {tool_name}")
if saw_text_delta:
print()
return stream_result.to_input_list()
async def main(
*,
model: str,
image: str,
question: str,
output_dir: Path,
skills_repo: str,
skills_ref: str,
) -> None:
docker_from_env, DockerSandboxClient, DockerSandboxClientOptions = _require_docker_dependency()
agent = _build_agent(model=model, skills_repo=skills_repo, skills_ref=skills_ref)
client = DockerSandboxClient(docker_from_env())
sandbox = await client.create(
manifest=agent.default_manifest,
options=DockerSandboxClientOptions(image=image),
)
run_config = RunConfig(
sandbox=SandboxRunConfig(session=sandbox),
workflow_name="Sandbox tax prep demo",
)
conversation: list[TResponseInputItem] = [{"role": "user", "content": question}]
try:
async with sandbox:
conversation = await _run_turn(
agent=agent,
input_items=conversation,
run_config=run_config,
)
while True:
try:
additional_input = input("> ")
except (EOFError, KeyboardInterrupt):
break
conversation.append({"role": "user", "content": additional_input})
conversation = await _run_turn(
agent=agent,
input_items=conversation,
run_config=run_config,
)
copied_files = await _copy_output_dir(session=sandbox, destination_root=output_dir)
finally:
await client.delete(sandbox)
print(f"\nCopied {len(copied_files)} file(s) to {output_dir}")
for copied_file in copied_files:
print(copied_file)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.")
parser.add_argument("--image", default=DEFAULT_IMAGE, help="Docker image for the sandbox.")
parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.")
parser.add_argument(
"--output-dir",
default="tax-prep-results",
help="Local directory where files from sandbox output/ will be copied.",
)
parser.add_argument(
"--skills-repo",
default=DEFAULT_SKILLS_REPO,
help="GitHub repo in owner/name form for the skills bundle.",
)
parser.add_argument(
"--skills-ref",
default=DEFAULT_SKILLS_REF,
help="Git ref for the skills bundle.",
)
args = parser.parse_args()
asyncio.run(
main(
model=args.model,
image=args.image,
question=args.question,
output_dir=Path(args.output_dir).resolve(),
skills_repo=args.skills_repo,
skills_ref=args.skills_ref,
)
)