diff --git a/README.md b/README.md index 415889b..d4c6f31 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A minimal CLI coding agent with a persistent IPython execution environment and o The model gets a single built-in tool, `ipython`: a persistent IPython kernel for Python, shell commands via `!command`, and multi-line shell scripts via `%%bash`. The tool set is not configurable. File edits, shell work, and orchestration all go through it. -For convenience, rlm ships built-in *skills* that can be enabled per run via `RLM_SKILLS` (comma-separated, off by default): `edit` (single-occurrence string replacement) and `search` (web search via Serper, needs `SERPER_API_KEY`). Enabled skills are pre-imported into the IPython kernel like any other skill (see [Skills](#skills)), so the agent calls `await edit(path=..., old_str=..., new_str=...)` or `await search(query=...)`. +For convenience, rlm ships built-in *skills* that can be enabled per run via `RLM_SKILLS` (comma-separated, off by default): `edit` (single-occurrence string replacement) and `search` (web search via Serper, needs `SERPER_API_KEY`; also exposes `search.open` to fetch a URL as text, via the Jina Reader API when `JINA_API_KEY` is set and a direct fetch otherwise). Enabled skills are pre-imported into the IPython kernel like any other skill (see [Skills](#skills)), so the agent calls `await edit(path=..., old_str=..., new_str=...)`, `await search(query=...)`, or `await search.open(url=...)`. Context is reclaimed automatically: when a turn's prompt token count crosses `RLM_SUMMARIZE_AT_TOKENS`, the engine compacts the conversation into a summary and continues on a fresh branch. The IPython kernel keeps running across the compaction, so REPL state survives (see [Compaction](#compaction)). @@ -57,6 +57,7 @@ All configuration is via environment variables: | `RLM_MODEL` | `openai/gpt-5-mini` | Model name (PI Inference slug). Override with `--model` or `RLM_MODEL` for OpenAI/Anthropic direct (e.g. `gpt-4o`, `claude-sonnet-4-5`) | | `RLM_API_KEY` / `RLM_BASE_URL` | — / SDK default (`https://api.openai.com/v1`) | Explicit override (highest priority). Independent: setting `RLM_API_KEY` alone targets the SDK default endpoint; set `RLM_BASE_URL` too for a custom endpoint. For PI, use `PRIME_API_KEY` (below) which owns the full pair. | | `SERPER_API_KEY` | — | API key for the built-in `search` skill (Serper backend). Required when `search` is enabled. | +| `JINA_API_KEY` | — | API key for `search.open` page reading. Optional: when set, pages are fetched via the Jina Reader API (markdown, handles JS-rendered pages), with direct-fetch fallback. | | `PRIME_API_KEY` | — | PI Inference pair: targets `https://api.pinference.ai/api/v1` and forwards `PRIME_TEAM_ID` as `X-Prime-Team-ID` when set. | | `OPENAI_API_KEY` / `OPENAI_BASE_URL` | resolved by SDK | OpenAI pair — when `OPENAI_API_KEY` is set, AsyncOpenAI's native env handling is used (covers OpenAI direct and verifiers' rollout tunnel both). Provider precedence: explicit → PI → OpenAI. Keys are scoped to their own base URL so an `OPENAI_API_KEY` lying around can't leak to PI Inference. | | `RLM_SKILLS` | — | Comma-separated built-in skills to enable (`edit`, `search`); pre-imported into the kernel. Unknown names raise. See [Skills](#skills). | @@ -119,7 +120,7 @@ These artifacts are consumable for debugging, visualization, or training-data ex ## Skills -`rlm` ships a small set of built-in skills enabled per run via `RLM_SKILLS` (`edit`, `search`; see [MCP tools as skills](#mcp-tools-as-skills) for the related MCP path). `search` does web search through Serper and needs `SERPER_API_KEY`; it returns title/URL/snippet for a single query (`await search(query="...")`). Additional skills are supplied by the host environment: before `install.sh` runs, the environment places skill packages under `/task/rlm-skills//`, and `install.sh` installs them alongside `rlm` so they're both importable and on `$PATH`. +`rlm` ships a small set of built-in skills enabled per run via `RLM_SKILLS` (`edit`, `search`; see [MCP tools as skills](#mcp-tools-as-skills) for the related MCP path). `search` does web search through Serper and needs `SERPER_API_KEY`; it returns title/URL/snippet for a single query (`await search(query="...")`), top 10 results by default. It also exposes `search.open` to fetch a URL and return its text (`await search.open(url="...")`), via the Jina Reader API when `JINA_API_KEY` is set and a direct fetch (HTML/PDF parsed locally) otherwise. Additional skills are supplied by the host environment: before `install.sh` runs, the environment places skill packages under `/task/rlm-skills//`, and `install.sh` installs them alongside `rlm` so they're both importable and on `$PATH`. From IPython, import a skill and call its async `run(...)` entrypoint: diff --git a/pyproject.toml b/pyproject.toml index bb93db7..d7541da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "scipy", "beautifulsoup4", "lxml", + "pdfminer.six", "pydantic", ] diff --git a/src/rlm/prompt.py b/src/rlm/prompt.py index 8a34b19..fe8e988 100644 --- a/src/rlm/prompt.py +++ b/src/rlm/prompt.py @@ -76,7 +76,10 @@ "For web search, use the pre-imported async `search` skill from IPython: " '`await search(query="...")`. Results come back as title, URL, and snippet; ' "assign the result to a variable so you can revisit it. To cover " - "several angles at once, fan out with `asyncio.gather(search(...), search(...))`." + "several angles at once, fan out with `asyncio.gather(search(...), search(...))`. " + 'To read a promising result in full, use `await search.open(url="...")` — it ' + "returns the page text (HTML and PDF are parsed); assign it to a variable so " + "you can slice and search it instead of re-fetching." ) diff --git a/src/rlm/skills/__init__.py b/src/rlm/skills/__init__.py index ca73e8c..ce15639 100644 --- a/src/rlm/skills/__init__.py +++ b/src/rlm/skills/__init__.py @@ -1,7 +1,8 @@ """Built-in skills shipped with rlm, enabled per run via ``RLM_SKILLS``. Each built-in skill is a module here exposing an async ``run(...)`` (the same contract as an -uploaded skill). When enabled, a thin re-export module is written into the session directory +uploaded skill), plus any extra public async functions listed in its ``__all__`` (e.g. +``search.open``). When enabled, a thin re-export module is written into the session directory (on the kernel's ``sys.path``) so the kernel pre-imports it by name — the same path MCP-tool skills take (see ``rlm.mcp``). The agent then calls it from IPython, e.g. ``await edit(...)``. """ @@ -10,7 +11,7 @@ from pathlib import Path -# Built-in skill name -> module its ``run`` is re-exported from. +# Built-in skill name -> module whose ``__all__`` is re-exported. _BUILTIN_SKILLS: dict[str, str] = { "edit": "rlm.skills.edit", "search": "rlm.skills.search", @@ -35,7 +36,5 @@ def enable_builtin_skills(names: list[str], dest_dir: Path) -> list[str]: ) dest_dir.mkdir(parents=True, exist_ok=True) for name in names: - (dest_dir / f"{name}.py").write_text( - f"from {_BUILTIN_SKILLS[name]} import run\n" - ) + (dest_dir / f"{name}.py").write_text(f"from {_BUILTIN_SKILLS[name]} import *\n") return names diff --git a/src/rlm/skills/edit.py b/src/rlm/skills/edit.py index 50c4cf8..8332471 100644 --- a/src/rlm/skills/edit.py +++ b/src/rlm/skills/edit.py @@ -9,6 +9,8 @@ from pathlib import Path +__all__ = ["run"] + async def run(path: str, old_str: str, new_str: str) -> str: """Replace a unique string in a file. diff --git a/src/rlm/skills/search.py b/src/rlm/skills/search.py index 337f758..2a3ed79 100644 --- a/src/rlm/skills/search.py +++ b/src/rlm/skills/search.py @@ -1,18 +1,26 @@ -"""Built-in ``search`` skill — web search via Serper. +"""Built-in ``search`` skill — web search via Serper, plus ``search.open`` page reading. Enabled via ``RLM_SKILLS``; pre-imported into the IPython kernel so the agent calls -``await search(query="...")``. Needs ``SERPER_API_KEY``. Ported from the Serper ``websearch`` -skill in research-environments/rlm_browsecomp. +``await search(query="...")`` and ``await search.open(url="...")``. Search needs +``SERPER_API_KEY``. ``open`` fetches pages through the Jina Reader API when +``JINA_API_KEY`` is set, falling back to a direct fetch parsed locally. Ported from +the Serper ``websearch`` skill in research-environments/rlm_browsecomp. """ from __future__ import annotations import asyncio +import io +import logging import os +import re import httpx +__all__ = ["run", "open"] + SERPER_URL = "https://google.serper.dev/search" +JINA_READER_URL = "https://r.jina.ai" def format_results(results, query: str) -> str: @@ -32,14 +40,14 @@ def format_results(results, query: str) -> str: return "\n\n---\n\n".join(sections) -def search(query: str, num_results: int = 5) -> str: +def search(query: str, num_results: int = 10) -> str: """Run a synchronous Serper web search and return formatted results.""" api_key = os.environ.get("SERPER_API_KEY", "") if not api_key: return "Error: SERPER_API_KEY environment variable is not set" response = httpx.post( SERPER_URL, - json={"q": query}, + json={"q": query, "num": num_results}, headers={"X-API-KEY": api_key, "Content-Type": "application/json"}, timeout=45, ) @@ -48,9 +56,11 @@ def search(query: str, num_results: int = 5) -> str: return format_results(organic[:num_results], query) -async def run(query: str, *, num_results: int = 5) -> str: +async def run(query: str, *, num_results: int = 10) -> str: """Run a web search via Serper and return formatted results. + Use ``await search.open(url=...)`` to read a result page in full. + Args: query: Web search query. num_results: Number of results to return. @@ -59,3 +69,77 @@ async def run(query: str, *, num_results: int = 5) -> str: Formatted results (title, URL, snippet). """ return await asyncio.to_thread(search, query, num_results) + + +def _pdf_to_text(pdf_bytes: bytes) -> str: + from pdfminer.high_level import extract_text + + logging.getLogger("pdfminer").setLevel(logging.ERROR) + return extract_text(io.BytesIO(pdf_bytes)) or "" + + +def _html_to_text(html_text: str) -> str: + from bs4 import BeautifulSoup + + soup = BeautifulSoup(html_text, "lxml") + for tag in soup(["script", "style", "noscript", "svg"]): + tag.decompose() + text = soup.get_text("\n") + text = re.sub(r"[ \t]{2,}", " ", text) + text = re.sub(r"\n{3,}", "\n\n", text) + return text + + +def _fetch_jina(url: str, api_key: str, timeout: float) -> str: + response = httpx.get( + f"{JINA_READER_URL}/{url}", + timeout=timeout, + headers={"Authorization": f"Bearer {api_key}", "X-Timeout": str(int(timeout))}, + ) + response.raise_for_status() + return response.text.strip() + + +def _fetch_direct(url: str, timeout: float) -> str: + response = httpx.get( + url, + timeout=timeout, + follow_redirects=True, + headers={"User-Agent": "Mozilla/5.0"}, + ) + response.raise_for_status() + content_type = (response.headers.get("content-type") or "").lower() + body = response.content + if body.startswith(b"%PDF-") or "application/pdf" in content_type: + return _pdf_to_text(body).strip() + if "text/html" in content_type or " str: + """Fetch a URL and return its text content, via Jina Reader when available.""" + api_key = os.environ.get("JINA_API_KEY", "") + if api_key: + try: + return _fetch_jina(url, api_key, timeout) + except httpx.HTTPError: + pass # fall back to direct fetch + return _fetch_direct(url, timeout) + + +# Shadows the builtin within this module on purpose: the agent-facing API is `search.open`. +async def open(url: str, *, timeout: float = 30) -> str: + """Fetch a URL and return its text content. Handles HTML and PDF. + + Uses the Jina Reader API (markdown; handles JS-rendered pages) when + ``JINA_API_KEY`` is set, falling back to a direct fetch parsed locally. + + Args: + url: The URL to fetch. + timeout: Request timeout in seconds. + + Returns: + The page text. + """ + return await asyncio.to_thread(open_page, url, timeout) diff --git a/src/rlm/tools/ipython.py b/src/rlm/tools/ipython.py index e538afe..3914298 100644 --- a/src/rlm/tools/ipython.py +++ b/src/rlm/tools/ipython.py @@ -203,12 +203,16 @@ def _wrap_callable(mod, log_source): wrapped = _CallableModule(mod.__name__) wrapped.__dict__.update(mod.__dict__) if log_source is not None: - _original_run = wrapped.run - @functools.wraps(_original_run) - async def _logged_run(*args, **kwargs): - _log_programmatic_call(mod.__name__, log_source) - return await _original_run(*args, **kwargs) - wrapped.run = _logged_run + def _logged(fn): + @functools.wraps(fn) + async def _call(*args, **kwargs): + _log_programmatic_call(mod.__name__, log_source) + return await fn(*args, **kwargs) + return _call + # Log every public async function the skill exposes (`run`, `search.open`, ...). + for _attr, _fn in list(wrapped.__dict__.items()): + if not _attr.startswith('_') and inspect.iscoroutinefunction(_fn): + setattr(wrapped, _attr, _logged(_fn)) # Mirror run's signature and docstring onto the module so # `inspect.signature()` and `help()` expose the real API # surface instead of `_CallableModule.__call__`'s `(*args, **kwargs)` diff --git a/tests/test_builtin_skills.py b/tests/test_builtin_skills.py index ddcf34d..78e5da7 100644 --- a/tests/test_builtin_skills.py +++ b/tests/test_builtin_skills.py @@ -34,7 +34,7 @@ async def test_edit_missing_file_raises(tmp_path): def test_enable_builtin_skills_writes_stub(tmp_path): assert "edit" in available_builtin_skills() assert enable_builtin_skills(["edit"], tmp_path) == ["edit"] - assert (tmp_path / "edit.py").read_text() == "from rlm.skills.edit import run\n" + assert (tmp_path / "edit.py").read_text() == "from rlm.skills.edit import *\n" def test_enable_unknown_skill_raises(tmp_path): @@ -45,7 +45,14 @@ def test_enable_unknown_skill_raises(tmp_path): def test_search_enable_writes_stub(tmp_path): assert "search" in available_builtin_skills() assert enable_builtin_skills(["search"], tmp_path) == ["search"] - assert (tmp_path / "search.py").read_text() == "from rlm.skills.search import run\n" + assert (tmp_path / "search.py").read_text() == "from rlm.skills.search import *\n" + + +def test_search_exports_run_and_open(): + import rlm.skills.search as search_skill + + assert search_skill.__all__ == ["run", "open"] + assert callable(search_skill.open) async def test_search_missing_api_key_returns_error(monkeypatch): @@ -54,6 +61,30 @@ async def test_search_missing_api_key_returns_error(monkeypatch): assert "SERPER_API_KEY" in result +async def test_search_requests_num_results(monkeypatch): + import rlm.skills.search as search_skill + + captured = {} + + class FakeResponse: + def raise_for_status(self): + pass + + def json(self): + return {"organic": [{"title": "t", "link": "https://x", "snippet": "s"}]} + + def fake_post(url, json=None, headers=None, timeout=None): + captured["json"] = json + return FakeResponse() + + monkeypatch.setenv("SERPER_API_KEY", "test-key") + monkeypatch.setattr(search_skill.httpx, "post", fake_post) + + out = await run_search(query="one") + assert captured["json"] == {"q": "one", "num": 10} + assert out.startswith("Result 1: t") + + def test_search_format_results(): results = [ {"title": "First", "link": "https://a", "snippet": "snippet one"}, diff --git a/uv.lock b/uv.lock index f52d659..42e140a 100644 --- a/uv.lock +++ b/uv.lock @@ -448,7 +448,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -580,17 +580,17 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "exceptiongroup" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } wheels = [ @@ -607,17 +607,17 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version == '3.11.*'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version == '3.11.*'" }, - { name = "jedi", marker = "python_full_version == '3.11.*'" }, - { name = "matplotlib-inline", marker = "python_full_version == '3.11.*'" }, - { name = "pexpect", marker = "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "stack-data", marker = "python_full_version == '3.11.*'" }, - { name = "traitlets", marker = "python_full_version == '3.11.*'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" } wheels = [ @@ -637,16 +637,16 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, - { name = "jedi", marker = "python_full_version >= '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, - { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "stack-data", marker = "python_full_version >= '3.12'" }, - { name = "traitlets", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } wheels = [ @@ -658,7 +658,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1195,10 +1195,10 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ @@ -1267,9 +1267,9 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } wheels = [ @@ -1331,12 +1331,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1910,6 +1923,7 @@ dependencies = [ { name = "openai" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pdfminer-six" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, @@ -1940,6 +1954,7 @@ requires-dist = [ { name = "numpy" }, { name = "openai", specifier = ">=1.0" }, { name = "pandas" }, + { name = "pdfminer-six" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "pyyaml" }, @@ -2263,7 +2278,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -2330,7 +2345,7 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -2625,8 +2640,8 @@ name = "uvicorn" version = "0.49.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, - { name = "h11", marker = "python_full_version < '3.11' or sys_platform != 'emscripten'" }, + { name = "click" }, + { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }