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.
Files changed (46) hide show
  1. gap_finder-0.1.0/LICENSE +21 -0
  2. gap_finder-0.1.0/PKG-INFO +240 -0
  3. gap_finder-0.1.0/README.md +216 -0
  4. gap_finder-0.1.0/pyproject.toml +48 -0
  5. gap_finder-0.1.0/src/gap_finder/__init__.py +31 -0
  6. gap_finder-0.1.0/src/gap_finder/_auth.py +18 -0
  7. gap_finder-0.1.0/src/gap_finder/embedding_model/__init__.py +40 -0
  8. gap_finder-0.1.0/src/gap_finder/embedding_model/client.py +24 -0
  9. gap_finder-0.1.0/src/gap_finder/embedding_model/embedding_model.py +37 -0
  10. gap_finder-0.1.0/src/gap_finder/embedding_model/factory.py +34 -0
  11. gap_finder-0.1.0/src/gap_finder/embedding_model/local_embedding_model.py +92 -0
  12. gap_finder-0.1.0/src/gap_finder/embedding_model/openai_embedding_model.py +43 -0
  13. gap_finder-0.1.0/src/gap_finder/io_helper.py +71 -0
  14. gap_finder-0.1.0/src/gap_finder/language_model/__init__.py +39 -0
  15. gap_finder-0.1.0/src/gap_finder/language_model/client.py +43 -0
  16. gap_finder-0.1.0/src/gap_finder/language_model/factory.py +40 -0
  17. gap_finder-0.1.0/src/gap_finder/language_model/language_model.py +31 -0
  18. gap_finder-0.1.0/src/gap_finder/language_model/local_language_model.py +69 -0
  19. gap_finder-0.1.0/src/gap_finder/language_model/openai_language_model.py +45 -0
  20. gap_finder-0.1.0/src/gap_finder/main.py +932 -0
  21. gap_finder-0.1.0/src/gap_finder/model_types.py +67 -0
  22. gap_finder-0.1.0/src/gap_finder/persistence.py +449 -0
  23. gap_finder-0.1.0/src/gap_finder/prompts/__init__.py +3 -0
  24. gap_finder-0.1.0/src/gap_finder/prompts/loader.py +44 -0
  25. gap_finder-0.1.0/src/gap_finder/prompts/templates/hyde.txt.j2 +13 -0
  26. gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/examples_bad.txt.j2 +3 -0
  27. gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/examples_good.txt.j2 +3 -0
  28. gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/main.txt.j2 +34 -0
  29. gap_finder-0.1.0/src/gap_finder/prompts/templates/question_generation/rules.txt.j2 +10 -0
  30. gap_finder-0.1.0/src/gap_finder/prompts/templates/summarize.txt.j2 +10 -0
  31. gap_finder-0.1.0/src/gap_finder/prompts/templates/topic_explanation.txt.j2 +21 -0
  32. gap_finder-0.1.0/src/gap_finder/prompts/templates/topic_relevance.txt.j2 +23 -0
  33. gap_finder-0.1.0/src/gap_finder/py.typed +0 -0
  34. gap_finder-0.1.0/src/gap_finder/question_generation.py +160 -0
  35. gap_finder-0.1.0/src/gap_finder/question_ranking.py +72 -0
  36. gap_finder-0.1.0/src/gap_finder/report.py +120 -0
  37. gap_finder-0.1.0/src/gap_finder/singleton/constants.py +47 -0
  38. gap_finder-0.1.0/src/gap_finder/topic_explanation.py +164 -0
  39. gap_finder-0.1.0/src/gap_finder/topic_generation.py +155 -0
  40. gap_finder-0.1.0/src/gap_finder/topic_ranking.py +109 -0
  41. gap_finder-0.1.0/src/gap_finder/topic_relevance.py +128 -0
  42. gap_finder-0.1.0/src/gap_finder/vector_store/__init__.py +18 -0
  43. gap_finder-0.1.0/src/gap_finder/vector_store/faiss_vector_store.py +74 -0
  44. gap_finder-0.1.0/src/gap_finder/vector_store/numpy_vector_store.py +88 -0
  45. gap_finder-0.1.0/src/gap_finder/vector_store/pdf_chunker.py +41 -0
  46. gap_finder-0.1.0/src/gap_finder/vector_store/vector_store.py +69 -0
@@ -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