diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..03b1e9c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + # Workflow actions are pinned to commit SHAs; this opens PRs that bump the SHA + # and the trailing version comment together. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..85424d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python: ["3.12", "3.13"] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ matrix.python }} + cache: pip + # The dependency (based-models-agentloop) comes from PyPI, like a real install. + - run: pip install -e ".[dev]" + - run: ruff check . + - run: mypy + - run: pytest -q + + wheel: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - run: pip install build twine + - run: bash scripts/test-wheel.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..48c4ef1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +name: Release + +# Tag vX.Y.Z -> verify -> build once -> PyPI (Trusted Publishing; the tag IS the release +# decision, there is no approval gate) -> GitHub Release carrying the SAME files. +# +# Dry run: Actions -> Release -> Run workflow on any branch publishes to TestPyPI only. +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Tag must match pyproject.toml and __version__ + if: github.ref_type == 'tag' + run: | + python - <<'PY' + import os, re, tomllib + tag = os.environ["GITHUB_REF_NAME"].removeprefix("v") + project = tomllib.load(open("pyproject.toml", "rb"))["project"]["version"] + init = re.search(r'__version__ = "([^"]+)"', + open("src/agentloop_tools/__init__.py").read()).group(1) + assert tag == project == init, f"tag={tag} pyproject={project} __init__={init}" + PY + - name: Changelog has an entry for this version + if: github.ref_type == 'tag' + run: grep -q "^## \[${GITHUB_REF_NAME#v}\]" CHANGELOG.md + - run: pip install -e ".[dev]" + - run: ruff check . && mypy && pytest -q + - run: bash scripts/test-wheel.sh + - name: Checksums + run: cd dist && sha256sum * > SHA256SUMS + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist + path: dist/ + if-no-files-found: error + + publish-testpypi: + # Manual dry run only. TestPyPI versions are immutable too: bump the version to retry. + if: github.event_name == 'workflow_dispatch' + needs: build + runs-on: ubuntu-latest + environment: testpypi + permissions: + id-token: write # Trusted Publishing; no API token anywhere + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + - run: rm dist/SHA256SUMS # not a distribution file + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: dist/ + + publish-pypi: + if: github.ref_type == 'tag' + needs: build + runs-on: ubuntu-latest + environment: pypi # must match the trusted publisher configured on PyPI + permissions: + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + - run: rm dist/SHA256SUMS + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/ + + github-release: + needs: publish-pypi + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist/ + - name: Publish the GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$GITHUB_REF_NAME" dist/* \ + --title "$GITHUB_REF_NAME" --verify-tag \ + --notes "Published to PyPI as based-models-agentloop-tools ${GITHUB_REF_NAME#v}. See CHANGELOG.md." diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..600355a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +Format: [Keep a Changelog](https://keepachangelog.com/). Versioning: SemVer; while `0.x`, a +minor release may change the public API. + +## [0.1.0] + +### Added +- `native_web_search`, `DomainPolicy`, `sources()` and `citations()` for agentloop's hosted + web search, published on PyPI as `based-models-agentloop-tools` (Apache-2.0). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1078a52 --- /dev/null +++ b/README.md @@ -0,0 +1,266 @@ +# based-models-agentloop-tools + +`based-models-agentloop-tools` provides typed, validated tools for +`based-models-agentloop` agents. Its first tool family makes provider-hosted web search easy +to configure, limits where searches may go, and extracts the sources and citations behind an +answer. + +The package is installed as `based-models-agentloop-tools` and imported as +`agentloop_tools`. It requires Python 3.12 or newer. + +## Why use agentloop-tools? + +- Add native Anthropic or OpenAI web search to an `Agent` with one tool factory. +- Validate domain filters, location data, search limits, and provider options before a + request is sent. +- Apply one allowlist or blocklist both to the provider request and to a post-response audit. +- Recover deduplicated search sources and ordered answer citations from a transcript. +- Detect disallowed domains in search results, citations, URLs, Markdown links, and blocked + host names written in an answer. +- Keep configuration and credentials in the application; this package never reads the + environment. +- Stay provider-light: the package depends only on `based-models-agentloop` and imports only + its public API. + +## Installation + +Install this package together with the extra for the provider your agent uses. + +For Anthropic: + +```bash +pip install based-models-agentloop-tools "based-models-agentloop[anthropic]" +``` + +For OpenAI: + +```bash +pip install based-models-agentloop-tools "based-models-agentloop[openai-compat]" +``` + +Native web search works with Anthropic Messages and the OpenAI Responses API. OpenAI uses +Responses by default; it will not work when `OPENAI_API=completions` is selected. OpenRouter, +Modal/vLLM, and other Chat Completions-compatible providers do not offer this hosted tool. + +## Quick start + +Create a hosted search tool, pass it to `Agent`, and use a `Transcript` when you want to +inspect the evidence: + +```python +import os + +from agentloop import Agent, Transcript, final_text +from agentloop_tools.web import citations, native_web_search, sources + +search = native_web_search( + allowed_domains=["europa.eu"], + user_location={"country": "BE"}, +) + +transcript = Transcript() +transcript.add_user_message( + "What does the EU AI Act say about general-purpose models?" +) + +with Agent.from_env( + os.environ, + tools=[search], + system="Use web search and cite your sources.", +) as agent: + agent.converse(transcript) + +print(final_text(transcript)) + +for source in sources(transcript): + print("searched:", source.url) + +for citation in citations(transcript): + print("cited:", citation.url) +``` + +The provider performs the search. `agentloop` records the search activity, sources, +citations, and provider replay data in the transcript; this package provides the validated +configuration and evidence readers. + +## Native web search options + +```python +from agentloop_tools.web import native_web_search + +search = native_web_search( + allowed_domains=["europa.eu", "oecd.org/ai"], + max_uses=3, + user_location={ + "city": "Brussels", + "country": "BE", + "timezone": "Europe/Brussels", + }, + search_context_size="high", +) +``` + +| Option | Meaning | Anthropic | OpenAI Responses | +|---|---|---:|---:| +| `allowed_domains` | Search only these domains and paths | Yes | Yes | +| `blocked_domains` | Exclude these domains and paths | Yes | Yes | +| `user_location` | Approximate city, region, country, or timezone | Yes | Yes | +| `max_uses` | Maximum searches per request | Yes | Ignored with a warning | +| `dynamic_filtering` | Filter results with provider-side code | Yes | Ignored with a warning | +| `search_context_size` | Amount of search content supplied to the model | Ignored with a warning | Yes | + +Unknown options are rejected. Unsupported domain restrictions are never silently dropped; +unsupported tuning options are logged and ignored by the adapter. + +### Domain validation + +Domain filters use bare ASCII domains without a URL scheme: + +```python +native_web_search(allowed_domains=["europa.eu", "example.com/reports"]) +``` + +- Pass either `allowed_domains` or `blocked_domains`, never both. +- A rule includes the named host and its subdomains. +- An optional path restricts the rule to that path prefix. +- `*` is allowed within a path, but not in a hostname. +- Schemes such as `https://` are rejected. +- Non-ASCII domains are rejected to prevent look-alike Unicode domains from bypassing a + filter. + +## Sources and citations + +The two evidence readers serve different purposes: + +```python +from agentloop_tools.web import citations, sources + +searched_pages = sources(transcript) +cited_claims = citations(transcript) +``` + +- `sources(transcript)` returns every page a hosted search consulted, deduplicated by URL in + first-appearance order. +- `citations(transcript)` returns every citation attached to assistant text, in transcript + order. + +A source shows what the provider searched. A citation ties part of the generated answer to a +page. They are related but not interchangeable: a provider may search a page without citing +it, and not every search mode produces citations. + +### Anthropic dynamic filtering + +`dynamic_filtering=False` is the default because Anthropic's basic search preserves +citations. When enabled, Anthropic runs provider-side code to filter search results before +they reach the model. This can help search-heavy requests, but it has important tradeoffs: + +- `sources()` still returns the pages consulted. +- `citations()` may be empty because the model reads the filtering program's output rather + than citation-bearing search-result blocks. +- The mode is not eligible for Zero Data Retention. + +OpenAI ignores `dynamic_filtering` with a warning. + +## Domain policy: prevent, then audit + +`DomainPolicy` keeps one allowlist or blocklist as the source of truth for both request-time +filtering and response-time auditing. + +```python +import os + +from agentloop import Agent, Transcript, final_text +from agentloop_tools.web import DomainPolicy + +policy = DomainPolicy.allow(["europa.eu", "oecd.org"]) +search = policy.native_web_search(search_context_size="high") + +transcript = Transcript() +before = len(transcript.messages) +transcript.add_user_message("Summarize recent AI-policy guidance.") + +with Agent.from_env(os.environ, tools=[search]) as agent: + agent.converse(transcript) + +violations = policy.audit(transcript, since=before) +if violations: + for violation in violations: + print(violation.where, violation.url, violation.rule) +else: + print(final_text(transcript)) +``` + +Construct policies with one of: + +```python +allowed = DomainPolicy.allow(["europa.eu", "oecd.org"]) +blocked = DomainPolicy.block(["example.com", "example.org/private"]) +``` + +`policy.native_web_search()` sends the policy's domains to the provider. `policy.audit()` +then inspects completed assistant messages for: + +- Sources returned by hosted search +- Citations attached to assistant text +- Full URLs, including Markdown link targets, written in the answer +- Bare mentions of blocked hosts in block mode + +Each `Violation` reports the URL or mention, where it appeared, the transcript message index, +and the matching block rule when applicable. Auditing does not modify the transcript, raise +an exception, retry the request, or choose an enforcement response; the application decides +whether to log, reject, re-ask, or warn the user. + +### Policy limits + +Native search runs inside the provider's response. A domain filter asks the provider not to +return a domain, but downstream code cannot prevent the model from reading a page that the +provider did return. `DomainPolicy` therefore follows a deliberate two-stage model: + +1. Prevent what the provider can prevent with its native domain filter. +2. Report disallowed domains that still appear in sources, citations, or answer text. + +In allow mode, the audit checks full URLs but does not treat every host-like word as a domain; +doing so would create false positives for text such as `config.py`. In block mode, it can +also look specifically for bare mentions of the configured blocked hosts. + +## API overview + +Import web tools explicitly from `agentloop_tools.web`: + +```python +from agentloop_tools.web import ( + DomainPolicy, + Violation, + citations, + native_web_search, + sources, +) +``` + +- `native_web_search(...)` builds a validated `HostedTool` for provider-run search. +- `sources(transcript)` extracts unique consulted pages. +- `citations(transcript)` extracts citations attached to assistant text. +- `DomainPolicy.allow(...)` and `DomainPolicy.block(...)` create domain policies. +- `policy.native_web_search(...)` builds a search tool using the policy's domain list. +- `policy.match(...)` identifies the matching rule for a URL or hostname. +- `policy.permits(...)` checks whether a URL or hostname is allowed. +- `policy.audit(...)` returns policy violations without mutating the transcript. + +Tool families are intentionally imported explicitly rather than re-exported from the package +root. The package uses only `agentloop`'s public API and never reads configuration, API keys, +or environment variables. + +This release provides provider-hosted web search. Client-side search—where your own Python +tool calls a search API and controls results before the model sees them—is a separate feature +and is not included yet. + +## Documentation + +Full documentation and additional examples are coming soon. + +## Stability and license + +`based-models-agentloop-tools` is currently beta software. While the version is `0.x`, a +minor release may change the public API. + +Licensed under the Apache License 2.0. diff --git a/pyproject.toml b/pyproject.toml index 35742ad..1e557b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,44 @@ [project] -name = "agentloop-tools" +name = "based-models-agentloop-tools" version = "0.1.0" description = "Tools for agentloop agents: native web search now; client search and coding tools next." +readme = "README.md" requires-python = ">=3.12" +license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = ["llm", "agent", "tools", "web-search", "agentloop"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Typing :: Typed", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] dependencies = [ # NAMED, not a git URL: this library uses agentloop's public API only - # (tests/test_layering.py), so it depends on it like any stranger would. - # Distribution name is "based-models-agentloop" (the "agentloop" name on - # PyPI belongs to an unrelated project); the importable package is still - # `agentloop`. - "based-models-agentloop>=0.3.0,<0.4.0", + # (tests/test_layering.py), so it depends on it like any other consumer would. + # The importable package is `agentloop`; the distribution is + # `based-models-agentloop` (the bare name on PyPI is an unrelated project). + "based-models-agentloop>=0.4.0,<0.5.0", ] [project.optional-dependencies] -dev = ["pytest>=8", "mypy>=1.11", "ruff>=0.6", "build>=1.2"] +dev = ["pytest>=8", "pytest-cov>=5", "mypy>=1.11", "ruff>=0.6", "build>=1.2", "twine>=5"] + +[project.urls] +Repository = "https://github.com/based-models/agent-loop-tools" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +packages = ["src/agentloop_tools"] + +[tool.hatch.build.targets.sdist] +include = ["src/agentloop_tools", "README.md", "LICENSE", "CHANGELOG.md", "pyproject.toml"] + [tool.ruff] line-length = 100 src = ["src"] diff --git a/scripts/test-wheel.sh b/scripts/test-wheel.sh new file mode 100644 index 0000000..fee994c --- /dev/null +++ b/scripts/test-wheel.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Build the sdist + wheel, install the wheel into a BARE virtualenv (dependencies come +# from PyPI, like a real consumer's install) and prove it works there. +set -euo pipefail +cd "$(dirname "$0")/.." + +export PIP_DISABLE_PIP_VERSION_CHECK=1 +PY=${PYTHON:-python3} +# Where the wheel's agentloop dependency comes from. Defaults to PyPI. Before +# based-models-agentloop is published, point this at a local wheel or checkout. +AGENTLOOP=${AGENTLOOP_REQUIREMENT:-} + +rm -rf dist +"$PY" -m build >/dev/null +"$PY" -m twine check dist/* + +whl=$(ls dist/based_models_agentloop_tools-*.whl) +sdist=$(ls dist/based_models_agentloop_tools-*.tar.gz) + +tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT +"$PY" -m venv "$tmp/venv" +[ -n "$AGENTLOOP" ] && "$tmp/venv/bin/pip" install -q "$AGENTLOOP" +"$tmp/venv/bin/pip" install -q "$whl" + +"$tmp/venv/bin/python" - <<'PY' +import sys +from importlib.metadata import version + +import agentloop_tools +from agentloop_tools.web import DomainPolicy, native_web_search, sources # noqa: F401 + +assert agentloop_tools.__version__ == version("based-models-agentloop-tools"), \ + "__version__ and pyproject.toml disagree" +tool = native_web_search(allowed_domains=["example.com"]) +assert tool.kind == "web_search" +for vendor in ("anthropic", "httpx", "opentelemetry"): + assert vendor not in sys.modules, f"importing agentloop_tools loaded {vendor}" +print("ok: the installed wheel works and stays light") +PY + +contents=$(unzip -Z1 "$whl") +grep -qx "agentloop_tools/py.typed" <<<"$contents" && echo "ok: py.typed shipped" +grep -q "dist-info/licenses/LICENSE$" <<<"$contents" || { echo "FAIL: LICENSE missing"; exit 1; } +meta=$(unzip -p "$whl" '*.dist-info/METADATA') +grep -q "^License-Expression: Apache-2.0" <<<"$meta" || { echo "FAIL: license metadata"; exit 1; } +grep -q "Private :: Do Not Upload" <<<"$meta" && { echo "FAIL: Private classifier"; exit 1; } + +for archive in wheel sdist; do + if [ "$archive" = wheel ]; then listing=$contents; else listing=$(tar tzf "$sdist"); fi + if grep -E '(^|/)(tests|scripts|docs|experiments|\.github)/|DESIGN\.md' <<<"$listing"; then + echo "FAIL: unexpected files in the $archive"; exit 1 + fi +done +echo "ok: the wheel and sdist contain the package and nothing else"