context-atlas 1.0.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.
Files changed (125) hide show
  1. context_atlas-1.0.0/.gitignore +13 -0
  2. context_atlas-1.0.0/LICENSE +21 -0
  3. context_atlas-1.0.0/PKG-INFO +196 -0
  4. context_atlas-1.0.0/docs/python.md +176 -0
  5. context_atlas-1.0.0/pyproject.toml +53 -0
  6. context_atlas-1.0.0/python/src/context_atlas/__init__.py +47 -0
  7. context_atlas-1.0.0/python/src/context_atlas/__main__.py +4 -0
  8. context_atlas-1.0.0/python/src/context_atlas/blocks.py +82 -0
  9. context_atlas-1.0.0/python/src/context_atlas/chunk.py +61 -0
  10. context_atlas-1.0.0/python/src/context_atlas/chunker.py +52 -0
  11. context_atlas-1.0.0/python/src/context_atlas/chunking/__init__.py +1 -0
  12. context_atlas-1.0.0/python/src/context_atlas/chunking/breadcrumbs.py +45 -0
  13. context_atlas-1.0.0/python/src/context_atlas/chunking/cut_planner.py +52 -0
  14. context_atlas-1.0.0/python/src/context_atlas/chunking/packer.py +239 -0
  15. context_atlas-1.0.0/python/src/context_atlas/chunking/seam_scorer.py +94 -0
  16. context_atlas-1.0.0/python/src/context_atlas/chunking/topic_shifts.py +118 -0
  17. context_atlas-1.0.0/python/src/context_atlas/compat.py +323 -0
  18. context_atlas-1.0.0/python/src/context_atlas/config.py +117 -0
  19. context_atlas-1.0.0/python/src/context_atlas/console.py +409 -0
  20. context_atlas-1.0.0/python/src/context_atlas/context.py +24 -0
  21. context_atlas-1.0.0/python/src/context_atlas/document.py +165 -0
  22. context_atlas-1.0.0/python/src/context_atlas/errors.py +2 -0
  23. context_atlas-1.0.0/python/src/context_atlas/index.py +474 -0
  24. context_atlas-1.0.0/python/src/context_atlas/jev.py +62 -0
  25. context_atlas-1.0.0/python/src/context_atlas/markup/__init__.py +1 -0
  26. context_atlas-1.0.0/python/src/context_atlas/markup/json_outline.py +120 -0
  27. context_atlas-1.0.0/python/src/context_atlas/markup/jsonl_outline.py +43 -0
  28. context_atlas-1.0.0/python/src/context_atlas/markup/profile.py +36 -0
  29. context_atlas-1.0.0/python/src/context_atlas/markup/subtrees.py +336 -0
  30. context_atlas-1.0.0/python/src/context_atlas/markup/toml_outline.py +236 -0
  31. context_atlas-1.0.0/python/src/context_atlas/markup/xml_outline.py +305 -0
  32. context_atlas-1.0.0/python/src/context_atlas/markup/yaml_outline.py +208 -0
  33. context_atlas-1.0.0/python/src/context_atlas/parsing/__init__.py +7 -0
  34. context_atlas-1.0.0/python/src/context_atlas/parsing/asciidoc.py +401 -0
  35. context_atlas-1.0.0/python/src/context_atlas/parsing/block_builder.py +171 -0
  36. context_atlas-1.0.0/python/src/context_atlas/parsing/block_list.py +24 -0
  37. context_atlas-1.0.0/python/src/context_atlas/parsing/captions.py +110 -0
  38. context_atlas-1.0.0/python/src/context_atlas/parsing/chat/__init__.py +171 -0
  39. context_atlas-1.0.0/python/src/context_atlas/parsing/chat/chat_json.py +297 -0
  40. context_atlas-1.0.0/python/src/context_atlas/parsing/chat/messages.py +439 -0
  41. context_atlas-1.0.0/python/src/context_atlas/parsing/chat/parts.py +284 -0
  42. context_atlas-1.0.0/python/src/context_atlas/parsing/chat/thread.py +201 -0
  43. context_atlas-1.0.0/python/src/context_atlas/parsing/docx.py +815 -0
  44. context_atlas-1.0.0/python/src/context_atlas/parsing/formats.py +97 -0
  45. context_atlas-1.0.0/python/src/context_atlas/parsing/html.py +354 -0
  46. context_atlas-1.0.0/python/src/context_atlas/parsing/html_tree/__init__.py +1 -0
  47. context_atlas-1.0.0/python/src/context_atlas/parsing/html_tree/content.py +275 -0
  48. context_atlas-1.0.0/python/src/context_atlas/parsing/html_tree/element.py +45 -0
  49. context_atlas-1.0.0/python/src/context_atlas/parsing/html_tree/entities.py +88 -0
  50. context_atlas-1.0.0/python/src/context_atlas/parsing/html_tree/table.py +132 -0
  51. context_atlas-1.0.0/python/src/context_atlas/parsing/html_tree/tree.py +202 -0
  52. context_atlas-1.0.0/python/src/context_atlas/parsing/latex.py +482 -0
  53. context_atlas-1.0.0/python/src/context_atlas/parsing/latex_inline.py +419 -0
  54. context_atlas-1.0.0/python/src/context_atlas/parsing/latex_table.py +128 -0
  55. context_atlas-1.0.0/python/src/context_atlas/parsing/markdown.py +117 -0
  56. context_atlas-1.0.0/python/src/context_atlas/parsing/mdx.py +137 -0
  57. context_atlas-1.0.0/python/src/context_atlas/parsing/notebook.py +143 -0
  58. context_atlas-1.0.0/python/src/context_atlas/parsing/org.py +255 -0
  59. context_atlas-1.0.0/python/src/context_atlas/parsing/rst.py +493 -0
  60. context_atlas-1.0.0/python/src/context_atlas/parsing/srt.py +43 -0
  61. context_atlas-1.0.0/python/src/context_atlas/parsing/text.py +92 -0
  62. context_atlas-1.0.0/python/src/context_atlas/parsing/vtt.py +117 -0
  63. context_atlas-1.0.0/python/src/context_atlas/passage.py +46 -0
  64. context_atlas-1.0.0/python/src/context_atlas/processor.py +386 -0
  65. context_atlas-1.0.0/python/src/context_atlas/prose/__init__.py +1 -0
  66. context_atlas-1.0.0/python/src/context_atlas/prose/appender.py +162 -0
  67. context_atlas-1.0.0/python/src/context_atlas/prose/conversation.py +491 -0
  68. context_atlas-1.0.0/python/src/context_atlas/prose/conversation_retriever.py +331 -0
  69. context_atlas-1.0.0/python/src/context_atlas/prose/detector.py +86 -0
  70. context_atlas-1.0.0/python/src/context_atlas/prose/entries.py +302 -0
  71. context_atlas-1.0.0/python/src/context_atlas/prose/links.py +260 -0
  72. context_atlas-1.0.0/python/src/context_atlas/prose/profile.py +188 -0
  73. context_atlas-1.0.0/python/src/context_atlas/prose/profiles.py +128 -0
  74. context_atlas-1.0.0/python/src/context_atlas/prose/retriever.py +699 -0
  75. context_atlas-1.0.0/python/src/context_atlas/prose/speakers.py +184 -0
  76. context_atlas-1.0.0/python/src/context_atlas/py.typed +0 -0
  77. context_atlas-1.0.0/python/src/context_atlas/reranker.py +105 -0
  78. context_atlas-1.0.0/python/src/context_atlas/tabular/__init__.py +4 -0
  79. context_atlas-1.0.0/python/src/context_atlas/tabular/card.py +71 -0
  80. context_atlas-1.0.0/python/src/context_atlas/tabular/cells.py +95 -0
  81. context_atlas-1.0.0/python/src/context_atlas/tabular/executor.py +511 -0
  82. context_atlas-1.0.0/python/src/context_atlas/tabular/grid.py +312 -0
  83. context_atlas-1.0.0/python/src/context_atlas/tabular/number.py +37 -0
  84. context_atlas-1.0.0/python/src/context_atlas/tabular/planner.py +521 -0
  85. context_atlas-1.0.0/python/src/context_atlas/tabular/profile.py +124 -0
  86. context_atlas-1.0.0/python/src/context_atlas/tabular/tables.py +371 -0
  87. context_atlas-1.0.0/python/src/context_atlas/text.py +122 -0
  88. context_atlas-1.0.0/python/tests/support.py +83 -0
  89. context_atlas-1.0.0/python/tests/test_chat.py +69 -0
  90. context_atlas-1.0.0/python/tests/test_config.py +49 -0
  91. context_atlas-1.0.0/python/tests/test_console.py +202 -0
  92. context_atlas-1.0.0/python/tests/test_documents.py +142 -0
  93. context_atlas-1.0.0/python/tests/test_file_store.py +64 -0
  94. context_atlas-1.0.0/python/tests/test_fixtures.py +148 -0
  95. context_atlas-1.0.0/python/tests/test_ingest_file.py +41 -0
  96. context_atlas-1.0.0/python/tests/test_jev_hook.py +57 -0
  97. context_atlas-1.0.0/python/tests/test_nearest.py +40 -0
  98. context_atlas-1.0.0/python/tests/test_prose.py +78 -0
  99. context_atlas-1.0.0/python/tests/test_tabular.py +134 -0
  100. context_atlas-1.0.0/spec/behaviour.md +329 -0
  101. context_atlas-1.0.0/spec/config.schema.json +28 -0
  102. context_atlas-1.0.0/spec/document.md +119 -0
  103. context_atlas-1.0.0/spec/fixtures/docx-code.docx +0 -0
  104. context_atlas-1.0.0/spec/fixtures/docx-doctype.docx +0 -0
  105. context_atlas-1.0.0/spec/fixtures/docx-lists.docx +0 -0
  106. context_atlas-1.0.0/spec/fixtures/docx-malformed.docx +0 -0
  107. context_atlas-1.0.0/spec/fixtures/docx-no-document.docx +0 -0
  108. context_atlas-1.0.0/spec/fixtures/docx-not-a-zip.docx +1 -0
  109. context_atlas-1.0.0/spec/fixtures/docx-tables.docx +0 -0
  110. context_atlas-1.0.0/spec/fixtures/docx-text.docx +0 -0
  111. context_atlas-1.0.0/spec/fixtures/workbook.xlsx +0 -0
  112. context_atlas-1.0.0/spec/fixtures.json +41998 -0
  113. context_atlas-1.0.0/spec/formats/asciidoc.md +158 -0
  114. context_atlas-1.0.0/spec/formats/chat.md +385 -0
  115. context_atlas-1.0.0/spec/formats/docx.md +280 -0
  116. context_atlas-1.0.0/spec/formats/html.md +309 -0
  117. context_atlas-1.0.0/spec/formats/latex.md +317 -0
  118. context_atlas-1.0.0/spec/formats/mdx.md +75 -0
  119. context_atlas-1.0.0/spec/formats/notebook.md +57 -0
  120. context_atlas-1.0.0/spec/formats/org.md +105 -0
  121. context_atlas-1.0.0/spec/formats/rst.md +219 -0
  122. context_atlas-1.0.0/spec/formats/transcripts.md +112 -0
  123. context_atlas-1.0.0/spec/markup.md +303 -0
  124. context_atlas-1.0.0/spec/prose.md +342 -0
  125. context_atlas-1.0.0/spec/tabular.md +287 -0
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ /vendor/
3
+ /node_modules/
4
+ /js/dist/
5
+ .env
6
+ .phpunit.result.cache
7
+ __pycache__/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .venv/
11
+ /dist/
12
+ *.egg-info/
13
+ /local/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fox Islam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.5
2
+ Name: context-atlas
3
+ Version: 1.0.0
4
+ Summary: Utility library to help process large context inputs in limited context window scenarios
5
+ Project-URL: Homepage, https://github.com/Fox-Islam/context-atlas
6
+ Project-URL: Issues, https://github.com/Fox-Islam/context-atlas/issues
7
+ Author-email: Fox Islam <foxislam@outlook.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: chunking,jev,reranking,retrieval,system-one,typesafe
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Topic :: Text Processing
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: typesafe-sdk>=0.7
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Context Atlas for Python
22
+
23
+ A utility library to help process large context inputs in limited context window scenarios:
24
+ it reads documents in the formats people give an LLM, structures each section by what kind of
25
+ text it is, and, given a question, gathers the context that answers it instead of the whole
26
+ document, using [Jev](https://docs.typesafe.ai) for the judgements. It reads documents the same
27
+ way and sends Jev the same requests as the PHP and JavaScript packages; the `spec/` directory in the
28
+ repository states the rules.
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ pip install context-atlas # or: uv add context-atlas
34
+ ```
35
+
36
+ Python 3.10 or later. Jev is reached through the official
37
+ [`typesafe-sdk`](https://pypi.org/project/typesafe-sdk/), which reads
38
+ `TYPESAFE_API_KEY`, or `OPENROUTER_API_KEY` with `TYPESAFE_PROVIDER=openrouter`.
39
+
40
+ Bad input (an unknown format or config key, a file that cannot be read, a malformed block
41
+ list, retrieval without Jev) raises `ContextAtlasError`. A Jev request that fails never
42
+ raises: it gives `None` or the structural result, as each section below says.
43
+
44
+ ## Chunking
45
+
46
+ ```python
47
+ from typesafe_sdk import TypeSafeClient
48
+ from context_atlas import Chunker, Config, parse_markdown
49
+
50
+ client = TypeSafeClient()
51
+ chunked = Chunker(client, Config(max_tokens=600)).chunk(parse_markdown(markdown))
52
+
53
+ for chunk in chunked.chunks:
54
+ index.add(chunk.text, heading_path=chunk.heading_path)
55
+ ```
56
+
57
+ `Chunker()` with no Jev, or a Jev that does not answer, cuts at headings and
58
+ sizes only, and `chunked.used_jev` is false. `parse_text` reads plain text and
59
+ `parse_blocks` a JSON list of `{"type", "text", "level"?, "meta"?}`.
60
+ `chunked.to_dict()` is the JSON the command line prints. `Config.from_dict` takes
61
+ the snake_case keys of `spec/config.schema.json` and refuses any other key.
62
+
63
+ ## Ranking
64
+
65
+ ```python
66
+ from context_atlas import Passage, Reranker
67
+
68
+ hits = index.search(query, limit=15)
69
+ ranked = Reranker(client).rank(
70
+ [Passage(hit.id, hit.text, hit.heading_path) for hit in hits], query, limit=5
71
+ )
72
+
73
+ if ranked is None:
74
+ ranked_ids = [hit.id for hit in hits[:5]] # Jev did not answer: keep the search order
75
+ else:
76
+ ranked_ids = [item.passage.id for item in ranked]
77
+ ```
78
+
79
+ ## Sending the request yourself
80
+
81
+ `Chunker`, `Reranker` and `Processor` also take a hook in place of the client. It is
82
+ handed the request, `{"state": ..., "questions": {...}}` with SDK `Noul` and `Choice`
83
+ objects, and returns the SDK's `SystemOneResponse`, or `None` for no answer. Caching and
84
+ logging go here:
85
+
86
+ ```python
87
+ from typesafe_sdk import SystemOneResponse
88
+ from context_atlas import Chunker, JevRequest
89
+
90
+ def hook(request: JevRequest) -> SystemOneResponse | None:
91
+ return client.system_one(request["state"], request["questions"])
92
+
93
+ chunked = Chunker(hook).chunk(blocks)
94
+ ```
95
+
96
+ An exception raised by the hook or the client counts as no answer.
97
+
98
+ ## Documents and retrieval
99
+
100
+ `Processor` turns input into a `Document`, plain data you store and hand back as a `Source` at
101
+ retrieval. `spec/document.md`, `spec/prose.md`, `spec/markup.md` and `spec/tabular.md` give
102
+ the rules.
103
+
104
+ ### Ingesting
105
+
106
+ ```python
107
+ from context_atlas import FileStore, Processor
108
+
109
+ processor = Processor(client)
110
+ store = FileStore("documents")
111
+
112
+ store.put(processor.ingest_file("terms.html", "terms"))
113
+ store.put(processor.ingest_file("spend.csv", "spend-2026-03", title="Spend over £25,000"))
114
+ ```
115
+
116
+ | Method | Reads |
117
+ | --- | --- |
118
+ | `ingest_file(path, id, profile=None, title=None)` | a file, in the format its extension names, `.docx` and `.xlsx` included |
119
+ | `ingest(text_or_bytes, id, format, profile=None, title=None)` | text or bytes in any format |
120
+ | `ingest_blocks(blocks, id, profile=None)` | blocks you built, such as the headings and paragraphs of a PDF |
121
+ | `ingest_markup(text, id, format, title=None)` | a data file, cut into path subtrees linked by `$ref`, YAML aliases and XML references, with no Jev request |
122
+ | `ingest_csv(text_or_bytes, id, title)` | CSV already in memory |
123
+ | `ingest_spreadsheet(paths, id, title=None)` | one CSV or XLSX file, or several (`{"Data": "population.csv", "Metadata": "regions.csv"}`) joined into one document |
124
+ | `append(document, blocks, profile=None)` | blocks added to the end of a prose document |
125
+
126
+ Document formats (`markdown`, `mdx`, `text`, `html`, `rst`, `asciidoc`, `org`, `latex`,
127
+ `ipynb`, `vtt`, `srt`, `chat`, `docx`, `blocks`) become prose; data formats (`yaml`, `json`,
128
+ `jsonl`, `xml`, `toml`) go to `ingest_markup`; `csv` and `xlsx` to the spreadsheet readers.
129
+
130
+ - **Prose** gets a profile per section (`guidance`, `normative`, `narrative`, `procedural`,
131
+ `reference` or `conversational`), detected by Jev unless `profile` names one. A section whose
132
+ blocks carry `meta.speaker` is conversational without asking Jev.
133
+ - **Chat logs**: `ingest_file` reads a `.json` or `.jsonl` file as a chat log when the chat
134
+ reader finds at least two messages in it, and as data otherwise. For a chat log in memory,
135
+ pass `"chat"` to `ingest`. For `append`,
136
+ `context_atlas.parsing.formats.blocks(new_lines, "chat")` reads new lines. The README's "Chat
137
+ and agent transcripts" covers attachments, `output` units and the `conversation_*` config
138
+ keys.
139
+ - **DOCX** is binary: `ingest` takes its bytes, not text.
140
+ - **Spreadsheets** are profiled without Jev; at retrieval Jev picks the table, measure, filters
141
+ and grouping, and the package computes the answer.
142
+ - `context_atlas.parsing.formats.blocks(text, format)` gives any document format's blocks
143
+ without ingesting them, and `formats.of(path)` the format an extension names.
144
+
145
+ ### Storing
146
+
147
+ - `FileStore(directory)` keeps one JSON file per document plus an append-only term index under
148
+ `index/`, in the layout the PHP and JavaScript packages share. `compact()` rewrites the index
149
+ without the lines of rewritten or removed documents.
150
+ - `Collection()` holds documents in memory.
151
+ - `put(document)` replaces a stored document with the same id; `remove(document_id)` deletes
152
+ one. Drop its vectors from your own store too.
153
+ - For your own database, `document.to_dict()` is the JSON to keep and `Document.from_dict()`
154
+ reads it back. Any object with `postings`, `stats` and `document` methods can stand in for a
155
+ store (see `Source`).
156
+
157
+ ### Retrieving
158
+
159
+ ```python
160
+ def nearest(query, limit, kinds, documents):
161
+ hits = vectors.search(embed(query), limit, document=documents)
162
+ return [{"document": hit.payload["document"], "unit": hit.payload["unit"]} for hit in hits]
163
+
164
+ context = processor.retrieve(question, store, nearest, documents=allowed_file_ids)
165
+ if context is not None and not context.abstained:
166
+ prompt = context.text
167
+ ```
168
+
169
+ `retrieve()` searches by exact terms. `nearest` fuses in your vector search: embed each unit of
170
+ `document.units` under its document and unit ids at ingestion, and return those ids (`embed`
171
+ and `vectors` stand for your model and store). A `nearest` with three parameters is not given
172
+ the documents. `documents` limits both searches to those documents.
173
+
174
+ It returns None when Jev did not answer. A `Context` has `text`, `sources` (a
175
+ `{"document", "unit"}` dict per part, with `rows` for a table and `superseded_by` for a
176
+ corrected message), `abstained` and `trace`.
177
+
178
+ ## Command line
179
+
180
+ ```sh
181
+ context-atlas chunk [FILE|-] [--format=markdown|text|blocks] [--no-jev] [--config=FILE] [--pretty]
182
+ context-atlas rank QUERY [FILE|-] [--limit=N] [--config=FILE] [--pretty]
183
+ context-atlas ingest FILE|- [--id=ID] [--format=FORMAT] [--profile=PROFILE] [--title=TITLE] [--store=DIR] [--no-jev] [--config=FILE] [--pretty]
184
+ context-atlas retrieve QUERY --store=DIR [--documents=ID,...] [--config=FILE] [--pretty]
185
+ python -m context_atlas ...
186
+ ```
187
+
188
+ `chunk` reads Markdown, a `.txt` or `.text` file as plain text, and a `.json`
189
+ file as blocks. `rank` reads a JSON list of passages or the output of `chunk`.
190
+ `ingest` reads any supported format (by extension, or `--format`, which a chat
191
+ log needs unless it is a `.json` or `.jsonl` file; stdin needs `--format` and `--id`) and prints the document, or with
192
+ `--store=DIR` saves it there and prints a summary. `retrieve` answers QUERY from that store by exact
193
+ terms, optionally limited to `--documents`.
194
+ `--config` names a JSON file of the keys in `spec/config.schema.json`. Exit
195
+ codes: 0 done, 1 bad input, 2 Jev did not answer. `TYPESAFE_PROVIDER=openrouter`
196
+ with `OPENROUTER_API_KEY` reaches Jev through OpenRouter.
@@ -0,0 +1,176 @@
1
+ # Context Atlas for Python
2
+
3
+ A utility library to help process large context inputs in limited context window scenarios:
4
+ it reads documents in the formats people give an LLM, structures each section by what kind of
5
+ text it is, and, given a question, gathers the context that answers it instead of the whole
6
+ document, using [Jev](https://docs.typesafe.ai) for the judgements. It reads documents the same
7
+ way and sends Jev the same requests as the PHP and JavaScript packages; the `spec/` directory in the
8
+ repository states the rules.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pip install context-atlas # or: uv add context-atlas
14
+ ```
15
+
16
+ Python 3.10 or later. Jev is reached through the official
17
+ [`typesafe-sdk`](https://pypi.org/project/typesafe-sdk/), which reads
18
+ `TYPESAFE_API_KEY`, or `OPENROUTER_API_KEY` with `TYPESAFE_PROVIDER=openrouter`.
19
+
20
+ Bad input (an unknown format or config key, a file that cannot be read, a malformed block
21
+ list, retrieval without Jev) raises `ContextAtlasError`. A Jev request that fails never
22
+ raises: it gives `None` or the structural result, as each section below says.
23
+
24
+ ## Chunking
25
+
26
+ ```python
27
+ from typesafe_sdk import TypeSafeClient
28
+ from context_atlas import Chunker, Config, parse_markdown
29
+
30
+ client = TypeSafeClient()
31
+ chunked = Chunker(client, Config(max_tokens=600)).chunk(parse_markdown(markdown))
32
+
33
+ for chunk in chunked.chunks:
34
+ index.add(chunk.text, heading_path=chunk.heading_path)
35
+ ```
36
+
37
+ `Chunker()` with no Jev, or a Jev that does not answer, cuts at headings and
38
+ sizes only, and `chunked.used_jev` is false. `parse_text` reads plain text and
39
+ `parse_blocks` a JSON list of `{"type", "text", "level"?, "meta"?}`.
40
+ `chunked.to_dict()` is the JSON the command line prints. `Config.from_dict` takes
41
+ the snake_case keys of `spec/config.schema.json` and refuses any other key.
42
+
43
+ ## Ranking
44
+
45
+ ```python
46
+ from context_atlas import Passage, Reranker
47
+
48
+ hits = index.search(query, limit=15)
49
+ ranked = Reranker(client).rank(
50
+ [Passage(hit.id, hit.text, hit.heading_path) for hit in hits], query, limit=5
51
+ )
52
+
53
+ if ranked is None:
54
+ ranked_ids = [hit.id for hit in hits[:5]] # Jev did not answer: keep the search order
55
+ else:
56
+ ranked_ids = [item.passage.id for item in ranked]
57
+ ```
58
+
59
+ ## Sending the request yourself
60
+
61
+ `Chunker`, `Reranker` and `Processor` also take a hook in place of the client. It is
62
+ handed the request, `{"state": ..., "questions": {...}}` with SDK `Noul` and `Choice`
63
+ objects, and returns the SDK's `SystemOneResponse`, or `None` for no answer. Caching and
64
+ logging go here:
65
+
66
+ ```python
67
+ from typesafe_sdk import SystemOneResponse
68
+ from context_atlas import Chunker, JevRequest
69
+
70
+ def hook(request: JevRequest) -> SystemOneResponse | None:
71
+ return client.system_one(request["state"], request["questions"])
72
+
73
+ chunked = Chunker(hook).chunk(blocks)
74
+ ```
75
+
76
+ An exception raised by the hook or the client counts as no answer.
77
+
78
+ ## Documents and retrieval
79
+
80
+ `Processor` turns input into a `Document`, plain data you store and hand back as a `Source` at
81
+ retrieval. `spec/document.md`, `spec/prose.md`, `spec/markup.md` and `spec/tabular.md` give
82
+ the rules.
83
+
84
+ ### Ingesting
85
+
86
+ ```python
87
+ from context_atlas import FileStore, Processor
88
+
89
+ processor = Processor(client)
90
+ store = FileStore("documents")
91
+
92
+ store.put(processor.ingest_file("terms.html", "terms"))
93
+ store.put(processor.ingest_file("spend.csv", "spend-2026-03", title="Spend over £25,000"))
94
+ ```
95
+
96
+ | Method | Reads |
97
+ | --- | --- |
98
+ | `ingest_file(path, id, profile=None, title=None)` | a file, in the format its extension names, `.docx` and `.xlsx` included |
99
+ | `ingest(text_or_bytes, id, format, profile=None, title=None)` | text or bytes in any format |
100
+ | `ingest_blocks(blocks, id, profile=None)` | blocks you built, such as the headings and paragraphs of a PDF |
101
+ | `ingest_markup(text, id, format, title=None)` | a data file, cut into path subtrees linked by `$ref`, YAML aliases and XML references, with no Jev request |
102
+ | `ingest_csv(text_or_bytes, id, title)` | CSV already in memory |
103
+ | `ingest_spreadsheet(paths, id, title=None)` | one CSV or XLSX file, or several (`{"Data": "population.csv", "Metadata": "regions.csv"}`) joined into one document |
104
+ | `append(document, blocks, profile=None)` | blocks added to the end of a prose document |
105
+
106
+ Document formats (`markdown`, `mdx`, `text`, `html`, `rst`, `asciidoc`, `org`, `latex`,
107
+ `ipynb`, `vtt`, `srt`, `chat`, `docx`, `blocks`) become prose; data formats (`yaml`, `json`,
108
+ `jsonl`, `xml`, `toml`) go to `ingest_markup`; `csv` and `xlsx` to the spreadsheet readers.
109
+
110
+ - **Prose** gets a profile per section (`guidance`, `normative`, `narrative`, `procedural`,
111
+ `reference` or `conversational`), detected by Jev unless `profile` names one. A section whose
112
+ blocks carry `meta.speaker` is conversational without asking Jev.
113
+ - **Chat logs**: `ingest_file` reads a `.json` or `.jsonl` file as a chat log when the chat
114
+ reader finds at least two messages in it, and as data otherwise. For a chat log in memory,
115
+ pass `"chat"` to `ingest`. For `append`,
116
+ `context_atlas.parsing.formats.blocks(new_lines, "chat")` reads new lines. The README's "Chat
117
+ and agent transcripts" covers attachments, `output` units and the `conversation_*` config
118
+ keys.
119
+ - **DOCX** is binary: `ingest` takes its bytes, not text.
120
+ - **Spreadsheets** are profiled without Jev; at retrieval Jev picks the table, measure, filters
121
+ and grouping, and the package computes the answer.
122
+ - `context_atlas.parsing.formats.blocks(text, format)` gives any document format's blocks
123
+ without ingesting them, and `formats.of(path)` the format an extension names.
124
+
125
+ ### Storing
126
+
127
+ - `FileStore(directory)` keeps one JSON file per document plus an append-only term index under
128
+ `index/`, in the layout the PHP and JavaScript packages share. `compact()` rewrites the index
129
+ without the lines of rewritten or removed documents.
130
+ - `Collection()` holds documents in memory.
131
+ - `put(document)` replaces a stored document with the same id; `remove(document_id)` deletes
132
+ one. Drop its vectors from your own store too.
133
+ - For your own database, `document.to_dict()` is the JSON to keep and `Document.from_dict()`
134
+ reads it back. Any object with `postings`, `stats` and `document` methods can stand in for a
135
+ store (see `Source`).
136
+
137
+ ### Retrieving
138
+
139
+ ```python
140
+ def nearest(query, limit, kinds, documents):
141
+ hits = vectors.search(embed(query), limit, document=documents)
142
+ return [{"document": hit.payload["document"], "unit": hit.payload["unit"]} for hit in hits]
143
+
144
+ context = processor.retrieve(question, store, nearest, documents=allowed_file_ids)
145
+ if context is not None and not context.abstained:
146
+ prompt = context.text
147
+ ```
148
+
149
+ `retrieve()` searches by exact terms. `nearest` fuses in your vector search: embed each unit of
150
+ `document.units` under its document and unit ids at ingestion, and return those ids (`embed`
151
+ and `vectors` stand for your model and store). A `nearest` with three parameters is not given
152
+ the documents. `documents` limits both searches to those documents.
153
+
154
+ It returns None when Jev did not answer. A `Context` has `text`, `sources` (a
155
+ `{"document", "unit"}` dict per part, with `rows` for a table and `superseded_by` for a
156
+ corrected message), `abstained` and `trace`.
157
+
158
+ ## Command line
159
+
160
+ ```sh
161
+ context-atlas chunk [FILE|-] [--format=markdown|text|blocks] [--no-jev] [--config=FILE] [--pretty]
162
+ context-atlas rank QUERY [FILE|-] [--limit=N] [--config=FILE] [--pretty]
163
+ context-atlas ingest FILE|- [--id=ID] [--format=FORMAT] [--profile=PROFILE] [--title=TITLE] [--store=DIR] [--no-jev] [--config=FILE] [--pretty]
164
+ context-atlas retrieve QUERY --store=DIR [--documents=ID,...] [--config=FILE] [--pretty]
165
+ python -m context_atlas ...
166
+ ```
167
+
168
+ `chunk` reads Markdown, a `.txt` or `.text` file as plain text, and a `.json`
169
+ file as blocks. `rank` reads a JSON list of passages or the output of `chunk`.
170
+ `ingest` reads any supported format (by extension, or `--format`, which a chat
171
+ log needs unless it is a `.json` or `.jsonl` file; stdin needs `--format` and `--id`) and prints the document, or with
172
+ `--store=DIR` saves it there and prints a summary. `retrieve` answers QUERY from that store by exact
173
+ terms, optionally limited to `--documents`.
174
+ `--config` names a JSON file of the keys in `spec/config.schema.json`. Exit
175
+ codes: 0 done, 1 bad input, 2 Jev did not answer. `TYPESAFE_PROVIDER=openrouter`
176
+ with `OPENROUTER_API_KEY` reaches Jev through OpenRouter.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "context-atlas"
7
+ version = "1.0.0"
8
+ description = "Utility library to help process large context inputs in limited context window scenarios"
9
+ readme = "docs/python.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ keywords = ["typesafe", "jev", "chunking", "retrieval", "reranking", "system-one"]
14
+ authors = [{ name = "Fox Islam", email = "foxislam@outlook.com" }]
15
+ classifiers = [
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Topic :: Text Processing",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "typesafe-sdk>=0.7",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/Fox-Islam/context-atlas"
29
+ Issues = "https://github.com/Fox-Islam/context-atlas/issues"
30
+
31
+ [project.scripts]
32
+ context-atlas = "context_atlas.console:main"
33
+
34
+ [dependency-groups]
35
+ dev = ["pytest>=8", "ruff>=0.13"]
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["python/src/context_atlas"]
39
+
40
+ [tool.hatch.build.targets.sdist]
41
+ include = ["/python", "/spec", "/docs/python.md", "/LICENSE"]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["python/tests"]
45
+ pythonpath = ["python/src"]
46
+
47
+ [tool.ruff]
48
+ line-length = 120
49
+ src = ["python/src"]
50
+ include = ["python/**/*.py"]
51
+
52
+ [tool.ruff.lint]
53
+ select = ["F", "E", "W", "I", "UP", "B", "C4", "SIM", "RET", "PTH", "RUF"]
@@ -0,0 +1,47 @@
1
+ """
2
+ Split documents into passages at headings, size limits and, using Jev, where the topic
3
+ changes; rank search hits by whether they answer a query; and ingest spreadsheets and prose into
4
+ stored documents that retrieval answers questions from.
5
+ """
6
+ from .blocks import Block, BlockType
7
+ from .chunk import Chunk, Chunked
8
+ from .chunker import Chunker
9
+ from .config import Config
10
+ from .context import Context
11
+ from .document import Document, Link, Unit
12
+ from .errors import ContextAtlasError
13
+ from .index import Collection, FileStore, Source
14
+ from .jev import Jev, JevHook, JevRequest
15
+ from .parsing import parse_blocks, parse_markdown, parse_text
16
+ from .passage import Passage, RankedPassage
17
+ from .processor import Processor
18
+ from .reranker import Reranker
19
+
20
+ __version__ = '1.0.0'
21
+
22
+ __all__ = [
23
+ 'Block',
24
+ 'BlockType',
25
+ 'Chunk',
26
+ 'Chunked',
27
+ 'Chunker',
28
+ 'Collection',
29
+ 'Config',
30
+ 'Context',
31
+ 'ContextAtlasError',
32
+ 'Document',
33
+ 'FileStore',
34
+ 'Jev',
35
+ 'JevHook',
36
+ 'JevRequest',
37
+ 'Link',
38
+ 'Passage',
39
+ 'Processor',
40
+ 'RankedPassage',
41
+ 'Reranker',
42
+ 'Source',
43
+ 'Unit',
44
+ 'parse_blocks',
45
+ 'parse_markdown',
46
+ 'parse_text',
47
+ ]
@@ -0,0 +1,4 @@
1
+ """`python -m context_atlas`, for a checkout with no console script on the PATH."""
2
+ from .console import main
3
+
4
+ raise SystemExit(main())
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any
6
+
7
+ from .errors import ContextAtlasError
8
+
9
+
10
+ class BlockType(str, Enum):
11
+ HEADING = 'heading'
12
+ PARAGRAPH = 'paragraph'
13
+ TABLE = 'table'
14
+ CODE = 'code'
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Block:
19
+ """
20
+ One unit of a document.
21
+
22
+ A paragraph is the only kind split into sentences when it is too long; a table or a code
23
+ block is kept whole however long it runs. Only a heading has a `level`. `meta` is carried
24
+ through and never read, so a caller can map chunks back to pages or offsets.
25
+ """
26
+
27
+ type: BlockType
28
+ text: str
29
+ level: int | None = None
30
+ meta: dict[str, Any] | list[Any] = field(default_factory=dict)
31
+
32
+ @staticmethod
33
+ def heading(text: str, level: int, meta: dict[str, Any] | None = None) -> Block:
34
+ return Block(BlockType.HEADING, text, level, meta or {})
35
+
36
+ @staticmethod
37
+ def paragraph(text: str, meta: dict[str, Any] | None = None) -> Block:
38
+ return Block(BlockType.PARAGRAPH, text, None, meta or {})
39
+
40
+ @staticmethod
41
+ def from_dict(value: Any) -> Block:
42
+ """
43
+ Reads `{"type", "text", "level"?, "meta"?}`.
44
+
45
+ :raises ContextAtlasError: on an unknown type, a text that is not a string, or a
46
+ heading without an integer level
47
+ """
48
+ value = value if isinstance(value, dict) else {}
49
+ kind = next((member for member in BlockType if member.value == value.get('type')), None)
50
+ text = value.get('text')
51
+ level = value.get('level')
52
+
53
+ if kind is None or not isinstance(text, str):
54
+ raise ContextAtlasError(
55
+ 'A block needs a "type" (heading, paragraph, table or code) and a "text".',
56
+ )
57
+ if kind is BlockType.HEADING and (not isinstance(level, int) or isinstance(level, bool)):
58
+ raise ContextAtlasError('A heading block needs an integer "level".')
59
+
60
+ return Block(kind, text, level if kind is BlockType.HEADING else None, _meta(value.get('meta')))
61
+
62
+ def is_heading(self) -> bool:
63
+ return self.type is BlockType.HEADING
64
+
65
+ def to_dict(self) -> dict[str, Any]:
66
+ """The block as `from_dict` reads it, without an empty `level` or `meta`."""
67
+ value: dict[str, Any] = {'type': self.type.value, 'text': self.text}
68
+
69
+ if self.level is not None:
70
+ value['level'] = self.level
71
+ if self.meta:
72
+ value['meta'] = self.meta
73
+
74
+ return value
75
+
76
+
77
+ def _meta(value: Any) -> dict[str, Any] | list[Any]:
78
+ """`meta` as PHP's `(array)` cast leaves it: an object or list kept, another value wrapped in a list."""
79
+ if value is None:
80
+ return {}
81
+
82
+ return value if isinstance(value, (dict, list)) else [value]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, replace
4
+ from typing import Any, TypedDict
5
+
6
+
7
+ class Heading(TypedDict):
8
+ block: int
9
+ title: str
10
+ level: int
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Chunk:
15
+ """
16
+ A passage ready to embed.
17
+
18
+ `text` is the heading path in brackets, any overlap carried from the previous chunk, then
19
+ the chunk's own text. `tokens` counts all of it and `overlap` the carried part. `blocks`
20
+ are indexes into the blocks given to `Chunker.chunk`.
21
+ """
22
+
23
+ text: str
24
+ blocks: list[int]
25
+ heading_path: list[str]
26
+ heading_level: int | None
27
+ section: int
28
+ tokens: int
29
+ overlap: int
30
+ topic_start: bool
31
+
32
+ def with_blocks(self, blocks: list[int]) -> Chunk:
33
+ return replace(self, blocks=blocks)
34
+
35
+ def to_dict(self) -> dict[str, Any]:
36
+ return {
37
+ 'text': self.text,
38
+ 'blocks': list(self.blocks),
39
+ 'heading_path': list(self.heading_path),
40
+ 'heading_level': self.heading_level,
41
+ 'section': self.section,
42
+ 'tokens': self.tokens,
43
+ 'overlap': self.overlap,
44
+ 'topic_start': self.topic_start,
45
+ }
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Chunked:
50
+ """The chunks of a document, with its outline: every heading in order."""
51
+
52
+ chunks: list[Chunk]
53
+ outline: list[Heading]
54
+ used_jev: bool
55
+
56
+ def to_dict(self) -> dict[str, Any]:
57
+ return {
58
+ 'chunks': [chunk.to_dict() for chunk in self.chunks],
59
+ 'outline': [dict(heading) for heading in self.outline],
60
+ 'used_jev': self.used_jev,
61
+ }