Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<p align="center">
<b><a href="https://hub.hcompany.ai/computer-use-agents">Documentation</a></b>
&nbsp;·&nbsp;
<a href="https://portal.hcompany.ai">Get an API key</a>
<a href="https://platform.hcompany.ai/settings/api-keys">Get an API key</a>
&nbsp;·&nbsp;
<a href="https://pypi.org/project/hai-agents/">PyPI</a>
&nbsp;·&nbsp;
Expand All @@ -39,7 +39,7 @@ Add the optional command-line tools with the `cli` extra:
pip install "hai-agents[cli]"
```

Python 3.10 or newer is required. Get an API key at [portal.hcompany.ai](https://portal.hcompany.ai) and export it:
Python 3.10 or newer is required. Get an API key at [platform.hcompany.ai/settings/api-keys](https://platform.hcompany.ai/settings/api-keys) and export it:

```bash
export HAI_API_KEY=hk-...
Expand Down Expand Up @@ -271,7 +271,7 @@ hai sessions watch <session-id>
hai mcp install # add the hai-agents MCP server to Cursor, VS Code, Claude Code, ...
```

Credentials resolve from `--api-key`, then `HAI_API_KEY`, then a local `.env`. Run `hai --help` for the full command set.
Credentials resolve from `--api-key`, then `HAI_API_KEY`, then a local `.env`, then `~/.config/hai/.env`. Run `hai --help` for the full command set.

## Documentation

Expand Down
10 changes: 4 additions & 6 deletions tests/integration/test_session_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The agent is dropped on a search-engine start URL (set on the environment spec)
and asked for Paris weather without being told which engine to use. Exercises
local_browser provisioning, the agent using whatever page it lands on, and
web browser provisioning, the agent using whatever page it lands on, and
end-to-end answer extraction.

Bing is used because it can be read without a CAPTCHA. The engine lives in the
Expand Down Expand Up @@ -40,8 +40,7 @@ def _poll_until_terminal(client: Client, session_id: str, timeout_s: float = 420
return s
if time.time() - start > timeout_s:
pytest.fail(
f"session {session_id} did not finish in {timeout_s}s "
f"(last status: {s.status}, steps: {s.steps})"
f"session {session_id} did not finish in {timeout_s}s (last status: {s.status}, steps: {s.steps})"
)
time.sleep(3)

Expand All @@ -62,9 +61,8 @@ def test_browser_session_finds_paris_weather(client: Client, run_id: str, create
"environments": [
{
"id": "browser",
"kind": "local_browser",
"width": 1280,
"height": 800,
"kind": "web",
"mode": {"type": "visual", "width": 1280, "height": 800},
"start_url": SEARCH_ENGINE_START_URL,
}
],
Expand Down
12 changes: 6 additions & 6 deletions tests/test_answer_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,34 +71,34 @@ def test_inline_dict_agent_gets_answer_format(self) -> None:
assert "JobListing" in schema["$defs"]

def test_catalog_agent_injected_via_overrides(self) -> None:
params: dict = {"agent": "h/web-surfer"}
params: dict = {"agent": "h/web-surfer-pro"}
_attach_answer_schema(params, JobListings)
assert params["overrides"]["agent.answer_format"]["title"] == "JobListings"

def test_existing_answer_format_conflicts(self) -> None:
with pytest.raises(ValueError, match="conflicts"):
_attach_answer_schema({"agent": {"answer_format": {"type": "object"}}}, JobListings)
with pytest.raises(ValueError, match="conflicts"):
_attach_answer_schema({"agent": "h/web-surfer", "overrides": {"agent.answer_format": {}}}, JobListings)
_attach_answer_schema({"agent": "h/web-surfer-pro", "overrides": {"agent.answer_format": {}}}, JobListings)

def test_inline_agent_with_answer_format_override_conflicts(self) -> None:
with pytest.raises(ValueError, match="conflicts"):
_attach_answer_schema({"agent": {"name": "a"}, "overrides": {"agent.answer_format": {}}}, JobListings)

def test_non_model_schema_rejected(self) -> None:
with pytest.raises(TypeError, match="BaseModel"):
_attach_answer_schema({"agent": "h/web-surfer"}, dict)
_attach_answer_schema({"agent": "h/web-surfer-pro"}, dict)

def test_user_overrides_preserved(self) -> None:
params: dict = {"agent": "h/web-surfer", "overrides": {"agent.max_steps": 5}}
params: dict = {"agent": "h/web-surfer-pro", "overrides": {"agent.max_steps": 5}}
_attach_answer_schema(params, JobListings)
assert params["overrides"]["agent.max_steps"] == 5


class TestAnswerParseBack:
def test_completed_answer_validates_into_model(self) -> None:
client = _Client(_VALID_ANSWER)
result = run_session(client, agent="h/web-surfer", messages="find jobs", answer_schema=JobListings) # type: ignore[arg-type]
result = run_session(client, agent="h/web-surfer-pro", messages="find jobs", answer_schema=JobListings) # type: ignore[arg-type]
assert isinstance(result.answer, JobListings)
assert result.answer.jobs[1].title == "SWE"
assert client.sessions.created_with["overrides"]["agent.answer_format"]["title"] == "JobListings"
Expand Down Expand Up @@ -152,7 +152,7 @@ def get_weather(city: str) -> str:

client = _Client(_VALID_ANSWER)
result = run_session( # type: ignore[arg-type]
client, agent="h/web-surfer", messages="go", answer_schema=JobListings, tools=[get_weather]
client, agent="h/web-surfer-pro", messages="go", answer_schema=JobListings, tools=[get_weather]
)
overrides = client.sessions.created_with["overrides"]
assert overrides["agent.answer_format"]["title"] == "JobListings"
Expand Down