Skip to content

Commit b4a6bf0

Browse files
committed
add run_agent; implement general-purpose multimodal code agent with task handling and image support
update ask_web_agent; clarify response instructions for task completion update shell; modify git diff command to reflect working directory
1 parent 6f69349 commit b4a6bf0

6 files changed

Lines changed: 289 additions & 4 deletions

File tree

argus/tools/ask_web_agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def execute(self, url: str, question: str = "", **_: Any) -> Content: # type: i
6969
"2. If the page is blank or shows errors, get console logs and report the exact error messages.\n"
7070
"3. If the question requires interaction (clicking buttons, typing values, scrolling), perform those actions and take screenshots to capture the result.\n"
7171
"4. Use get_text to read specific values if visual inspection is not precise enough.\n"
72-
"5. End your response with a one-line verdict: PASS, FAIL, or PARTIAL — and explain why with specific evidence (values seen, errors found, etc.)."
72+
"5. End your response with a clear answer and explain why with specific evidence (values seen, errors found, etc.)."
7373
)
7474
text, screenshot = self._web_agent.run_with_screenshot(Content(text=task))
7575
return Content(text=text, images=[screenshot] if screenshot else [])

argus/tools/shell.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ def __init__(
5050
self._port: int = port
5151

5252
def get_patch(self) -> str | None:
53-
"""Return the git diff of /testbed via docker exec, bypassing the pexpect shell."""
53+
"""Return the git diff of the working directory via docker exec, bypassing the pexpect shell."""
5454
if self._container is None:
5555
return None
5656
result = self._container.exec_run(
57-
["bash", "-c", "git -C /testbed -c core.fileMode=false diff"]
57+
["bash", "-c", f"git -C {self.workdir} -c core.fileMode=false diff"]
5858
)
5959
return result.output.decode("utf-8", errors="replace")
6060

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
agent:
2+
max_steps: 80
3+
log_dir: logs/
4+
enable_memory: false
5+
summarize_threshold: 7000
6+
system_prompt: |
7+
You are a software engineer that interacts multiple times with a computer shell to solve programming tasks.
8+
You operate in a REPL (Read-Eval-Print Loop) environment where you must issue exactly ONE shell tool call at a time.
9+
IMPORTANT: To execute commands you MUST call the shell tool — do NOT just write bash code blocks in your response text, as those are not executed.
10+
After each tool call, wait for the result before deciding the next step.
11+
Please briefly explain your reasoning before each tool call.
12+
13+
## Task Planning (MANDATORY FIRST STEP)
14+
Before doing anything else, call the `checklist` tool with action='init' to make a detailed plan listing every step you plan to take.
15+
After completing each step, you have to call `checklist` with action='complete' and the step's index to mark this step is done.
16+
If you discover new steps mid-task, use action='add'.
17+
If you find your plan is not working, you may make a new plan by calling `checklist` with action='init' again.
18+
Your plan should not be too long — ideally around 5-8 steps. Do NOT make a plan with 12+ steps as that is likely too detailed and will not be effective.
19+
20+
## Shell Usage Rules (CRITICAL)
21+
- NEVER wrap commands in `bash -lc '...'` or `bash -c '...'`. You are already inside a bash session.
22+
- When using grep/find, always search within /testbed (use `.` or `/testbed`). NEVER search from `..` or `/` as it will time out.
23+
- Do NOT run blocking commands that take more than 120 seconds. Start servers/processes in the background (append `&`) and return immediately.
24+
- If the command timed out, please try a different command or approach and continue. Do NOT repeat the same command again.
25+
26+
## Efficient Code Reading
27+
- Use `grep -n "pattern" -r src/ --include="*.js"` to find relevant lines first.
28+
- Then read only the relevant section with `sed -n 'START,ENDp' file`. Keep ranges to ~100 lines maximum — this is a hard limit, not a suggestion.
29+
- Do NOT use `cat` on source files. Do NOT use sed ranges larger than 100 lines. Outputs too long will be summarized and you will lose detail.
30+
- Never search for something you already found — track what you know.
31+
32+
## Editing Source Code
33+
- Do NOT use `applypatch`, `patch`, or any tool not available in a standard bash environment.
34+
- After each edit, verify the change took effect with `grep -n` or a small `sed -n` read.
35+
- If an edit fails twice in a row, switch to a different approach immediately.
36+
- NEVER use `node - << 'EOF'` or `python3 - << 'EOF'` to edit files — heredoc + scripting language causes multi-level escaping bugs where replacements silently fail. Use `sed -i` for simple substitutions.
37+
38+
### Edit files with sed:
39+
```bash
40+
# Replace all occurrences
41+
sed -i 's/old_string/new_string/g' filename
42+
43+
# Replace only first occurrence
44+
sed -i 's/old_string/new_string/' filename
45+
46+
# Replace first occurrence on line 1
47+
sed -i '1s/old_string/new_string/' filename
48+
49+
# Replace all occurrences in lines 1-10
50+
sed -i '1,10s/old_string/new_string/g' filename
51+
52+
## Using ask_web_agent
53+
- Use ask_web_agent when the task requires visual inspection of a web page (e.g. checking rendered output, UI layout, or interactive behaviour).
54+
- Do NOT use it to check whether a server is running — use `curl -s -o /dev/null -w "%{http_code}" http://localhost:PORT` instead.
55+
- Ask specific, targeted questions describing exactly what you expect to see. Vague questions produce vague answers.
56+
- After every call, read the response carefully. If it reports a problem, adjust your approach before calling again.
57+
58+
## MANDATORY: Responding to ask_web_agent Errors (CRITICAL RULE)
59+
- After EVERY ask_web_agent call, you MUST read its response.
60+
- If the response indicates a FAIL or PARTIAL result, you MUST immediately analyze the failure reason and adjust your approach accordingly.
61+
- Ignoring a FAIL/PARTIAL result and continuing anyway is a critical mistake — do not do it.
62+
63+
## Tool Output
64+
- If a tool result starts with "[Output was ... chars — summarized below]", the raw output was too long and has been automatically summarized for you. Treat the summary as the actual result and continue accordingly.
65+
66+
LLM:
67+
provider: openai
68+
model: gpt-4o
69+
api_key: ""
70+
base_url: ""
71+
temperature: 0.5
72+
Memory:
73+
user_id: default
74+
base_url: http://localhost:1995/api/v1
75+
api_key: null
76+
retrieve_method: hybrid
77+
top_k: 5
78+
79+
web_agent:
80+
headless: true
81+
max_steps: 10
82+
log_dir: logs/
83+
system_prompt: |
84+
You are a web browsing agent. Navigate to URLs, take screenshots, and report what you see clearly and concisely.
85+
86+
- Always navigate to the URL first, then take a screenshot before drawing conclusions.
87+
- If the page is unreachable or blank, check console logs and report immediately.
88+
- If the task involves interaction (clicking, typing, scrolling), perform the action and take a screenshot after.
89+
- Base your judgement on visual appearance and behaviour, not source code.
90+
LLM:
91+
provider: openai
92+
model: gpt-4o
93+
api_key: ""
94+
base_url: ""
95+
temperature: null
96+
Memory:
97+
user_id: default
98+
base_url: http://localhost:1995/api/v1
99+
api_key: null
100+
retrieve_method: hybrid
101+
top_k: 5

run_agent.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
"""General-purpose multimodal code agent entry point."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import base64
8+
import logging
9+
import random
10+
from pathlib import Path
11+
12+
import requests
13+
14+
from argus import Agent, AskWebAgentTool, EverMindMemory, WebAgent
15+
from argus.config import AgentConfig, Config, LLMConfig, WebAgentConfig
16+
from argus.data.content import Content
17+
from argus.llm.anthropic import AnthropicClient
18+
from argus.llm.base import LLMClient
19+
from argus.llm.openai import OpenAIClient
20+
from argus.tools.checklist import ChecklistTool
21+
from argus.tools.shell import ShellTool
22+
23+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
24+
logger = logging.getLogger(__name__)
25+
26+
DEFAULT_DOCKER_IMAGE = "python:3.11"
27+
28+
29+
def _load_image(source: str) -> str | None:
30+
"""Load an image from a local path or URL and return base64-encoded bytes."""
31+
if source.startswith("http://") or source.startswith("https://"):
32+
try:
33+
resp = requests.get(source, timeout=15)
34+
resp.raise_for_status()
35+
return base64.b64encode(resp.content).decode()
36+
except Exception as exc:
37+
logger.warning("Failed to download image %s: %s", source, exc)
38+
return None
39+
path = Path(source)
40+
if not path.exists():
41+
logger.warning("Image file not found: %s", source)
42+
return None
43+
return base64.b64encode(path.read_bytes()).decode()
44+
45+
46+
def _build_task(text: str, port: int, max_steps: int, images: list[str]) -> Content:
47+
"""Build the task Content, injecting runtime context and optionally attaching images."""
48+
preamble = (
49+
f"You have {max_steps} steps available. Use them wisely.\n"
50+
f"A port has been reserved for you on the host: {port}. "
51+
f"If you need to run a server or web service, bind it to {port} inside the container.\n\n"
52+
)
53+
text = preamble + text
54+
if not images:
55+
return Content(text=text)
56+
loaded = [b64 for src in images if (b64 := _load_image(src)) is not None]
57+
if loaded:
58+
text += f"\n\n{len(loaded)} image(s) are attached after this text block."
59+
return Content(text=text, images=loaded)
60+
61+
62+
def _build_llm(cfg: LLMConfig) -> LLMClient:
63+
provider = cfg.provider.lower()
64+
if provider == "anthropic":
65+
return AnthropicClient(
66+
model=cfg.model,
67+
api_key=cfg.api_key,
68+
base_url=cfg.base_url or None,
69+
temperature=cfg.temperature,
70+
)
71+
if provider == "openai":
72+
return OpenAIClient(
73+
model=cfg.model,
74+
api_key=cfg.api_key,
75+
base_url=cfg.base_url or None,
76+
temperature=cfg.temperature,
77+
)
78+
raise ValueError(f"Unknown LLM provider: {cfg.provider!r}. Supported: openai, anthropic")
79+
80+
81+
def _build_web_agent(cfg: WebAgentConfig) -> WebAgent:
82+
llm = _build_llm(cfg.LLM)
83+
log_dir = Path(cfg.log_dir) if cfg.log_dir else None
84+
return WebAgent(
85+
llm=llm,
86+
system_prompt=cfg.system_prompt,
87+
max_steps=cfg.max_steps,
88+
log_dir=log_dir,
89+
headless=cfg.headless,
90+
)
91+
92+
93+
def _build_agent(
94+
cfg: AgentConfig, docker_image: str, workdir: str, port: int, web_agent: WebAgent
95+
) -> Agent:
96+
shell = ShellTool(docker_image, workdir=workdir, remove_on_cleanup=False, port=port)
97+
log_dir = Path(cfg.log_dir) if cfg.log_dir else None
98+
llm = _build_llm(cfg.LLM)
99+
100+
memory = None
101+
if cfg.enable_memory:
102+
memory = EverMindMemory(
103+
user_id=cfg.Memory.user_id,
104+
base_url=cfg.Memory.base_url,
105+
api_key=cfg.Memory.api_key,
106+
retrieve_method=cfg.Memory.retrieve_method,
107+
top_k=cfg.Memory.top_k,
108+
)
109+
110+
return Agent(
111+
llm=llm,
112+
tools=[ChecklistTool(), shell, AskWebAgentTool(web_agent)],
113+
system_prompt=cfg.system_prompt,
114+
max_steps=cfg.max_steps,
115+
log_dir=log_dir,
116+
memory=memory,
117+
summarize_threshold=cfg.summarize_threshold,
118+
)
119+
120+
121+
def main() -> None:
122+
parser = argparse.ArgumentParser(
123+
description="Run the Argus multimodal code agent on an arbitrary task."
124+
)
125+
parser.add_argument("--config", help="Path to config.yaml", required=True)
126+
parser.add_argument(
127+
"--docker",
128+
default=DEFAULT_DOCKER_IMAGE,
129+
metavar="IMAGE",
130+
help=f"Docker image to use (default: {DEFAULT_DOCKER_IMAGE})",
131+
)
132+
parser.add_argument(
133+
"--workdir",
134+
default="/workspace",
135+
metavar="DIR",
136+
help="Working directory inside the container (default: /workspace)",
137+
)
138+
parser.add_argument("--task", metavar="TEXT", help="Task description.", required=True)
139+
parser.add_argument(
140+
"--images",
141+
nargs="*",
142+
metavar="SRC",
143+
help="Image files or URLs to attach to the task (local paths or http(s):// URLs).",
144+
)
145+
parser.add_argument(
146+
"--port",
147+
type=int,
148+
default=None,
149+
metavar="PORT",
150+
help="Host port to expose from the container. Randomly assigned if omitted.",
151+
)
152+
args = parser.parse_args()
153+
154+
# Resolve task text
155+
task_text = args.task
156+
157+
port = args.port if args.port is not None else random.randint(10000, 65535)
158+
159+
cfg = Config.from_yaml(Path(args.config))
160+
task = _build_task(task_text, port, cfg.agent.max_steps, args.images or [])
161+
162+
logger.info("Docker image : %s", args.docker)
163+
logger.info("Workdir : %s", args.workdir)
164+
logger.info("Port : %d", port)
165+
logger.info("Max steps : %d", cfg.agent.max_steps)
166+
logger.info("Images : %d", len(task.images))
167+
168+
web_agent = _build_web_agent(cfg.web_agent)
169+
agent = _build_agent(cfg.agent, args.docker, args.workdir, port, web_agent)
170+
171+
agent.run(task)
172+
173+
shell = next((t for t in agent.tool_list if isinstance(t, ShellTool)), None)
174+
if shell is not None:
175+
patch = shell.get_patch()
176+
if patch:
177+
logger.info("=== git diff (patch) ===\n%s", patch)
178+
shell.cleanup()
179+
180+
logger.info("Done.")
181+
182+
183+
if __name__ == "__main__":
184+
main()

run_swebench_multimodal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def _build_llm(cfg: LLMConfig) -> LLMClient:
129129
def _build_agent(cfg: AgentConfig, instance: dict, port: int, web_agent: WebAgent) -> Agent:
130130
"""Instantiate the agent with the appropriate tools, LLM, and memory (if enabled)."""
131131
shell = ShellTool(
132-
_docker_image(instance), workdir="/testbed", remove_on_cleanup=False, port=port
132+
_docker_image(instance), workdir="/testbed", remove_on_cleanup=True, port=port
133133
)
134134
log_dir = Path(cfg.log_dir) / instance["instance_id"] if cfg.log_dir else None
135135
llm = _build_llm(cfg.LLM)

0 commit comments

Comments
 (0)