bridgekit 0.2.0__tar.gz → 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: AI tools that make you a better data scientist, not a redundant one.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/getbridgekit/bridgekit
@@ -21,8 +21,13 @@ Requires-Dist: pypdf>=3.0.0
21
21
  Requires-Dist: python-docx>=1.0.0
22
22
  Requires-Dist: python-pptx>=0.6.0
23
23
  Requires-Dist: nbformat>=5.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
24
27
  Dynamic: license-file
25
28
 
29
+ <img src="assets/logo.png" width="150"/>
30
+
26
31
  # Bridgekit
27
32
 
28
33
  **AI tools that make you a better data scientist, not a redundant one.**
@@ -148,6 +153,8 @@ Uses a vector database and semantic similarity to find relevant context across y
148
153
 
149
154
  Supports `.txt`, `.md`, `.pdf`, `.docx`, `.pptx`, and `.ipynb` files.
150
155
 
156
+ > **Note:** The first run will download the MiniLM embedding model (~90MB). This is a one-time download — it gets cached locally for all subsequent calls.
157
+
151
158
  **From a folder:**
152
159
  ```python
153
160
  from bridgekit import ask
@@ -1,3 +1,5 @@
1
+ <img src="assets/logo.png" width="150"/>
2
+
1
3
  # Bridgekit
2
4
 
3
5
  **AI tools that make you a better data scientist, not a redundant one.**
@@ -123,6 +125,8 @@ Uses a vector database and semantic similarity to find relevant context across y
123
125
 
124
126
  Supports `.txt`, `.md`, `.pdf`, `.docx`, `.pptx`, and `.ipynb` files.
125
127
 
128
+ > **Note:** The first run will download the MiniLM embedding model (~90MB). This is a one-time download — it gets cached locally for all subsequent calls.
129
+
126
130
  **From a folder:**
127
131
  ```python
128
132
  from bridgekit import ask
@@ -1,5 +1,5 @@
1
1
  from .reviewer import evaluate
2
2
  from .search import ask
3
3
 
4
- __version__ = "0.2.0"
4
+ __version__ = "0.2.1"
5
5
  __all__ = ["evaluate", "ask"]
@@ -56,6 +56,9 @@ def evaluate(text: str) -> str:
56
56
  Returns:
57
57
  Structured feedback across five dimensions.
