docpull 4.0.1__tar.gz → 4.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docpull-4.0.1/src/docpull.egg-info → docpull-4.1.0}/PKG-INFO +151 -8
- {docpull-4.0.1 → docpull-4.1.0}/README.md +147 -7
- {docpull-4.0.1 → docpull-4.1.0}/pyproject.toml +6 -2
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/__init__.py +1 -1
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/cli.py +12 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/conversion/chunking.py +56 -23
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/conversion/extractor.py +129 -1
- docpull-4.1.0/src/docpull/conversion/markdown.py +516 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/conversion/special_cases.py +108 -3
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/core/fetcher.py +6 -4
- docpull-4.1.0/src/docpull/fixtures/__init__.py +1 -0
- docpull-4.1.0/src/docpull/fixtures/parallel-search-extract.json +103 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/http/client.py +80 -11
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/mcp/server.py +299 -1
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/mcp/sources.py +1 -0
- docpull-4.1.0/src/docpull/pack_tools.py +500 -0
- docpull-4.1.0/src/docpull/parallel_workflows.py +5809 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/convert.py +7 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/fetch.py +8 -24
- docpull-4.1.0/src/docpull/security/__init__.py +13 -0
- docpull-4.1.0/src/docpull/security/download_policy.py +339 -0
- docpull-4.1.0/src/docpull/source_scoring.py +98 -0
- {docpull-4.0.1 → docpull-4.1.0/src/docpull.egg-info}/PKG-INFO +151 -8
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull.egg-info/SOURCES.txt +8 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull.egg-info/requires.txt +4 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_chunking.py +24 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_cli.py +30 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_conversion.py +88 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_convert_step_new.py +25 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_mcp_server.py +13 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_mcp_tools.py +1 -0
- docpull-4.1.0/tests/test_pack_tools.py +241 -0
- docpull-4.1.0/tests/test_parallel_workflows.py +2205 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_security_hardening.py +237 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_special_cases.py +99 -1
- docpull-4.0.1/src/docpull/conversion/markdown.py +0 -280
- docpull-4.0.1/src/docpull/security/__init__.py +0 -6
- {docpull-4.0.1 → docpull-4.1.0}/LICENSE +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/setup.cfg +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/__main__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/cache/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/cache/frontier.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/cache/manager.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/cache/streaming_dedup.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/conversion/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/conversion/protocols.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/conversion/trafilatura_extractor.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/core/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/_fetch.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/composite.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/crawler.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/filters.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/link_extractors/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/link_extractors/enhanced.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/link_extractors/protocols.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/link_extractors/static.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/protocols.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/discovery/sitemap.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/doctor.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/http/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/http/protocols.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/http/rate_limiter.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/mcp/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/mcp/tools.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/metadata_extractor.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/models/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/models/config.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/models/document.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/models/events.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/models/profiles.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/models/run.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/base.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/manifest.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/__init__.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/chunk.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/dedup.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/metadata.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/save.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/save_json.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/save_ndjson.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/save_sqlite.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/pipeline/steps/validate.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/py.typed +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/security/robots.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/security/url_validator.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull/time_utils.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull.egg-info/dependency_links.txt +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull.egg-info/entry_points.txt +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/src/docpull.egg-info/top_level.txt +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_cache_conditional_get.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_ci_policy.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_discovery.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_frontier_resume.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_integration.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_link_extractors.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_naming.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_outputs_e2e.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_pipeline.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_real_site_regressions.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_save_ndjson.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_save_sqlite.py +0 -0
- {docpull-4.0.1 → docpull-4.1.0}/tests/test_time_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: docpull
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Pull documentation from the web and convert to clean markdown
|
|
5
5
|
Author-email: Zachary Roth <support@raintree.technology>
|
|
6
6
|
Maintainer-email: Raintree Technology <support@raintree.technology>
|
|
@@ -62,6 +62,8 @@ Requires-Dist: mcp>=1.0.0; extra == "mcp"
|
|
|
62
62
|
Requires-Dist: pyjwt>=2.13.0; extra == "mcp"
|
|
63
63
|
Requires-Dist: python-multipart>=0.0.27; extra == "mcp"
|
|
64
64
|
Requires-Dist: starlette>=1.0.1; extra == "mcp"
|
|
65
|
+
Provides-Extra: parallel
|
|
66
|
+
Requires-Dist: parallel-web>=1.0.1; extra == "parallel"
|
|
65
67
|
Provides-Extra: llm
|
|
66
68
|
Requires-Dist: tiktoken>=0.7.0; extra == "llm"
|
|
67
69
|
Provides-Extra: all
|
|
@@ -73,6 +75,7 @@ Requires-Dist: mcp>=1.0.0; extra == "all"
|
|
|
73
75
|
Requires-Dist: pyjwt>=2.13.0; extra == "all"
|
|
74
76
|
Requires-Dist: python-multipart>=0.0.27; extra == "all"
|
|
75
77
|
Requires-Dist: starlette>=1.0.1; extra == "all"
|
|
78
|
+
Requires-Dist: parallel-web>=1.0.1; extra == "all"
|
|
76
79
|
Provides-Extra: dev
|
|
77
80
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
78
81
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -220,6 +223,141 @@ docpull https://site.com --profile mirror # Full archive, polite, cached, hier
|
|
|
220
223
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
221
224
|
```
|
|
222
225
|
|
|
226
|
+
## Parallel context packs
|
|
227
|
+
|
|
228
|
+
`docpull[parallel]` adds an optional source-discovery and research layer on top
|
|
229
|
+
of Parallel web intelligence APIs. Use the core crawler when you already know
|
|
230
|
+
the docs URL. Use Parallel packs when an agent needs current web sources found,
|
|
231
|
+
extracted, researched, organized, and loaded as durable local context.
|
|
232
|
+
|
|
233
|
+
Parallel handles live Search, Extract, Task, Entity Search, FindAll, TaskGroup,
|
|
234
|
+
and Monitor calls. docpull turns those results into local, inspectable packs
|
|
235
|
+
with Markdown, NDJSON, manifests, hashes, source indexes, workflow metadata, and
|
|
236
|
+
an `AGENT_CONTEXT.md` load plan so agents do not need to reverse-engineer raw API
|
|
237
|
+
responses before using the pack.
|
|
238
|
+
|
|
239
|
+
Live calls use your own Parallel API key. docpull never proxies requests through
|
|
240
|
+
a shared account or writes the key into pack artifacts. For normal PyPI installs,
|
|
241
|
+
`docpull parallel init` stores the key in a local secrets file with restricted
|
|
242
|
+
permissions so it works across shells and agent sessions. Pack artifacts can
|
|
243
|
+
still contain source content, workflow inputs/outputs, selected URLs, and
|
|
244
|
+
metadata, so treat them as research data.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
pip install 'docpull[parallel]'
|
|
248
|
+
|
|
249
|
+
# Recommended local setup. Prompts securely and writes ~/.config/docpull/secrets.env.
|
|
250
|
+
docpull parallel init
|
|
251
|
+
docpull parallel auth --json
|
|
252
|
+
|
|
253
|
+
# Optional project-local setup for agent worktrees. Writes .env.local and
|
|
254
|
+
# adds it to .gitignore.
|
|
255
|
+
docpull parallel init --project
|
|
256
|
+
|
|
257
|
+
# CI can still use a normal environment secret instead.
|
|
258
|
+
export PARALLEL_API_KEY="<your-parallel-api-key>"
|
|
259
|
+
|
|
260
|
+
docpull parallel context-pack "Compare AI web-search APIs for agents" \
|
|
261
|
+
--query "AI web search API" \
|
|
262
|
+
--query "agent web extraction API" \
|
|
263
|
+
--include-domain parallel.ai \
|
|
264
|
+
--exclude-domain onparallel.com \
|
|
265
|
+
--output-dir ./packs/ai-web-search \
|
|
266
|
+
--mode advanced \
|
|
267
|
+
--extract-limit 8 \
|
|
268
|
+
--max-estimated-cost 0.05 \
|
|
269
|
+
--task-brief
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`docpull parallel auth` checks local SDK/key presence without printing the key
|
|
273
|
+
value and reports the key source (`env`, `project_env`, `user_config`, or
|
|
274
|
+
`missing`). It does not make a live Parallel call or prove the key is valid. Use
|
|
275
|
+
`--json` when an agent or CI job needs machine-readable configuration status.
|
|
276
|
+
|
|
277
|
+
The pack contains:
|
|
278
|
+
|
|
279
|
+
- `AGENT_CONTEXT.md` — agent load plan with source order, pack signals, warnings, errors, and artifact map.
|
|
280
|
+
- `documents.ndjson` — chunked records for agents and RAG pipelines.
|
|
281
|
+
- `corpus.manifest.json` — stable docpull record manifest.
|
|
282
|
+
- `parallel.pack.json` — Parallel workflow metadata, selected URLs, errors, IDs, and usage.
|
|
283
|
+
- `sources.md` and `sources/*.md` — human-readable sources and extracted Markdown.
|
|
284
|
+
- `brief.md` when `--task-brief` is enabled.
|
|
285
|
+
|
|
286
|
+
That makes the Parallel layer more than a pass-through wrapper: Parallel finds
|
|
287
|
+
and enriches live web sources, while docpull normalizes them into a repeatable
|
|
288
|
+
context pack that can be scored, diffed, audited, and loaded offline.
|
|
289
|
+
|
|
290
|
+
For planning without spending credits, use `--dry-run`. For repeatable packs,
|
|
291
|
+
use `--include-domain`, `--exclude-domain`, `--after-date`, and `--max-search-results`
|
|
292
|
+
to pin the source policy that appears later in `parallel.pack.json`.
|
|
293
|
+
Use `--fetch-max-age-seconds`, `--fetch-timeout-seconds`,
|
|
294
|
+
`--disable-cache-fallback`, `--excerpt-chars-per-result`, and `--location`
|
|
295
|
+
when you need Parallel's freshness, excerpt-size, and geo controls.
|
|
296
|
+
|
|
297
|
+
Offline fixtures are supported for demos and tests without a Parallel account:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
docpull parallel demo --output-dir ./packs/demo
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
When working from the source checkout, the same fixture can also be imported
|
|
304
|
+
directly with `docpull parallel import ./docs/examples/parallel-search-extract.json`.
|
|
305
|
+
|
|
306
|
+
The core workflow is Search + Extract + optional Task. Broader pack workflows
|
|
307
|
+
cover Search-only snapshots, docs discovery plans, Extract-known-URL packs,
|
|
308
|
+
core-fetch-with-Parallel-fallback packs, diff briefs, Task lifecycle snapshots,
|
|
309
|
+
Entity Search, FindAll lifecycle actions, TaskGroup batches, Monitor
|
|
310
|
+
metadata/events, API specs, and local source scoring.
|
|
311
|
+
See [docs/parallel.md](docs/parallel.md) for the Parallel product cross-reference
|
|
312
|
+
and the boundary between docpull, Parallel MCP, and planned workflows.
|
|
313
|
+
|
|
314
|
+
Additional Parallel-backed artifact commands:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Readiness and API-key setup checks.
|
|
318
|
+
docpull parallel auth
|
|
319
|
+
docpull parallel init
|
|
320
|
+
docpull parallel init --project
|
|
321
|
+
|
|
322
|
+
# Search + Extract + optional Task brief.
|
|
323
|
+
docpull parallel context-pack "Compare AI web-search APIs for agents" \
|
|
324
|
+
--query "AI web search API" \
|
|
325
|
+
--include-domain parallel.ai \
|
|
326
|
+
--dry-run
|
|
327
|
+
|
|
328
|
+
# Search-only, docs-discovery, known-URL extract, fallback, and structured Task packs.
|
|
329
|
+
docpull parallel search-pack "Parallel Search API docs" --query "Parallel Search API" --dry-run
|
|
330
|
+
docpull parallel discover-docs "Find Parallel API docs" \
|
|
331
|
+
--query "Parallel Search API docs" \
|
|
332
|
+
--include-domain docs.parallel.ai \
|
|
333
|
+
--dry-run
|
|
334
|
+
docpull parallel extract-pack https://docs.parallel.ai/api-reference/search/search --dry-run
|
|
335
|
+
docpull parallel fallback-pack https://docs.parallel.ai/api-reference/search/search --dry-run
|
|
336
|
+
docpull parallel task-pack "Research Parallel Search API" \
|
|
337
|
+
--source-include-domain docs.parallel.ai \
|
|
338
|
+
--output-schema-json '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}' \
|
|
339
|
+
--dry-run
|
|
340
|
+
|
|
341
|
+
# Entity dossiers, FindAll lifecycle, batch enrichment, monitors, and API specs.
|
|
342
|
+
docpull parallel entity-pack "AI developer infrastructure companies" --entity-type companies
|
|
343
|
+
docpull parallel findall-pack "AI developer infrastructure companies" --generator preview --dry-run
|
|
344
|
+
docpull parallel findall-ingest-pack "Find companies with public API docs"
|
|
345
|
+
docpull parallel findall-result-pack findall_123 --output-dir ./packs/findall-result
|
|
346
|
+
docpull parallel taskgroup-pack ./companies.json --prompt-template "Research {company}" --dry-run
|
|
347
|
+
docpull parallel taskgroup-pack ./companies.json --prompt-template "Research {company}" --wait
|
|
348
|
+
docpull parallel monitor-pack create "New vendor pricing changes" --dry-run
|
|
349
|
+
docpull parallel monitor-pack create --type snapshot --task-run-id run_123 --dry-run
|
|
350
|
+
docpull parallel monitor-pack list --status active --output-dir ./packs/monitors
|
|
351
|
+
docpull parallel monitor-pack events monitor_123 --cursor next_cursor --output-dir ./packs/events
|
|
352
|
+
docpull parallel api-pack https://docs.parallel.ai/public-openapi.json --output-dir ./packs/parallel-openapi
|
|
353
|
+
docpull parallel run ./docs/examples/parallel-llms-api-pack.yaml --dry-run
|
|
354
|
+
docpull parallel run ./docs/examples/parallel-openapi-api-pack.yaml --dry-run
|
|
355
|
+
docpull pack score ./packs/parallel-openapi
|
|
356
|
+
docpull pack sources ./packs/parallel-openapi --require-domain docs.parallel.ai
|
|
357
|
+
docpull pack diff ./packs/old ./packs/new --markdown ./packs/changes.md
|
|
358
|
+
docpull parallel diff-brief ./packs/old ./packs/new --dry-run
|
|
359
|
+
```
|
|
360
|
+
|
|
223
361
|
## MCP server
|
|
224
362
|
|
|
225
363
|
docpull ships an MCP (Model Context Protocol) server so AI agents can call it
|
|
@@ -271,17 +409,21 @@ pip install 'docpull[mcp]'
|
|
|
271
409
|
|
|
272
410
|
See [plugin/README.md](plugin/README.md) for details.
|
|
273
411
|
|
|
274
|
-
Tools exposed (
|
|
412
|
+
Tools exposed (12 total — read tools advertise `readOnlyHint` so hosts that auto-approve safe tools won't prompt):
|
|
275
413
|
|
|
276
414
|
Read:
|
|
277
415
|
- `fetch_url(url, max_tokens?)` — one-shot fetch, no crawl. HTTPS-only, SSRF-validated.
|
|
278
|
-
- `list_sources(category?)` — show available aliases (react, nextjs, fastapi, …)
|
|
416
|
+
- `list_sources(category?)` — show available aliases (react, nextjs, parallel, fastapi, …)
|
|
279
417
|
- `list_indexed()` — what has been fetched locally, with last-fetched age
|
|
280
418
|
- `grep_docs(pattern, library?, limit?, context?)` — regex search across fetched Markdown (length-capped + wall-clock budgeted to mitigate ReDoS)
|
|
281
419
|
- `read_doc(library, path, line_start?, line_end?)` — read a specific cached file, optionally line-sliced
|
|
420
|
+
- `pack_score(pack_dir, required_domains?)` — score a local context pack for agent readiness.
|
|
421
|
+
- `pack_diff(old_pack_dir, new_pack_dir)` — compare pack URL sets and content hashes.
|
|
282
422
|
|
|
283
423
|
Write:
|
|
284
424
|
- `ensure_docs(source, force?, profile?)` — fetch a named library (cached 7 days). Forwards progress to clients that supply a `progressToken`.
|
|
425
|
+
- `parallel_context_pack(...)` — build or dry-run a Parallel Search + Extract pack without shelling out.
|
|
426
|
+
- `parallel_api_pack(source, kind?, output_dir?)` — build a pack from `llms.txt` or OpenAPI.
|
|
285
427
|
- `add_source(name, url, description?, category?, max_pages?, force?)` — register a user alias (HTTPS-only, atomic write to `sources.yaml`).
|
|
286
428
|
- `remove_source(name, delete_cache?)` — drop a user alias and (optionally) its cached docs.
|
|
287
429
|
|
|
@@ -388,15 +530,16 @@ End-to-end numbers from `tests/benchmarks/test_10k_pages.py` against a
|
|
|
388
530
|
synthetic 10,000-page localhost site (RAG profile, `max_concurrent=50`,
|
|
389
531
|
`per_host_concurrent=50`, HTTP keep-alive, 5% injected duplicate content).
|
|
390
532
|
The benchmark emits progress every 1,000 pages plus a final JSON report for
|
|
391
|
-
trend tooling.
|
|
533
|
+
trend tooling. Wall time varies by machine; the value below is the latest
|
|
534
|
+
local audit run.
|
|
392
535
|
|
|
393
536
|
| Metric | Value |
|
|
394
537
|
|---|---|
|
|
395
|
-
| Total wall time | ~
|
|
538
|
+
| Total wall time | ~309 s |
|
|
396
539
|
| Pages fetched / skipped / failed | 9,501 / 499 / 0 |
|
|
397
|
-
| Time to first saved page | ~
|
|
398
|
-
| Per-page latency p50 / p95 / p99 | ~0 /
|
|
399
|
-
| Peak RSS delta from baseline | ~
|
|
540
|
+
| Time to first saved page | ~119 ms |
|
|
541
|
+
| Per-page latency p50 / p95 / p99 | ~0 / 168 / 271 ms |
|
|
542
|
+
| Peak RSS delta from baseline | ~93 MB |
|
|
400
543
|
| Cache manifest size on disk | ~8.9 MB |
|
|
401
544
|
| Duplicates detected (5% injected) | 499 / 500 |
|
|
402
545
|
|
|
@@ -130,6 +130,141 @@ docpull https://site.com --profile mirror # Full archive, polite, cached, hier
|
|
|
130
130
|
docpull https://site.com --profile quick # Sampling: 50 pages, depth 2.
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
## Parallel context packs
|
|
134
|
+
|
|
135
|
+
`docpull[parallel]` adds an optional source-discovery and research layer on top
|
|
136
|
+
of Parallel web intelligence APIs. Use the core crawler when you already know
|
|
137
|
+
the docs URL. Use Parallel packs when an agent needs current web sources found,
|
|
138
|
+
extracted, researched, organized, and loaded as durable local context.
|
|
139
|
+
|
|
140
|
+
Parallel handles live Search, Extract, Task, Entity Search, FindAll, TaskGroup,
|
|
141
|
+
and Monitor calls. docpull turns those results into local, inspectable packs
|
|
142
|
+
with Markdown, NDJSON, manifests, hashes, source indexes, workflow metadata, and
|
|
143
|
+
an `AGENT_CONTEXT.md` load plan so agents do not need to reverse-engineer raw API
|
|
144
|
+
responses before using the pack.
|
|
145
|
+
|
|
146
|
+
Live calls use your own Parallel API key. docpull never proxies requests through
|
|
147
|
+
a shared account or writes the key into pack artifacts. For normal PyPI installs,
|
|
148
|
+
`docpull parallel init` stores the key in a local secrets file with restricted
|
|
149
|
+
permissions so it works across shells and agent sessions. Pack artifacts can
|
|
150
|
+
still contain source content, workflow inputs/outputs, selected URLs, and
|
|
151
|
+
metadata, so treat them as research data.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pip install 'docpull[parallel]'
|
|
155
|
+
|
|
156
|
+
# Recommended local setup. Prompts securely and writes ~/.config/docpull/secrets.env.
|
|
157
|
+
docpull parallel init
|
|
158
|
+
docpull parallel auth --json
|
|
159
|
+
|
|
160
|
+
# Optional project-local setup for agent worktrees. Writes .env.local and
|
|
161
|
+
# adds it to .gitignore.
|
|
162
|
+
docpull parallel init --project
|
|
163
|
+
|
|
164
|
+
# CI can still use a normal environment secret instead.
|
|
165
|
+
export PARALLEL_API_KEY="<your-parallel-api-key>"
|
|
166
|
+
|
|
167
|
+
docpull parallel context-pack "Compare AI web-search APIs for agents" \
|
|
168
|
+
--query "AI web search API" \
|
|
169
|
+
--query "agent web extraction API" \
|
|
170
|
+
--include-domain parallel.ai \
|
|
171
|
+
--exclude-domain onparallel.com \
|
|
172
|
+
--output-dir ./packs/ai-web-search \
|
|
173
|
+
--mode advanced \
|
|
174
|
+
--extract-limit 8 \
|
|
175
|
+
--max-estimated-cost 0.05 \
|
|
176
|
+
--task-brief
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`docpull parallel auth` checks local SDK/key presence without printing the key
|
|
180
|
+
value and reports the key source (`env`, `project_env`, `user_config`, or
|
|
181
|
+
`missing`). It does not make a live Parallel call or prove the key is valid. Use
|
|
182
|
+
`--json` when an agent or CI job needs machine-readable configuration status.
|
|
183
|
+
|
|
184
|
+
The pack contains:
|
|
185
|
+
|
|
186
|
+
- `AGENT_CONTEXT.md` — agent load plan with source order, pack signals, warnings, errors, and artifact map.
|
|
187
|
+
- `documents.ndjson` — chunked records for agents and RAG pipelines.
|
|
188
|
+
- `corpus.manifest.json` — stable docpull record manifest.
|
|
189
|
+
- `parallel.pack.json` — Parallel workflow metadata, selected URLs, errors, IDs, and usage.
|
|
190
|
+
- `sources.md` and `sources/*.md` — human-readable sources and extracted Markdown.
|
|
191
|
+
- `brief.md` when `--task-brief` is enabled.
|
|
192
|
+
|
|
193
|
+
That makes the Parallel layer more than a pass-through wrapper: Parallel finds
|
|
194
|
+
and enriches live web sources, while docpull normalizes them into a repeatable
|
|
195
|
+
context pack that can be scored, diffed, audited, and loaded offline.
|
|
196
|
+
|
|
197
|
+
For planning without spending credits, use `--dry-run`. For repeatable packs,
|
|
198
|
+
use `--include-domain`, `--exclude-domain`, `--after-date`, and `--max-search-results`
|
|
199
|
+
to pin the source policy that appears later in `parallel.pack.json`.
|
|
200
|
+
Use `--fetch-max-age-seconds`, `--fetch-timeout-seconds`,
|
|
201
|
+
`--disable-cache-fallback`, `--excerpt-chars-per-result`, and `--location`
|
|
202
|
+
when you need Parallel's freshness, excerpt-size, and geo controls.
|
|
203
|
+
|
|
204
|
+
Offline fixtures are supported for demos and tests without a Parallel account:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
docpull parallel demo --output-dir ./packs/demo
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
When working from the source checkout, the same fixture can also be imported
|
|
211
|
+
directly with `docpull parallel import ./docs/examples/parallel-search-extract.json`.
|
|
212
|
+
|
|
213
|
+
The core workflow is Search + Extract + optional Task. Broader pack workflows
|
|
214
|
+
cover Search-only snapshots, docs discovery plans, Extract-known-URL packs,
|
|
215
|
+
core-fetch-with-Parallel-fallback packs, diff briefs, Task lifecycle snapshots,
|
|
216
|
+
Entity Search, FindAll lifecycle actions, TaskGroup batches, Monitor
|
|
217
|
+
metadata/events, API specs, and local source scoring.
|
|
218
|
+
See [docs/parallel.md](docs/parallel.md) for the Parallel product cross-reference
|
|
219
|
+
and the boundary between docpull, Parallel MCP, and planned workflows.
|
|
220
|
+
|
|
221
|
+
Additional Parallel-backed artifact commands:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
# Readiness and API-key setup checks.
|
|
225
|
+
docpull parallel auth
|
|
226
|
+
docpull parallel init
|
|
227
|
+
docpull parallel init --project
|
|
228
|
+
|
|
229
|
+
# Search + Extract + optional Task brief.
|
|
230
|
+
docpull parallel context-pack "Compare AI web-search APIs for agents" \
|
|
231
|
+
--query "AI web search API" \
|
|
232
|
+
--include-domain parallel.ai \
|
|
233
|
+
--dry-run
|
|
234
|
+
|
|
235
|
+
# Search-only, docs-discovery, known-URL extract, fallback, and structured Task packs.
|
|
236
|
+
docpull parallel search-pack "Parallel Search API docs" --query "Parallel Search API" --dry-run
|
|
237
|
+
docpull parallel discover-docs "Find Parallel API docs" \
|
|
238
|
+
--query "Parallel Search API docs" \
|
|
239
|
+
--include-domain docs.parallel.ai \
|
|
240
|
+
--dry-run
|
|
241
|
+
docpull parallel extract-pack https://docs.parallel.ai/api-reference/search/search --dry-run
|
|
242
|
+
docpull parallel fallback-pack https://docs.parallel.ai/api-reference/search/search --dry-run
|
|
243
|
+
docpull parallel task-pack "Research Parallel Search API" \
|
|
244
|
+
--source-include-domain docs.parallel.ai \
|
|
245
|
+
--output-schema-json '{"type":"object","properties":{"summary":{"type":"string"}},"required":["summary"]}' \
|
|
246
|
+
--dry-run
|
|
247
|
+
|
|
248
|
+
# Entity dossiers, FindAll lifecycle, batch enrichment, monitors, and API specs.
|
|
249
|
+
docpull parallel entity-pack "AI developer infrastructure companies" --entity-type companies
|
|
250
|
+
docpull parallel findall-pack "AI developer infrastructure companies" --generator preview --dry-run
|
|
251
|
+
docpull parallel findall-ingest-pack "Find companies with public API docs"
|
|
252
|
+
docpull parallel findall-result-pack findall_123 --output-dir ./packs/findall-result
|
|
253
|
+
docpull parallel taskgroup-pack ./companies.json --prompt-template "Research {company}" --dry-run
|
|
254
|
+
docpull parallel taskgroup-pack ./companies.json --prompt-template "Research {company}" --wait
|
|
255
|
+
docpull parallel monitor-pack create "New vendor pricing changes" --dry-run
|
|
256
|
+
docpull parallel monitor-pack create --type snapshot --task-run-id run_123 --dry-run
|
|
257
|
+
docpull parallel monitor-pack list --status active --output-dir ./packs/monitors
|
|
258
|
+
docpull parallel monitor-pack events monitor_123 --cursor next_cursor --output-dir ./packs/events
|
|
259
|
+
docpull parallel api-pack https://docs.parallel.ai/public-openapi.json --output-dir ./packs/parallel-openapi
|
|
260
|
+
docpull parallel run ./docs/examples/parallel-llms-api-pack.yaml --dry-run
|
|
261
|
+
docpull parallel run ./docs/examples/parallel-openapi-api-pack.yaml --dry-run
|
|
262
|
+
docpull pack score ./packs/parallel-openapi
|
|
263
|
+
docpull pack sources ./packs/parallel-openapi --require-domain docs.parallel.ai
|
|
264
|
+
docpull pack diff ./packs/old ./packs/new --markdown ./packs/changes.md
|
|
265
|
+
docpull parallel diff-brief ./packs/old ./packs/new --dry-run
|
|
266
|
+
```
|
|
267
|
+
|
|
133
268
|
## MCP server
|
|
134
269
|
|
|
135
270
|
docpull ships an MCP (Model Context Protocol) server so AI agents can call it
|
|
@@ -181,17 +316,21 @@ pip install 'docpull[mcp]'
|
|
|
181
316
|
|
|
182
317
|
See [plugin/README.md](plugin/README.md) for details.
|
|
183
318
|
|
|
184
|
-
Tools exposed (
|
|
319
|
+
Tools exposed (12 total — read tools advertise `readOnlyHint` so hosts that auto-approve safe tools won't prompt):
|
|
185
320
|
|
|
186
321
|
Read:
|
|
187
322
|
- `fetch_url(url, max_tokens?)` — one-shot fetch, no crawl. HTTPS-only, SSRF-validated.
|
|
188
|
-
- `list_sources(category?)` — show available aliases (react, nextjs, fastapi, …)
|
|
323
|
+
- `list_sources(category?)` — show available aliases (react, nextjs, parallel, fastapi, …)
|
|
189
324
|
- `list_indexed()` — what has been fetched locally, with last-fetched age
|
|
190
325
|
- `grep_docs(pattern, library?, limit?, context?)` — regex search across fetched Markdown (length-capped + wall-clock budgeted to mitigate ReDoS)
|
|
191
326
|
- `read_doc(library, path, line_start?, line_end?)` — read a specific cached file, optionally line-sliced
|
|
327
|
+
- `pack_score(pack_dir, required_domains?)` — score a local context pack for agent readiness.
|
|
328
|
+
- `pack_diff(old_pack_dir, new_pack_dir)` — compare pack URL sets and content hashes.
|
|
192
329
|
|
|
193
330
|
Write:
|
|
194
331
|
- `ensure_docs(source, force?, profile?)` — fetch a named library (cached 7 days). Forwards progress to clients that supply a `progressToken`.
|
|
332
|
+
- `parallel_context_pack(...)` — build or dry-run a Parallel Search + Extract pack without shelling out.
|
|
333
|
+
- `parallel_api_pack(source, kind?, output_dir?)` — build a pack from `llms.txt` or OpenAPI.
|
|
195
334
|
- `add_source(name, url, description?, category?, max_pages?, force?)` — register a user alias (HTTPS-only, atomic write to `sources.yaml`).
|
|
196
335
|
- `remove_source(name, delete_cache?)` — drop a user alias and (optionally) its cached docs.
|
|
197
336
|
|
|
@@ -298,15 +437,16 @@ End-to-end numbers from `tests/benchmarks/test_10k_pages.py` against a
|
|
|
298
437
|
synthetic 10,000-page localhost site (RAG profile, `max_concurrent=50`,
|
|
299
438
|
`per_host_concurrent=50`, HTTP keep-alive, 5% injected duplicate content).
|
|
300
439
|
The benchmark emits progress every 1,000 pages plus a final JSON report for
|
|
301
|
-
trend tooling.
|
|
440
|
+
trend tooling. Wall time varies by machine; the value below is the latest
|
|
441
|
+
local audit run.
|
|
302
442
|
|
|
303
443
|
| Metric | Value |
|
|
304
444
|
|---|---|
|
|
305
|
-
| Total wall time | ~
|
|
445
|
+
| Total wall time | ~309 s |
|
|
306
446
|
| Pages fetched / skipped / failed | 9,501 / 499 / 0 |
|
|
307
|
-
| Time to first saved page | ~
|
|
308
|
-
| Per-page latency p50 / p95 / p99 | ~0 /
|
|
309
|
-
| Peak RSS delta from baseline | ~
|
|
447
|
+
| Time to first saved page | ~119 ms |
|
|
448
|
+
| Per-page latency p50 / p95 / p99 | ~0 / 168 / 271 ms |
|
|
449
|
+
| Peak RSS delta from baseline | ~93 MB |
|
|
310
450
|
| Cache manifest size on disk | ~8.9 MB |
|
|
311
451
|
| Duplicates detected (5% injected) | 499 / 500 |
|
|
312
452
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "docpull"
|
|
7
|
-
version = "4.0
|
|
7
|
+
version = "4.1.0"
|
|
8
8
|
dynamic = []
|
|
9
9
|
description = "Pull documentation from the web and convert to clean markdown"
|
|
10
10
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
@@ -94,6 +94,9 @@ mcp = [
|
|
|
94
94
|
"python-multipart>=0.0.27",
|
|
95
95
|
"starlette>=1.0.1",
|
|
96
96
|
]
|
|
97
|
+
parallel = [
|
|
98
|
+
"parallel-web>=1.0.1",
|
|
99
|
+
]
|
|
97
100
|
llm = [
|
|
98
101
|
"tiktoken>=0.7.0",
|
|
99
102
|
]
|
|
@@ -106,6 +109,7 @@ all = [
|
|
|
106
109
|
"pyjwt>=2.13.0",
|
|
107
110
|
"python-multipart>=0.0.27",
|
|
108
111
|
"starlette>=1.0.1",
|
|
112
|
+
"parallel-web>=1.0.1",
|
|
109
113
|
]
|
|
110
114
|
dev = [
|
|
111
115
|
"pytest>=7.0.0",
|
|
@@ -138,7 +142,7 @@ where = ["src"]
|
|
|
138
142
|
include = ["docpull*"]
|
|
139
143
|
|
|
140
144
|
[tool.setuptools.package-data]
|
|
141
|
-
docpull = ["py.typed"]
|
|
145
|
+
docpull = ["py.typed", "fixtures/*.json"]
|
|
142
146
|
|
|
143
147
|
[tool.ruff]
|
|
144
148
|
line-length = 110
|
|
@@ -557,6 +557,10 @@ def run_fetcher(args: argparse.Namespace) -> int:
|
|
|
557
557
|
failure_skips = {
|
|
558
558
|
SkipReason.URL_VALIDATION_FAILED,
|
|
559
559
|
SkipReason.ROBOTS_DISALLOWED,
|
|
560
|
+
SkipReason.HTTP_ERROR,
|
|
561
|
+
SkipReason.INVALID_CONTENT_TYPE,
|
|
562
|
+
SkipReason.NO_CONTENT_EXTRACTED,
|
|
563
|
+
SkipReason.NO_CONTENT_TO_SAVE,
|
|
560
564
|
}
|
|
561
565
|
return 1 if ctx.skip_code in failure_skips else 0
|
|
562
566
|
if not args.quiet:
|
|
@@ -662,6 +666,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
662
666
|
from .mcp.server import run_mcp_server
|
|
663
667
|
|
|
664
668
|
return run_mcp_server(raw_argv[1:])
|
|
669
|
+
if raw_argv and raw_argv[0] == "parallel":
|
|
670
|
+
from .parallel_workflows import run_parallel_cli
|
|
671
|
+
|
|
672
|
+
return run_parallel_cli(raw_argv[1:])
|
|
673
|
+
if raw_argv and raw_argv[0] == "pack":
|
|
674
|
+
from .pack_tools import run_pack_cli
|
|
675
|
+
|
|
676
|
+
return run_pack_cli(raw_argv[1:])
|
|
665
677
|
|
|
666
678
|
parser = create_parser()
|
|
667
679
|
args = parser.parse_args(raw_argv)
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
_HEADING_RE = re.compile(r"^(#{1,6})\s+"
|
|
17
|
+
_HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*#*\s*$")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass
|
|
@@ -88,19 +88,38 @@ def _split_on_headings(body: str) -> list[tuple[str | None, str]]:
|
|
|
88
88
|
The first tuple may have ``heading=None`` for any preamble before the
|
|
89
89
|
first heading.
|
|
90
90
|
"""
|
|
91
|
-
matches =
|
|
91
|
+
matches: list[tuple[int, str]] = []
|
|
92
|
+
in_fence = False
|
|
93
|
+
fence_marker = ""
|
|
94
|
+
offset = 0
|
|
95
|
+
for line in body.splitlines(keepends=True):
|
|
96
|
+
stripped = line.strip()
|
|
97
|
+
if stripped.startswith(("```", "~~~")):
|
|
98
|
+
marker = stripped[:3]
|
|
99
|
+
if not in_fence:
|
|
100
|
+
in_fence = True
|
|
101
|
+
fence_marker = marker
|
|
102
|
+
elif stripped.startswith(fence_marker):
|
|
103
|
+
in_fence = False
|
|
104
|
+
fence_marker = ""
|
|
105
|
+
offset += len(line)
|
|
106
|
+
continue
|
|
107
|
+
if not in_fence:
|
|
108
|
+
match = _HEADING_RE.match(line.rstrip("\n"))
|
|
109
|
+
if match:
|
|
110
|
+
matches.append((offset, match.group(2).strip()))
|
|
111
|
+
offset += len(line)
|
|
112
|
+
|
|
92
113
|
if not matches:
|
|
93
114
|
return [(None, body)]
|
|
94
115
|
|
|
95
116
|
sections: list[tuple[str | None, str]] = []
|
|
96
|
-
if matches[0]
|
|
97
|
-
sections.append((None, body[: matches[0]
|
|
117
|
+
if matches[0][0] > 0:
|
|
118
|
+
sections.append((None, body[: matches[0][0]]))
|
|
98
119
|
|
|
99
|
-
for i,
|
|
100
|
-
|
|
101
|
-
end = matches[i + 1].start() if i + 1 < len(matches) else len(body)
|
|
120
|
+
for i, (start, heading_line) in enumerate(matches):
|
|
121
|
+
end = matches[i + 1][0] if i + 1 < len(matches) else len(body)
|
|
102
122
|
chunk = body[start:end]
|
|
103
|
-
heading_line = chunk.split("\n", 1)[0].lstrip("#").strip()
|
|
104
123
|
sections.append((heading_line, chunk))
|
|
105
124
|
return sections
|
|
106
125
|
|
|
@@ -109,10 +128,18 @@ def _split_paragraphs(section: str) -> list[str]:
|
|
|
109
128
|
# Split on blank lines while preserving code blocks intact.
|
|
110
129
|
parts: list[str] = []
|
|
111
130
|
in_code = False
|
|
131
|
+
fence_marker = ""
|
|
112
132
|
buf: list[str] = []
|
|
113
133
|
for line in section.split("\n"):
|
|
114
|
-
|
|
115
|
-
|
|
134
|
+
stripped = line.strip()
|
|
135
|
+
if stripped.startswith(("```", "~~~")):
|
|
136
|
+
marker = stripped[:3]
|
|
137
|
+
if not in_code:
|
|
138
|
+
in_code = True
|
|
139
|
+
fence_marker = marker
|
|
140
|
+
elif stripped.startswith(fence_marker):
|
|
141
|
+
in_code = False
|
|
142
|
+
fence_marker = ""
|
|
116
143
|
if line.strip() == "" and not in_code and buf:
|
|
117
144
|
parts.append("\n".join(buf))
|
|
118
145
|
buf = []
|
|
@@ -155,16 +182,17 @@ def chunk_markdown(
|
|
|
155
182
|
chunks: list[Chunk] = []
|
|
156
183
|
buf_parts: list[str] = []
|
|
157
184
|
buf_tokens = 0
|
|
158
|
-
|
|
185
|
+
buf_heading: str | None = None
|
|
159
186
|
|
|
160
187
|
def flush() -> None:
|
|
161
|
-
nonlocal buf_parts, buf_tokens
|
|
188
|
+
nonlocal buf_parts, buf_tokens, buf_heading
|
|
162
189
|
if not buf_parts:
|
|
163
190
|
return
|
|
164
191
|
text = "\n\n".join(part.strip() for part in buf_parts if part.strip())
|
|
165
192
|
if not text:
|
|
166
193
|
buf_parts = []
|
|
167
194
|
buf_tokens = 0
|
|
195
|
+
buf_heading = None
|
|
168
196
|
return
|
|
169
197
|
prefix = frontmatter if keep_frontmatter_in_first and not chunks else ""
|
|
170
198
|
final = (prefix + text).strip() + "\n"
|
|
@@ -173,25 +201,31 @@ def chunk_markdown(
|
|
|
173
201
|
index=len(chunks),
|
|
174
202
|
text=final,
|
|
175
203
|
token_count=counter.count(final),
|
|
176
|
-
heading=
|
|
204
|
+
heading=buf_heading,
|
|
177
205
|
)
|
|
178
206
|
)
|
|
179
207
|
buf_parts = []
|
|
180
208
|
buf_tokens = 0
|
|
209
|
+
buf_heading = None
|
|
210
|
+
|
|
211
|
+
def append_part(part: str, token_count: int, heading: str | None) -> None:
|
|
212
|
+
nonlocal buf_tokens, buf_heading
|
|
213
|
+
if not part.strip():
|
|
214
|
+
return
|
|
215
|
+
if buf_heading is None and heading is not None:
|
|
216
|
+
buf_heading = heading
|
|
217
|
+
buf_parts.append(part)
|
|
218
|
+
buf_tokens += token_count
|
|
181
219
|
|
|
182
220
|
for heading, section in sections:
|
|
183
|
-
if heading is not None:
|
|
184
|
-
current_heading = heading
|
|
185
221
|
section_tokens = counter.count(section)
|
|
186
222
|
if section_tokens <= max_tokens and buf_tokens + section_tokens <= max_tokens:
|
|
187
|
-
|
|
188
|
-
buf_tokens += section_tokens
|
|
223
|
+
append_part(section, section_tokens, heading)
|
|
189
224
|
continue
|
|
190
225
|
# Section alone fits but buffer is full: flush then add.
|
|
191
226
|
if section_tokens <= max_tokens:
|
|
192
227
|
flush()
|
|
193
|
-
|
|
194
|
-
buf_tokens = section_tokens
|
|
228
|
+
append_part(section, section_tokens, heading)
|
|
195
229
|
continue
|
|
196
230
|
# Section too large: flush current buffer, then split paragraphs.
|
|
197
231
|
flush()
|
|
@@ -207,15 +241,14 @@ def chunk_markdown(
|
|
|
207
241
|
Chunk(
|
|
208
242
|
index=len(chunks),
|
|
209
243
|
text=text,
|
|
210
|
-
token_count=
|
|
211
|
-
heading=
|
|
244
|
+
token_count=counter.count(text),
|
|
245
|
+
heading=heading,
|
|
212
246
|
)
|
|
213
247
|
)
|
|
214
248
|
continue
|
|
215
249
|
if buf_tokens + p_tokens > max_tokens:
|
|
216
250
|
flush()
|
|
217
|
-
|
|
218
|
-
buf_tokens += p_tokens
|
|
251
|
+
append_part(para, p_tokens, heading)
|
|
219
252
|
|
|
220
253
|
flush()
|
|
221
254
|
return chunks
|