286 lines
8.3 KiB
Python
286 lines
8.3 KiB
Python
from io import StringIO
|
|
from pathlib import Path
|
|
|
|
import aisuite as ai
|
|
from examples.cli.dev import (
|
|
DEFAULT_ALLOWED_COMMANDS,
|
|
ApprovalController,
|
|
CliConfig,
|
|
DevCli,
|
|
parse_args,
|
|
)
|
|
from examples.cli.create_demo_trace import create_demo_trace
|
|
|
|
|
|
def make_context(tool_name="run_shell", metadata=None):
|
|
return ai.ToolPolicyContext(
|
|
agent_name="assistant",
|
|
tool_name=tool_name,
|
|
arguments={"command": "python3 -m pytest"},
|
|
run_name="cli_turn",
|
|
trace_id="trace_1",
|
|
group_id="group_1",
|
|
tags=[],
|
|
metadata={},
|
|
messages=[],
|
|
tool_metadata=metadata,
|
|
)
|
|
|
|
|
|
def test_parse_args_uses_default_allowed_commands():
|
|
config = parse_args(
|
|
[
|
|
"--model",
|
|
"openai:gpt-4o-mini",
|
|
"--cwd",
|
|
".",
|
|
"--trace-http",
|
|
"http://127.0.0.1:8780/api/events",
|
|
]
|
|
)
|
|
|
|
assert config.model == "openai:gpt-4o-mini"
|
|
assert config.allowed_commands == DEFAULT_ALLOWED_COMMANDS
|
|
assert config.allow_write is False
|
|
assert config.allow_shell_all is False
|
|
assert config.trace_http == "http://127.0.0.1:8780/api/events"
|
|
|
|
|
|
def test_parse_args_accepts_repeated_allowed_commands():
|
|
config = parse_args(
|
|
[
|
|
"--allow-command",
|
|
"python3",
|
|
"--allow-command",
|
|
"pytest",
|
|
]
|
|
)
|
|
|
|
assert config.allowed_commands == ["python3", "pytest"]
|
|
|
|
|
|
def test_approval_controller_allows_low_risk_tool_without_prompt():
|
|
output = StringIO()
|
|
controller = ApprovalController(input_stream=StringIO(""), output_stream=output)
|
|
|
|
decision = controller.evaluate(make_context(tool_name="read_file"))
|
|
|
|
assert decision.allowed is True
|
|
assert decision.reason == "low risk"
|
|
assert output.getvalue() == ""
|
|
|
|
|
|
def test_approval_controller_denies_high_risk_tool_by_default():
|
|
output = StringIO()
|
|
metadata = ai.ToolMetadata(
|
|
category="shell",
|
|
risk_level="high",
|
|
requires_approval=True,
|
|
)
|
|
controller = ApprovalController(input_stream=StringIO("\n"), output_stream=output)
|
|
|
|
decision = controller.evaluate(make_context(metadata=metadata))
|
|
|
|
assert decision.allowed is False
|
|
assert decision.reason == "denied by user"
|
|
assert "Permission required" in output.getvalue()
|
|
assert "run_shell" in output.getvalue()
|
|
|
|
|
|
def test_approval_controller_can_allow_tool_for_session():
|
|
output = StringIO()
|
|
metadata = ai.ToolMetadata(
|
|
category="shell",
|
|
risk_level="high",
|
|
requires_approval=True,
|
|
)
|
|
controller = ApprovalController(input_stream=StringIO("a\n"), output_stream=output)
|
|
|
|
first = controller.evaluate(make_context(metadata=metadata))
|
|
second = controller.evaluate(make_context(metadata=metadata))
|
|
|
|
assert first.allowed is True
|
|
assert first.reason == "allowed for session"
|
|
assert second.allowed is True
|
|
assert second.reason == "allowed for session"
|
|
assert output.getvalue().count("Permission required") == 1
|
|
|
|
|
|
def test_dev_cli_prints_tool_arguments_and_result_preview(tmp_path):
|
|
output = StringIO()
|
|
cli = DevCli(
|
|
CliConfig(
|
|
model="openai:gpt-4o-mini",
|
|
cwd=tmp_path,
|
|
trace_file=tmp_path / "events.jsonl",
|
|
allowed_commands=DEFAULT_ALLOWED_COMMANDS,
|
|
),
|
|
input_stream=StringIO(""),
|
|
output_stream=output,
|
|
)
|
|
result = ai.RunResult(
|
|
final_output="done",
|
|
status="completed",
|
|
agent=cli.agent,
|
|
last_agent=cli.agent,
|
|
input="run tests",
|
|
messages=[],
|
|
new_items=[],
|
|
raw_responses=[],
|
|
run_name="cli_turn",
|
|
trace_id="trace_1",
|
|
parent_run_id=None,
|
|
group_id="group",
|
|
tags=[],
|
|
metadata={},
|
|
steps=[
|
|
ai.RunStep(
|
|
id="step_1",
|
|
type="tool_call",
|
|
name="run_shell",
|
|
trace_id="trace_1",
|
|
data={
|
|
"allowed": True,
|
|
"arguments": {"command": "python3 -m pytest tests/toolkits"},
|
|
"reason": "approved by user",
|
|
},
|
|
),
|
|
ai.RunStep(
|
|
id="step_2",
|
|
type="tool_result",
|
|
name="run_shell",
|
|
trace_id="trace_1",
|
|
data={
|
|
"status": "success",
|
|
"result_preview": (
|
|
'{"exit_code": 0, "stdout": "2 passed\\n", '
|
|
'"stderr": "", "timed_out": false}'
|
|
),
|
|
},
|
|
),
|
|
],
|
|
max_turns=5,
|
|
)
|
|
|
|
cli._print_steps(result)
|
|
|
|
rendered = output.getvalue()
|
|
assert "arguments:" in rendered
|
|
assert "command: python3 -m pytest tests/toolkits" in rendered
|
|
assert "exit_code: 0" in rendered
|
|
assert "stdout: 2 passed\\n" in rendered
|
|
|
|
|
|
def test_dev_cli_viewer_command_prints_hint(tmp_path):
|
|
output = StringIO()
|
|
cli = DevCli(
|
|
CliConfig(
|
|
model="openai:gpt-4o-mini",
|
|
cwd=tmp_path,
|
|
trace_file=tmp_path / "events.jsonl",
|
|
allowed_commands=DEFAULT_ALLOWED_COMMANDS,
|
|
),
|
|
input_stream=StringIO(""),
|
|
output_stream=output,
|
|
)
|
|
|
|
assert cli._handle_command("/viewer") is False
|
|
|
|
rendered = output.getvalue()
|
|
assert "python -m aisuite.agents.viewer" in rendered
|
|
assert "pass --trace-http" in rendered
|
|
|
|
|
|
def test_dev_cli_viewer_start_launches_viewer(tmp_path, monkeypatch):
|
|
output = StringIO()
|
|
cli = DevCli(
|
|
CliConfig(
|
|
model="openai:gpt-4o-mini",
|
|
cwd=tmp_path,
|
|
trace_file=tmp_path / "events.jsonl",
|
|
allowed_commands=DEFAULT_ALLOWED_COMMANDS,
|
|
),
|
|
input_stream=StringIO(""),
|
|
output_stream=output,
|
|
)
|
|
viewer = type(
|
|
"Viewer",
|
|
(),
|
|
{"url": "http://127.0.0.1:1234", "stop": lambda self: None},
|
|
)()
|
|
calls = []
|
|
|
|
def start_viewer(trace_file, port):
|
|
calls.append((trace_file, port))
|
|
return viewer
|
|
|
|
monkeypatch.setattr(ai.tracing, "start_viewer", start_viewer)
|
|
|
|
assert cli._handle_command("/viewer start") is False
|
|
|
|
assert calls == [(tmp_path / "events.jsonl", 0)]
|
|
assert "http://127.0.0.1:1234" in output.getvalue()
|
|
|
|
|
|
def test_create_demo_trace_writes_sample_runs(tmp_path):
|
|
trace_file = tmp_path / "demo.jsonl"
|
|
|
|
create_demo_trace(trace_file=trace_file, cwd=Path.cwd())
|
|
|
|
runs = ai.tracing.read_trace_file(trace_file)
|
|
assert len(runs) == 8
|
|
assert {run["group_id"] for run in runs} == {"aisuite-demo"}
|
|
assert {run["metadata"]["app"] for run in runs} == {"aisuite_demo_trace"}
|
|
assert any(
|
|
step["name"] == "read_file_lines"
|
|
for run in runs
|
|
for step in run["steps"]
|
|
if step["type"] == "tool_call"
|
|
)
|
|
assert any(
|
|
step["name"] == "run_shell"
|
|
for run in runs
|
|
for step in run["steps"]
|
|
if step["type"] == "tool_result"
|
|
)
|
|
assert any(
|
|
step["name"] == "write_file" and step["data"].get("argument_artifacts")
|
|
for run in runs
|
|
for step in run["steps"]
|
|
if step["type"] == "tool_call"
|
|
)
|
|
assert any(
|
|
step["name"] == "run_shell" and step["data"].get("result_artifacts")
|
|
for run in runs
|
|
for step in run["steps"]
|
|
if step["type"] == "tool_result"
|
|
)
|
|
|
|
|
|
def test_create_demo_trace_includes_subagent_and_denied_event(tmp_path):
|
|
trace_file = tmp_path / "demo.jsonl"
|
|
|
|
create_demo_trace(trace_file=trace_file, cwd=Path.cwd())
|
|
|
|
runs = ai.tracing.read_trace_file(trace_file)
|
|
assert any(run["agent_name"] == "reviewer" for run in runs)
|
|
parent = next(
|
|
run
|
|
for run in runs
|
|
if run["agent_name"] == "aisuite_demo_dev" and run["display"]["child_count"]
|
|
)
|
|
reviewer = next(run for run in runs if run["agent_name"] == "reviewer")
|
|
assert reviewer["parent_run_id"] == parent["trace_id"]
|
|
assert any(
|
|
event["event_type"] == "tool.denied"
|
|
and event["data"].get("reason") == "blocked by demo policy"
|
|
for run in runs
|
|
for event in run["events"]
|
|
)
|
|
assert any(
|
|
activity.get("tool_name") == "review_changes"
|
|
and activity.get("child_run", {}).get("trace_id") == reviewer["trace_id"]
|
|
for run in runs
|
|
for activity in run["display"]["activities"]
|
|
)
|