58
58
  """
59
+ if not text or not text.strip():
60
+ raise ValueError("Text cannot be empty.")
61
+
59
62
  api_key = os.environ.get("ANTHROPIC_API_KEY")
60
63
  if not api_key:
61
64
  raise EnvironmentError(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: AI tools that make you a better data scientist, not a redundant one.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/getbridgekit/bridgekit
@@ -21,8 +21,13 @@ Requires-Dist: pypdf>=3.0.0
21
21
  Requires-Dist: python-docx>=1.0.0
22
22
  Requires-Dist: python-pptx>=0.6.0
23
23
  Requires-Dist: nbformat>=5.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
26
+ Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
24
27
  Dynamic: license-file
25
28
 
29
+ <img src="assets/logo.png" width="150"/>
30
+
26
31
  # Bridgekit
27
32
 
28
33
  **AI tools that make you a better data scientist, not a redundant one.**
@@ -148,6 +153,8 @@ Uses a vector database and semantic similarity to find relevant context across y
148
153
 
149
154
  Supports `.txt`, `.md`, `.pdf`, `.docx`, `.pptx`, and `.ipynb` files.
150
155
 
156
+ > **Note:** The first run will download the MiniLM embedding model (~90MB). This is a one-time download — it gets cached locally for all subsequent calls.
157
+
151
158
  **From a folder:**
152
159
  ```python
153
160
  from bridgekit import ask
@@ -8,4 +8,6 @@ bridgekit.egg-info/PKG-INFO
8
8
  bridgekit.egg-info/SOURCES.txt
9
9
  bridgekit.egg-info/dependency_links.txt
10
10
  bridgekit.egg-info/requires.txt
11
- bridgekit.egg-info/top_level.txt
11
+ bridgekit.egg-info/top_level.txt
12
+ tests/test_reviewer.py
13
+ tests/test_search.py
@@ -5,3 +5,7 @@ pypdf>=3.0.0
5
5
  python-docx>=1.0.0
6
6
  python-pptx>=0.6.0
7
7
  nbformat>=5.0.0
8
+
9
+ [dev]
10
+ pytest>=7.0.0
11
+ pytest-mock>=3.0.0
@@ -7,7 +7,7 @@ include = ["bridgekit*"]
7
7
 
8
8
  [project]
9
9
  name = "bridgekit"
10
- version = "0.2.0"
10
+ version = "0.2.1"
11
11
  description = "AI tools that make you a better data scientist, not a redundant one."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.9"
@@ -30,6 +30,12 @@ dependencies = [
30
30
  "nbformat>=5.0.0",
31
31
  ]
32
32
 
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0.0",
36
+ "pytest-mock>=3.0.0",
37
+ ]
38
+
33
39
  [project.urls]
34
40
  Homepage = "https://github.com/getbridgekit/bridgekit"
35
41
  Issues = "https://github.com/getbridgekit/bridgekit/issues"
@@ -0,0 +1,162 @@
1
+ import os
2
+ import pytest
3
+ from unittest.mock import MagicMock, patch
4
+
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Helpers
8
+ # ---------------------------------------------------------------------------
9
+
10
+ def _make_mock_message(text: str):
11
+ """Build a minimal fake Anthropic message response."""
12
+ content_block = MagicMock()
13
+ content_block.text = text
14
+ message = MagicMock()
15
+ message.content = [content_block]
16
+ return message
17
+
18
+
19
+ FAKE_RESPONSE = (
20
+ "BRIDGEKIT ANALYSIS REVIEW\n"
21
+ "─────────────────────────────────────────\n\n"
22
+ "1. CLARITY\n"
23
+ "✅ STRONG — The writeup is clear and jargon-free.\n\n"
24
+ "2. AUDIENCE CLARITY\n"
25
+ "✅ STRONG — Written for the right audience.\n\n"
26
+ "3. STATISTICAL RIGOR\n"
27
+ "⚠️ NEEDS WORK — Sample size is not mentioned.\n\n"
28
+ "4. METHODOLOGY\n"
29
+ "✅ STRONG — Approach is well explained.\n\n"
30
+ "5. BUSINESS IMPACT\n"
31
+ "❌ MISSING — No quantified outcomes.\n\n"
32
+ "─────────────────────────────────────────\n"
33
+ "BOTTOM LINE\n"
34
+ "Add specific metrics to quantify business impact."
35
+ )
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Tests
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class TestEvaluateReturnsString:
43
+ """evaluate() should return a plain string."""
44
+
45
+ def test_returns_string(self):
46
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
47
+ with patch("anthropic.Anthropic") as MockAnthropic:
48
+ mock_client = MagicMock()
49
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
50
+ MockAnthropic.return_value = mock_client
51
+
52
+ from bridgekit.reviewer import evaluate
53
+ result = evaluate("We ran an A/B test on 500 users.")
54
+
55
+ assert isinstance(result, str)
56
+
57
+ def test_returns_non_empty_string(self):
58
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
59
+ with patch("anthropic.Anthropic") as MockAnthropic:
60
+ mock_client = MagicMock()
61
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
62
+ MockAnthropic.return_value = mock_client
63
+
64
+ from bridgekit.reviewer import evaluate
65
+ result = evaluate("We ran an A/B test on 500 users.")
66
+
67
+ assert len(result) > 0
68
+
69
+
70
+ class TestEvaluateOutputStructure:
71
+ """evaluate() output should contain the required section headers."""
72
+
73
+ def test_output_contains_clarity(self):
74
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
75
+ with patch("anthropic.Anthropic") as MockAnthropic:
76
+ mock_client = MagicMock()
77
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
78
+ MockAnthropic.return_value = mock_client
79
+
80
+ from bridgekit.reviewer import evaluate
81
+ result = evaluate("Some analysis text.")
82
+
83
+ assert "CLARITY" in result
84
+
85
+ def test_output_contains_bottom_line(self):
86
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
87
+ with patch("anthropic.Anthropic") as MockAnthropic:
88
+ mock_client = MagicMock()
89
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
90
+ MockAnthropic.return_value = mock_client
91
+
92
+ from bridgekit.reviewer import evaluate
93
+ result = evaluate("Some analysis text.")
94
+
95
+ assert "BOTTOM LINE" in result
96
+
97
+ def test_output_contains_both_required_sections(self):
98
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
99
+ with patch("anthropic.Anthropic") as MockAnthropic:
100
+ mock_client = MagicMock()
101
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
102
+ MockAnthropic.return_value = mock_client
103
+
104
+ from bridgekit.reviewer import evaluate
105
+ result = evaluate("Some analysis text.")
106
+
107
+ assert "CLARITY" in result and "BOTTOM LINE" in result
108
+
109
+
110
+ class TestEvaluateMissingApiKey:
111
+ """evaluate() should raise EnvironmentError when the API key is absent."""
112
+
113
+ def test_raises_environment_error_when_key_missing(self):
114
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
115
+ with patch.dict(os.environ, env, clear=True):
116
+ from bridgekit.reviewer import evaluate
117
+ with pytest.raises(EnvironmentError):
118
+ evaluate("Some analysis text.")
119
+
120
+ def test_error_message_mentions_key(self):
121
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
122
+ with patch.dict(os.environ, env, clear=True):
123
+ from bridgekit.reviewer import evaluate
124
+ with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
125
+ evaluate("Some analysis text.")
126
+
127
+
128
+ class TestEvaluateEmptyInput:
129
+ """evaluate() should raise ValueError for empty or whitespace-only input."""
130
+
131
+ def test_empty_string_raises_value_error(self):
132
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
133
+ from bridgekit.reviewer import evaluate
134
+ with pytest.raises(ValueError, match="empty"):
135
+ evaluate("")
136
+
137
+ def test_whitespace_only_raises_value_error(self):
138
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
139
+ from bridgekit.reviewer import evaluate
140
+ with pytest.raises(ValueError, match="empty"):
141
+ evaluate(" ")
142
+
143
+
144
+ class TestEvaluateApiCallShape:
145
+ """evaluate() should pass the user text through to the Anthropic API."""
146
+
147
+ def test_api_called_with_user_text(self):
148
+ user_text = "Our conversion rate improved after the campaign."
149
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
150
+ with patch("anthropic.Anthropic") as MockAnthropic:
151
+ mock_client = MagicMock()
152
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
153
+ MockAnthropic.return_value = mock_client
154
+
155
+ from bridgekit.reviewer import evaluate
156
+ evaluate(user_text)
157
+
158
+ call_kwargs = mock_client.messages.create.call_args
159
+ # The user text should appear somewhere in the messages payload
160
+ messages_arg = call_kwargs.kwargs.get("messages") or call_kwargs.args[0]
161
+ content = str(messages_arg)
162
+ assert user_text in content
@@ -0,0 +1,238 @@
1
+ import os
2
+ import pytest
3
+ from pathlib import Path
4
+ from unittest.mock import MagicMock, patch
5
+ import tempfile
6
+
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Helpers
10
+ # ---------------------------------------------------------------------------
11
+
12
+ def _make_mock_message(text: str):
13
+ """Build a minimal fake Anthropic message response."""
14
+ content_block = MagicMock()
15
+ content_block.text = text
16
+ message = MagicMock()
17
+ message.content = [content_block]
18
+ return message
19
+
20
+
21
+ FAKE_ANSWER = "Based on the documents, the conversion rate increased by 12%."
22
+
23
+
24
+ def _make_mock_chromadb(chunks: list[str] | None = None):
25
+ """
26
+ Return a (mock_chromadb_module, mock_embedding_fn_class) pair whose
27
+ collection.query() returns the supplied chunks as context.
28
+ """
29
+ returned_docs = chunks if chunks is not None else ["sample context chunk"]
30
+
31
+ mock_collection = MagicMock()
32
+ mock_collection.query.return_value = {"documents": [returned_docs]}
33
+
34
+ mock_chroma_client = MagicMock()
35
+ mock_chroma_client.get_or_create_collection.return_value = mock_collection
36
+
37
+ mock_chromadb = MagicMock()
38
+ mock_chromadb.Client.return_value = mock_chroma_client
39
+
40
+ mock_embedding_fn_class = MagicMock()
41
+ mock_embedding_fn_class.return_value = MagicMock()
42
+
43
+ return mock_chromadb, mock_embedding_fn_class
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Tests
48
+ # ---------------------------------------------------------------------------
49
+
50
+ class TestAskReturnsString:
51
+ """ask() should return a non-empty string."""
52
+
53
+ def test_returns_string_with_text_input(self):
54
+ mock_chromadb, mock_ef = _make_mock_chromadb()
55
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
56
+ with patch("anthropic.Anthropic") as MockAnthropic, \
57
+ patch("chromadb.Client", mock_chromadb.Client), \
58
+ patch(
59
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
60
+ mock_ef,
61
+ ):
62
+ mock_client = MagicMock()
63
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
64
+ MockAnthropic.return_value = mock_client
65
+
66
+ from bridgekit.search import ask
67
+ result = ask("What was the conversion rate?", text="The conversion rate increased by 12%.")
68
+
69
+ assert isinstance(result, str)
70
+ assert len(result) > 0
71
+
72
+ def test_returns_non_empty_answer(self):
73
+ mock_chromadb, mock_ef = _make_mock_chromadb()
74
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
75
+ with patch("anthropic.Anthropic") as MockAnthropic, \
76
+ patch("chromadb.Client", mock_chromadb.Client), \
77
+ patch(
78
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
79
+ mock_ef,
80
+ ):
81
+ mock_client = MagicMock()
82
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
83
+ MockAnthropic.return_value = mock_client
84
+
85
+ from bridgekit.search import ask
86
+ result = ask("What was the conversion rate?", text="The conversion rate increased by 12%.")
87
+
88
+ assert result == FAKE_ANSWER
89
+
90
+
91
+ class TestAskMissingSourceAndText:
92
+ """ask() should raise ValueError when neither source nor text is supplied."""
93
+
94
+ def test_raises_value_error_with_no_inputs(self):
95
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
96
+ from bridgekit.search import ask
97
+ with pytest.raises(ValueError, match="source"):
98
+ ask("What happened?")
99
+
100
+ def test_raises_value_error_message_mentions_text(self):
101
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
102
+ from bridgekit.search import ask
103
+ with pytest.raises(ValueError):
104
+ ask("What happened?", source=None, text=None)
105
+
106
+
107
+ class TestAskMissingApiKey:
108
+ """ask() should raise EnvironmentError when ANTHROPIC_API_KEY is absent."""
109
+
110
+ def test_raises_environment_error_when_key_missing(self):
111
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
112
+ with patch.dict(os.environ, env, clear=True):
113
+ from bridgekit.search import ask
114
+ with pytest.raises(EnvironmentError):
115
+ ask("What happened?", text="Some text about results.")
116
+
117
+ def test_error_message_mentions_key(self):
118
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
119
+ with patch.dict(os.environ, env, clear=True):
120
+ from bridgekit.search import ask
121
+ with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
122
+ ask("What happened?", text="Some text about results.")
123
+
124
+
125
+ class TestAskWithTextInput:
126
+ """ask() should work correctly when called with the text= parameter."""
127
+
128
+ def test_text_input_reaches_api(self):
129
+ raw_text = "Revenue grew 25% year-over-year driven by enterprise sales."
130
+ mock_chromadb, mock_ef = _make_mock_chromadb([raw_text])
131
+
132
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
133
+ with patch("anthropic.Anthropic") as MockAnthropic, \
134
+ patch("chromadb.Client", mock_chromadb.Client), \
135
+ patch(
136
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
137
+ mock_ef,
138
+ ):
139
+ mock_client = MagicMock()
140
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
141
+ MockAnthropic.return_value = mock_client
142
+
143
+ from bridgekit.search import ask
144
+ ask("What drove revenue growth?", text=raw_text)
145
+
146
+ # Verify the Anthropic API was actually called once
147
+ assert mock_client.messages.create.call_count == 1
148
+
149
+ def test_text_input_included_in_context(self):
150
+ raw_text = "Churn dropped from 8% to 3% after onboarding improvements."
151
+ mock_chromadb, mock_ef = _make_mock_chromadb([raw_text])
152
+
153
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
154
+ with patch("anthropic.Anthropic") as MockAnthropic, \
155
+ patch("chromadb.Client", mock_chromadb.Client), \
156
+ patch(
157
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
158
+ mock_ef,
159
+ ):
160
+ mock_client = MagicMock()
161
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
162
+ MockAnthropic.return_value = mock_client
163
+
164
+ from bridgekit.search import ask
165
+ ask("What happened to churn?", text=raw_text)
166
+
167
+ call_kwargs = mock_client.messages.create.call_args
168
+ messages_arg = call_kwargs.kwargs.get("messages") or call_kwargs.args[0]
169
+ # The retrieved chunk (raw_text) should appear in the prompt context
170
+ assert raw_text in str(messages_arg)
171
+
172
+
173
+ class TestAskWithSourceFolder:
174
+ """ask() should load .txt files from a folder and pass their content to the API."""
175
+
176
+ def test_source_folder_with_txt_file(self):
177
+ with tempfile.TemporaryDirectory() as tmpdir:
178
+ sample_file = Path(tmpdir) / "report.txt"
179
+ sample_content = "The experiment showed a 15% lift in click-through rate."
180
+ sample_file.write_text(sample_content, encoding="utf-8")
181
+
182
+ mock_chromadb, mock_ef = _make_mock_chromadb([sample_content])
183
+
184
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
185
+ with patch("anthropic.Anthropic") as MockAnthropic, \
186
+ patch("chromadb.Client", mock_chromadb.Client), \
187
+ patch(
188
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
189
+ mock_ef,
190
+ ):
191
+ mock_client = MagicMock()
192
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
193
+ MockAnthropic.return_value = mock_client
194
+
195
+ from bridgekit.search import ask
196
+ result = ask("What was the lift?", source=tmpdir)
197
+
198
+ assert isinstance(result, str)
199
+ assert len(result) > 0
200
+
201
+ def test_source_folder_calls_api_once(self):
202
+ with tempfile.TemporaryDirectory() as tmpdir:
203
+ (Path(tmpdir) / "notes.txt").write_text(
204
+ "User satisfaction scores improved by 20 points.", encoding="utf-8"
205
+ )
206
+
207
+ mock_chromadb, mock_ef = _make_mock_chromadb()
208
+
209
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
210
+ with patch("anthropic.Anthropic") as MockAnthropic, \
211
+ patch("chromadb.Client", mock_chromadb.Client), \
212
+ patch(
213
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
214
+ mock_ef,
215
+ ):
216
+ mock_client = MagicMock()
217
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
218
+ MockAnthropic.return_value = mock_client
219
+
220
+ from bridgekit.search import ask
221
+ ask("How did satisfaction change?", source=tmpdir)
222
+
223
+ assert mock_client.messages.create.call_count == 1
224
+
225
+ def test_source_folder_empty_raises_value_error(self):
226
+ with tempfile.TemporaryDirectory() as tmpdir:
227
+ # Folder exists but has no supported files
228
+ mock_chromadb, mock_ef = _make_mock_chromadb()
229
+
230
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
231
+ with patch("chromadb.Client", mock_chromadb.Client), \
232
+ patch(
233
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
234
+ mock_ef,
235
+ ):
236
+ from bridgekit.search import ask
237
+ with pytest.raises(ValueError, match="No content found"):
238
+ ask("What happened?", source=tmpdir)
File without changes
File without changes
File without changes