gap-finder 0.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.
- gap_finder-0.1.0/LICENSE +21 -0
- gap_finder-0.1.0/PKG-INFO +240 -0
- gap_finder-0.1.0/README.md +216 -0
- gap_finder-0.1.0/pyproject.toml +48 -0
- gap_finder-0.1.0/src/gap_finder/__init__.py +31 -0
- gap_finder-0.1.0/src/gap_finder/_auth.py +18 -0
- gap_finder-0.1.0/src/gap_finder/embedding_model/__init__.py +40 -0
- gap_finder-0.1.0/src/gap_finder/embedding_model/client.py +24 -0
- gap_finder-0.1.0/src/gap_finder/embedding_model/embedding_model.py +37 -0
- gap_finder-0.1.0/src/gap_finder/embedding_model/factory.py +34 -0
- gap_finder-0.1.0/src/gap_finder/embedding_model/local_embedding_model.py +92 -0
- gap_finder-0.1.0/src/gap_finder/embedding_model/openai_embedding_model.py +43 -0
- gap_finder-0.1.0/src/gap_finder/io_helper.py +71 -0
- gap_finder-0.1.0/src/gap_finder/language_model/__init__.py +39 -0
- gap_finder-0.1.0/src/gap_finder/language_model/client.py +43 -0
- gap_finder-0.1.0/src/gap_finder/language_model/factory.py +40 -0
- gap_finder-0.1.0/src/gap_finder/language_model/language_model.py +31 -0
- gap_finder-0.1.0/src/gap_finder/language_model/local_language_model.py +69 -0
- gap_finder-0.1.0/src/gap_finder/language_model/openai_language_model.py +45 -0
- gap_finder-0.1.0/src/gap_finder/main.py +932 -0
- gap_finder-0.1.0/src/gap_finder/model_types.py +67 -0
- gap_finder-0.1.0/src/gap_finder/persistence.py +449 -0
- gap_finder-0.1.0/src/gap_finder/prompts/__init__.py +3 -0
- gap_finder-0.1.0/src/gap_finder/prompts/loader.py +44 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/hyde.txt.j2 +13 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/examples_bad.txt.j2 +3 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/examples_good.txt.j2 +3 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/main.txt.j2 +34 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/rules.txt.j2 +10 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/summarize.txt.j2 +10 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/topic_explanation.txt.j2 +21 -0
- gap_finder-0.1.0/src/gap_finder/prompts/templates/topic_relevance.txt.j2 +23 -0
- gap_finder-0.1.0/src/gap_finder/py.typed +0 -0
- gap_finder-0.1.0/src/gap_finder/question_generation.py +160 -0
- gap_finder-0.1.0/src/gap_finder/question_ranking.py +72 -0
- gap_finder-0.1.0/src/gap_finder/report.py +120 -0
- gap_finder-0.1.0/src/gap_finder/singleton/constants.py +47 -0
- gap_finder-0.1.0/src/gap_finder/topic_explanation.py +164 -0
- gap_finder-0.1.0/src/gap_finder/topic_generation.py +155 -0
- gap_finder-0.1.0/src/gap_finder/topic_ranking.py +109 -0
- gap_finder-0.1.0/src/gap_finder/topic_relevance.py +128 -0
- gap_finder-0.1.0/src/gap_finder/vector_store/__init__.py +18 -0
- gap_finder-0.1.0/src/gap_finder/vector_store/faiss_vector_store.py +74 -0
- gap_finder-0.1.0/src/gap_finder/vector_store/numpy_vector_store.py +88 -0
- gap_finder-0.1.0/src/gap_finder/vector_store/pdf_chunker.py +41 -0
- gap_finder-0.1.0/src/gap_finder/vector_store/vector_store.py +69 -0
gap_finder-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KI.SH
|
|
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,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gap-finder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Identify potential knowledge gaps in document collections using language models and topic modeling
|
|
5
|
+
Author: Ben Biesenbach, Josef Küstner
|
|
6
|
+
Author-email: Ben Biesenbach <bbi@informatik.uni-kiel.de>, Josef Küstner <josef.kuestner@informatik.uni-kiel.de>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Requires-Dist: bertopic>=0.17.4
|
|
11
|
+
Requires-Dist: faiss-cpu>=1.14.3
|
|
12
|
+
Requires-Dist: jinja2>=3.1.6
|
|
13
|
+
Requires-Dist: langchain-text-splitters>=1.1.2
|
|
14
|
+
Requires-Dist: openai>=2.46.0
|
|
15
|
+
Requires-Dist: sentence-transformers>=5.6.0
|
|
16
|
+
Requires-Dist: tqdm>=4.68.3
|
|
17
|
+
Requires-Dist: unstructured[pdf]>=0.18.32,<0.23.1 ; python_full_version < '3.11'
|
|
18
|
+
Requires-Dist: unstructured[pdf]>=0.23.1 ; python_full_version >= '3.11'
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Project-URL: Homepage, https://github.com/KI-Schleswig-Holstein/gap-finder
|
|
21
|
+
Project-URL: Repository, https://github.com/KI-Schleswig-Holstein/gap-finder
|
|
22
|
+
Project-URL: Issues, https://github.com/KI-Schleswig-Holstein/gap-finder/issues
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Gap Finder
|
|
26
|
+
|
|
27
|
+
Gap Finder identifies potential knowledge gaps in a vector store, a collection of PDF documents,
|
|
28
|
+
or text chunks. It generates questions that the source material does not answer,
|
|
29
|
+
groups them into topics with BERTopic, and ranks those topics by how well the
|
|
30
|
+
indexed corpus covers them.
|
|
31
|
+
|
|
32
|
+
The project was created during a feasibility study at KI.SH in
|
|
33
|
+
Schleswig-Holstein. Its pipeline is based on `GapView`, described in
|
|
34
|
+
[Vanessa Klotzmann's dissertation](https://escholarship.org/uc/item/2zs4w991).
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Install Gap Finder from PyPI:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python -m pip install gap-finder
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For development, clone the repository and install the project with
|
|
45
|
+
[uv](https://docs.astral.sh/uv/):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/KI-Schleswig-Holstein/gap-finder.git
|
|
49
|
+
cd gap-finder
|
|
50
|
+
uv sync
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
The following example indexes all PDFs below `documents/` and uses an
|
|
56
|
+
OpenAI-compatible chat API for question generation and topic explanations:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
export GAP_FINDER_API_KEY="your-api-key"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from gap_finder import GapFinder
|
|
64
|
+
|
|
65
|
+
gap_finder = GapFinder.from_pdf(
|
|
66
|
+
folder_path="documents",
|
|
67
|
+
language_model="your-model-name",
|
|
68
|
+
base_url="https://your-provider.example/v1",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
topics = gap_finder.find()
|
|
72
|
+
print(topics[["title", "avg_similarity", "explanation"]])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For a local OpenAI-compatible server such as Ollama, set its `/v1` endpoint as
|
|
76
|
+
`base_url`. If the server does not require authentication, pass a placeholder
|
|
77
|
+
such as `api_key="-"`.
|
|
78
|
+
|
|
79
|
+
The default corpus and topic embedding models run locally and may be downloaded
|
|
80
|
+
on first use. Question generation is normally the most computationally
|
|
81
|
+
expensive part of the pipeline. Using an API offloads that work, but embedding
|
|
82
|
+
the corpus and fitting BERTopic still require local CPU or GPU time and memory.
|
|
83
|
+
|
|
84
|
+
### Use a local language model
|
|
85
|
+
|
|
86
|
+
Pass a Hugging Face text-generation pipeline to run the language model on your
|
|
87
|
+
machine. A GPU is recommended for all but very small models.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from transformers import pipeline
|
|
91
|
+
|
|
92
|
+
from gap_finder import GapFinder
|
|
93
|
+
|
|
94
|
+
language_model = pipeline(
|
|
95
|
+
"text-generation",
|
|
96
|
+
model="Qwen/Qwen3-0.6B",
|
|
97
|
+
device="cuda",
|
|
98
|
+
max_new_tokens=1024,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
gap_finder = GapFinder.from_pdf(
|
|
102
|
+
folder_path="documents",
|
|
103
|
+
language_model=language_model,
|
|
104
|
+
question_generation_batch_size=8,
|
|
105
|
+
)
|
|
106
|
+
topics = gap_finder.find()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Generate or provide questions separately
|
|
110
|
+
|
|
111
|
+
Questions can be generated before the rest of the pipeline. Because
|
|
112
|
+
`create_questions()` stores its result on the instance, the following call to
|
|
113
|
+
`find()` reuses them instead of generating them again:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
questions = gap_finder.create_questions()
|
|
117
|
+
# Inspect or persist questions here.
|
|
118
|
+
topics = gap_finder.find()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To skip automatic question generation, provide existing questions when the
|
|
122
|
+
instance is created or set them later:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
gap_finder = GapFinder.from_pdf(
|
|
126
|
+
folder_path="documents",
|
|
127
|
+
language_model="your-model-name",
|
|
128
|
+
base_url="https://your-provider.example/v1",
|
|
129
|
+
questions=[
|
|
130
|
+
"Which populations are not represented in the study?",
|
|
131
|
+
"How does the method behave outside the tested conditions?",
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Equivalent after construction:
|
|
136
|
+
gap_finder.set_questions(["Which limitations remain unexamined?"])
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A questions DataFrame must contain a `question` column. Additional columns are
|
|
140
|
+
preserved as the pipeline adds topic and ranking information.
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
Use `GapFinder.from_pdf()` for a folder of PDFs, `GapFinder.from_chunks()` for
|
|
145
|
+
pre-split text, or construct `GapFinder` directly with a populated vector store.
|
|
146
|
+
|
|
147
|
+
## Vector stores
|
|
148
|
+
|
|
149
|
+
Two vector-store implementations are included:
|
|
150
|
+
|
|
151
|
+
- `NumpyVectorStore` is the default and performs exact cosine-similarity search
|
|
152
|
+
with NumPy.
|
|
153
|
+
- `FaissVectorStore` performs exact cosine-similarity search with a FAISS
|
|
154
|
+
`IndexFlatIP` index.
|
|
155
|
+
|
|
156
|
+
Select FAISS while building the index:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from gap_finder import GapFinder
|
|
160
|
+
from gap_finder.vector_store import FaissVectorStore
|
|
161
|
+
|
|
162
|
+
gap_finder = GapFinder.from_pdf(
|
|
163
|
+
folder_path="documents",
|
|
164
|
+
vector_store_cls=FaissVectorStore,
|
|
165
|
+
language_model="your-model-name",
|
|
166
|
+
base_url="https://your-provider.example/v1",
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Custom backends can subclass `VectorStore`.
|
|
171
|
+
|
|
172
|
+
## Pipeline
|
|
173
|
+
|
|
174
|
+
`find()` runs the following steps:
|
|
175
|
+
|
|
176
|
+
1. Load chunks and embeddings from the vector store.
|
|
177
|
+
2. Reuse configured questions or generate questions with the language model.
|
|
178
|
+
3. Group questions into topics with BERTopic.
|
|
179
|
+
4. Compare questions with the corpus and aggregate coverage scores per topic.
|
|
180
|
+
5. Add titles and explanations to the lowest-coverage topics.
|
|
181
|
+
6. Score those topics for domain relevance when `domain_description` is set.
|
|
182
|
+
7. Create a Markdown report section for each explained topic.
|
|
183
|
+
|
|
184
|
+
Lower coverage means the source corpus covers a topic less well and therefore
|
|
185
|
+
indicates a stronger potential knowledge gap.
|
|
186
|
+
|
|
187
|
+
## Results
|
|
188
|
+
|
|
189
|
+
`find()` returns the final topics DataFrame and stores the same object in
|
|
190
|
+
`gap_finder.topics`. Its columns are:
|
|
191
|
+
|
|
192
|
+
| Column | Description |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `id` | BERTopic topic identifier; `-1` represents outliers. |
|
|
195
|
+
| `topic_words` | Representative `(word, score)` pairs. |
|
|
196
|
+
| `question_count` | Number of questions assigned to the topic. |
|
|
197
|
+
| `avg_similarity` | Mean question-to-corpus similarity and primary coverage score. |
|
|
198
|
+
| `std_similarity` | Standard deviation of question similarity. |
|
|
199
|
+
| `min_similarity`, `max_similarity` | Similarity range within the topic. |
|
|
200
|
+
| `title`, `explanation` | LLM-generated description for the configured lowest-coverage topics. |
|
|
201
|
+
| `relevance_score`, `relevance_reason` | Optional domain-relevance result, present when `domain_description` is set. |
|
|
202
|
+
| `report` | Markdown summary for an explained topic. |
|
|
203
|
+
|
|
204
|
+
After `find()`, `gap_finder.questions` contains the question records enriched
|
|
205
|
+
with `topic_id`, an optional `topic_prob`, an `embedding`, and a
|
|
206
|
+
`similarity_score`. Automatically generated questions also contain `id` and
|
|
207
|
+
`chunk_id`; caller-provided columns are retained.
|
|
208
|
+
|
|
209
|
+
`gap_finder.topic_model` is the fitted `BERTopic` object, not a table. Use its
|
|
210
|
+
standard API for deeper inspection, for example:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
topic_info = gap_finder.topic_model.get_topic_info()
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
Install the project and development dependencies with:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
uv sync
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Run the test suite:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
uv run pytest
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The repository also has opt-in integration tests for real embedding models,
|
|
231
|
+
BERTopic, local language models, and OpenAI-compatible APIs. See the
|
|
232
|
+
[integration test guide](test/integration/README.md) for configuration and
|
|
233
|
+
commands. These tests may download models and can require a GPU or a configured
|
|
234
|
+
API endpoint.
|
|
235
|
+
|
|
236
|
+
To contribute, open an issue/ticket or submit a pull/merge request.
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
Gap Finder is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Gap Finder
|
|
2
|
+
|
|
3
|
+
Gap Finder identifies potential knowledge gaps in a vector store, a collection of PDF documents,
|
|
4
|
+
or text chunks. It generates questions that the source material does not answer,
|
|
5
|
+
groups them into topics with BERTopic, and ranks those topics by how well the
|
|
6
|
+
indexed corpus covers them.
|
|
7
|
+
|
|
8
|
+
The project was created during a feasibility study at KI.SH in
|
|
9
|
+
Schleswig-Holstein. Its pipeline is based on `GapView`, described in
|
|
10
|
+
[Vanessa Klotzmann's dissertation](https://escholarship.org/uc/item/2zs4w991).
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Install Gap Finder from PyPI:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m pip install gap-finder
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For development, clone the repository and install the project with
|
|
21
|
+
[uv](https://docs.astral.sh/uv/):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/KI-Schleswig-Holstein/gap-finder.git
|
|
25
|
+
cd gap-finder
|
|
26
|
+
uv sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
The following example indexes all PDFs below `documents/` and uses an
|
|
32
|
+
OpenAI-compatible chat API for question generation and topic explanations:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
export GAP_FINDER_API_KEY="your-api-key"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from gap_finder import GapFinder
|
|
40
|
+
|
|
41
|
+
gap_finder = GapFinder.from_pdf(
|
|
42
|
+
folder_path="documents",
|
|
43
|
+
language_model="your-model-name",
|
|
44
|
+
base_url="https://your-provider.example/v1",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
topics = gap_finder.find()
|
|
48
|
+
print(topics[["title", "avg_similarity", "explanation"]])
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For a local OpenAI-compatible server such as Ollama, set its `/v1` endpoint as
|
|
52
|
+
`base_url`. If the server does not require authentication, pass a placeholder
|
|
53
|
+
such as `api_key="-"`.
|
|
54
|
+
|
|
55
|
+
The default corpus and topic embedding models run locally and may be downloaded
|
|
56
|
+
on first use. Question generation is normally the most computationally
|
|
57
|
+
expensive part of the pipeline. Using an API offloads that work, but embedding
|
|
58
|
+
the corpus and fitting BERTopic still require local CPU or GPU time and memory.
|
|
59
|
+
|
|
60
|
+
### Use a local language model
|
|
61
|
+
|
|
62
|
+
Pass a Hugging Face text-generation pipeline to run the language model on your
|
|
63
|
+
machine. A GPU is recommended for all but very small models.
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from transformers import pipeline
|
|
67
|
+
|
|
68
|
+
from gap_finder import GapFinder
|
|
69
|
+
|
|
70
|
+
language_model = pipeline(
|
|
71
|
+
"text-generation",
|
|
72
|
+
model="Qwen/Qwen3-0.6B",
|
|
73
|
+
device="cuda",
|
|
74
|
+
max_new_tokens=1024,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
gap_finder = GapFinder.from_pdf(
|
|
78
|
+
folder_path="documents",
|
|
79
|
+
language_model=language_model,
|
|
80
|
+
question_generation_batch_size=8,
|
|
81
|
+
)
|
|
82
|
+
topics = gap_finder.find()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Generate or provide questions separately
|
|
86
|
+
|
|
87
|
+
Questions can be generated before the rest of the pipeline. Because
|
|
88
|
+
`create_questions()` stores its result on the instance, the following call to
|
|
89
|
+
`find()` reuses them instead of generating them again:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
questions = gap_finder.create_questions()
|
|
93
|
+
# Inspect or persist questions here.
|
|
94
|
+
topics = gap_finder.find()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
To skip automatic question generation, provide existing questions when the
|
|
98
|
+
instance is created or set them later:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
gap_finder = GapFinder.from_pdf(
|
|
102
|
+
folder_path="documents",
|
|
103
|
+
language_model="your-model-name",
|
|
104
|
+
base_url="https://your-provider.example/v1",
|
|
105
|
+
questions=[
|
|
106
|
+
"Which populations are not represented in the study?",
|
|
107
|
+
"How does the method behave outside the tested conditions?",
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Equivalent after construction:
|
|
112
|
+
gap_finder.set_questions(["Which limitations remain unexamined?"])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A questions DataFrame must contain a `question` column. Additional columns are
|
|
116
|
+
preserved as the pipeline adds topic and ranking information.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Use `GapFinder.from_pdf()` for a folder of PDFs, `GapFinder.from_chunks()` for
|
|
121
|
+
pre-split text, or construct `GapFinder` directly with a populated vector store.
|
|
122
|
+
|
|
123
|
+
## Vector stores
|
|
124
|
+
|
|
125
|
+
Two vector-store implementations are included:
|
|
126
|
+
|
|
127
|
+
- `NumpyVectorStore` is the default and performs exact cosine-similarity search
|
|
128
|
+
with NumPy.
|
|
129
|
+
- `FaissVectorStore` performs exact cosine-similarity search with a FAISS
|
|
130
|
+
`IndexFlatIP` index.
|
|
131
|
+
|
|
132
|
+
Select FAISS while building the index:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from gap_finder import GapFinder
|
|
136
|
+
from gap_finder.vector_store import FaissVectorStore
|
|
137
|
+
|
|
138
|
+
gap_finder = GapFinder.from_pdf(
|
|
139
|
+
folder_path="documents",
|
|
140
|
+
vector_store_cls=FaissVectorStore,
|
|
141
|
+
language_model="your-model-name",
|
|
142
|
+
base_url="https://your-provider.example/v1",
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Custom backends can subclass `VectorStore`.
|
|
147
|
+
|
|
148
|
+
## Pipeline
|
|
149
|
+
|
|
150
|
+
`find()` runs the following steps:
|
|
151
|
+
|
|
152
|
+
1. Load chunks and embeddings from the vector store.
|
|
153
|
+
2. Reuse configured questions or generate questions with the language model.
|
|
154
|
+
3. Group questions into topics with BERTopic.
|
|
155
|
+
4. Compare questions with the corpus and aggregate coverage scores per topic.
|
|
156
|
+
5. Add titles and explanations to the lowest-coverage topics.
|
|
157
|
+
6. Score those topics for domain relevance when `domain_description` is set.
|
|
158
|
+
7. Create a Markdown report section for each explained topic.
|
|
159
|
+
|
|
160
|
+
Lower coverage means the source corpus covers a topic less well and therefore
|
|
161
|
+
indicates a stronger potential knowledge gap.
|
|
162
|
+
|
|
163
|
+
## Results
|
|
164
|
+
|
|
165
|
+
`find()` returns the final topics DataFrame and stores the same object in
|
|
166
|
+
`gap_finder.topics`. Its columns are:
|
|
167
|
+
|
|
168
|
+
| Column | Description |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `id` | BERTopic topic identifier; `-1` represents outliers. |
|
|
171
|
+
| `topic_words` | Representative `(word, score)` pairs. |
|
|
172
|
+
| `question_count` | Number of questions assigned to the topic. |
|
|
173
|
+
| `avg_similarity` | Mean question-to-corpus similarity and primary coverage score. |
|
|
174
|
+
| `std_similarity` | Standard deviation of question similarity. |
|
|
175
|
+
| `min_similarity`, `max_similarity` | Similarity range within the topic. |
|
|
176
|
+
| `title`, `explanation` | LLM-generated description for the configured lowest-coverage topics. |
|
|
177
|
+
| `relevance_score`, `relevance_reason` | Optional domain-relevance result, present when `domain_description` is set. |
|
|
178
|
+
| `report` | Markdown summary for an explained topic. |
|
|
179
|
+
|
|
180
|
+
After `find()`, `gap_finder.questions` contains the question records enriched
|
|
181
|
+
with `topic_id`, an optional `topic_prob`, an `embedding`, and a
|
|
182
|
+
`similarity_score`. Automatically generated questions also contain `id` and
|
|
183
|
+
`chunk_id`; caller-provided columns are retained.
|
|
184
|
+
|
|
185
|
+
`gap_finder.topic_model` is the fitted `BERTopic` object, not a table. Use its
|
|
186
|
+
standard API for deeper inspection, for example:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
topic_info = gap_finder.topic_model.get_topic_info()
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
Install the project and development dependencies with:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv sync
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Run the test suite:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
uv run pytest
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The repository also has opt-in integration tests for real embedding models,
|
|
207
|
+
BERTopic, local language models, and OpenAI-compatible APIs. See the
|
|
208
|
+
[integration test guide](test/integration/README.md) for configuration and
|
|
209
|
+
commands. These tests may download models and can require a GPU or a configured
|
|
210
|
+
API endpoint.
|
|
211
|
+
|
|
212
|
+
To contribute, open an issue/ticket or submit a pull/merge request.
|
|
213
|
+
|
|
214
|
+
## License
|
|
215
|
+
|
|
216
|
+
Gap Finder is available under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gap-finder"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Identify potential knowledge gaps in document collections using language models and topic modeling"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Ben Biesenbach", email = "bbi@informatik.uni-kiel.de" },
|
|
10
|
+
{ name = "Josef Küstner", email = "josef.kuestner@informatik.uni-kiel.de" }
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"bertopic>=0.17.4",
|
|
18
|
+
"faiss-cpu>=1.14.3",
|
|
19
|
+
"jinja2>=3.1.6",
|
|
20
|
+
"langchain-text-splitters>=1.1.2",
|
|
21
|
+
"openai>=2.46.0",
|
|
22
|
+
"sentence-transformers>=5.6.0",
|
|
23
|
+
"tqdm>=4.68.3",
|
|
24
|
+
"unstructured[pdf]>=0.18.32,<0.23.1; python_version < '3.11'",
|
|
25
|
+
"unstructured[pdf]>=0.23.1; python_version >= '3.11'",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/KI-Schleswig-Holstein/gap-finder"
|
|
30
|
+
Repository = "https://github.com/KI-Schleswig-Holstein/gap-finder"
|
|
31
|
+
Issues = "https://github.com/KI-Schleswig-Holstein/gap-finder/issues"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.11.16,<0.12.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"python-dotenv>=1.2.1",
|
|
40
|
+
"pytest>=9.1.1",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
markers = [
|
|
45
|
+
"vs: vector-store integration tests (pdf -> chunk -> embed -> FaissVectorStore); downloads and runs a real embedding model (slow, needs network)",
|
|
46
|
+
"llm: real chat-model integration tests; requires GAP_FINDER_RUN_LLM_INTEGRATION=1 and configured local/API models",
|
|
47
|
+
"embedding: real embedding-model integration tests; requires GAP_FINDER_RUN_EMBEDDING_INTEGRATION=1 and configured local/API models",
|
|
48
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from gap_finder._auth import MissingAPIKeyError
|
|
4
|
+
from gap_finder.model_types import (
|
|
5
|
+
EmbeddingModelInput,
|
|
6
|
+
LanguageModelInput,
|
|
7
|
+
SentenceTransformerLike,
|
|
8
|
+
TextGenerationPipelineLike,
|
|
9
|
+
TopicEmbeddingModelInput,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from gap_finder.main import GapFinder
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"EmbeddingModelInput",
|
|
17
|
+
"GapFinder",
|
|
18
|
+
"LanguageModelInput",
|
|
19
|
+
"MissingAPIKeyError",
|
|
20
|
+
"SentenceTransformerLike",
|
|
21
|
+
"TextGenerationPipelineLike",
|
|
22
|
+
"TopicEmbeddingModelInput",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def __getattr__(name: str):
|
|
27
|
+
if name == "GapFinder":
|
|
28
|
+
from gap_finder.main import GapFinder
|
|
29
|
+
|
|
30
|
+
return GapFinder
|
|
31
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MissingAPIKeyError(RuntimeError):
|
|
5
|
+
"""Raised when no API key was provided or configured."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_api_key(
|
|
9
|
+
explicit: str | None = None,
|
|
10
|
+
env_var: str = "GAP_FINDER_API_KEY",
|
|
11
|
+
) -> str:
|
|
12
|
+
key = explicit or os.getenv(env_var)
|
|
13
|
+
if not key:
|
|
14
|
+
raise MissingAPIKeyError(
|
|
15
|
+
f"No API key found. Pass api_key=... or set the environment "
|
|
16
|
+
f"variable {env_var}."
|
|
17
|
+
)
|
|
18
|
+
return key
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from gap_finder.embedding_model.client import OpenAIEmbeddingClient
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from gap_finder.embedding_model.embedding_model import EmbeddingModel
|
|
8
|
+
from gap_finder.embedding_model.factory import create_embedding_model
|
|
9
|
+
from gap_finder.embedding_model.local_embedding_model import LocalEmbeddingModel
|
|
10
|
+
from gap_finder.embedding_model.openai_embedding_model import OpenAIEmbeddingModel
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"EmbeddingModel",
|
|
14
|
+
"LocalEmbeddingModel",
|
|
15
|
+
"OpenAIEmbeddingClient",
|
|
16
|
+
"OpenAIEmbeddingModel",
|
|
17
|
+
"create_embedding_model",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def __getattr__(name: str):
|
|
22
|
+
if name == "EmbeddingModel":
|
|
23
|
+
from gap_finder.embedding_model.embedding_model import EmbeddingModel
|
|
24
|
+
|
|
25
|
+
return EmbeddingModel
|
|
26
|
+
if name == "create_embedding_model":
|
|
27
|
+
from gap_finder.embedding_model.factory import create_embedding_model
|
|
28
|
+
|
|
29
|
+
return create_embedding_model
|
|
30
|
+
if name == "LocalEmbeddingModel":
|
|
31
|
+
from gap_finder.embedding_model.local_embedding_model import LocalEmbeddingModel
|
|
32
|
+
|
|
33
|
+
return LocalEmbeddingModel
|
|
34
|
+
if name == "OpenAIEmbeddingModel":
|
|
35
|
+
from gap_finder.embedding_model.openai_embedding_model import OpenAIEmbeddingModel
|
|
36
|
+
|
|
37
|
+
return OpenAIEmbeddingModel
|
|
38
|
+
if name == "factory":
|
|
39
|
+
return import_module("gap_finder.embedding_model.factory")
|
|
40
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
|
|
3
|
+
from gap_finder._auth import resolve_api_key
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OpenAIEmbeddingClient:
|
|
7
|
+
"""An OpenAI client paired with the embedding model it should call."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
model_name: str,
|
|
12
|
+
base_url: str | None = None,
|
|
13
|
+
api_key: str | None = None,
|
|
14
|
+
client: OpenAI | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.model_name = model_name
|
|
17
|
+
self.client = (
|
|
18
|
+
client
|
|
19
|
+
if client is not None
|
|
20
|
+
else OpenAI(
|
|
21
|
+
base_url=base_url,
|
|
22
|
+
api_key=resolve_api_key(api_key),
|
|
23
|
+
)
|
|
24
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EmbeddingModel(ABC):
|
|
7
|
+
"""Common interface for local and API-backed text embedding models."""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def encode(
|
|
11
|
+
self,
|
|
12
|
+
texts: list[str],
|
|
13
|
+
show_progress_bar: bool = False,
|
|
14
|
+
batch_size: int = 32,
|
|
15
|
+
) -> np.ndarray:
|
|
16
|
+
"""Encode text into an embedding matrix."""
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def embed(self, documents: list[str], verbose: bool = False) -> np.ndarray:
|
|
20
|
+
"""Embed documents using BERTopic-compatible arguments."""
|
|
21
|
+
return self.encode(
|
|
22
|
+
documents,
|
|
23
|
+
show_progress_bar=verbose,
|
|
24
|
+
batch_size=32,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def activate(self) -> None:
|
|
28
|
+
"""Make internally owned resources ready for use."""
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
def deactivate(self) -> None:
|
|
32
|
+
"""Optionally offload internally owned resources between steps."""
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
def cleanup(self) -> None:
|
|
36
|
+
"""Release internally owned resources after the pipeline finishes."""
|
|
37
|
+
return
|