bridgekit 0.3.5__tar.gz → 0.3.6__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.
- {bridgekit-0.3.5/bridgekit.egg-info → bridgekit-0.3.6}/PKG-INFO +60 -3
- bridgekit-0.3.5/PKG-INFO → bridgekit-0.3.6/README.md +55 -30
- bridgekit-0.3.6/bridgekit/config.py +83 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/planner.py +16 -17
- bridgekit-0.3.6/bridgekit/providers.py +112 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/redteam.py +18 -17
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/reviewer.py +19 -19
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/search.py +19 -16
- bridgekit-0.3.5/README.md → bridgekit-0.3.6/bridgekit.egg-info/PKG-INFO +87 -2
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/SOURCES.txt +2 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/requires.txt +6 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/pyproject.toml +3 -1
- bridgekit-0.3.6/tests/test_providers.py +71 -0
- bridgekit-0.3.5/bridgekit/config.py +0 -18
- {bridgekit-0.3.5 → bridgekit-0.3.6}/LICENSE +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/__init__.py +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/dependency_links.txt +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/top_level.txt +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/setup.cfg +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_config.py +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_planner.py +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_reviewer.py +0 -0
- {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_search.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bridgekit
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.6
|
|
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://usebridgekit.com
|
|
@@ -21,6 +21,10 @@ 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: openai
|
|
25
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
26
|
+
Provides-Extra: gemini
|
|
27
|
+
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
24
28
|
Provides-Extra: dev
|
|
25
29
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
30
|
Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
|
|
@@ -63,12 +67,26 @@ pip install bridgekit
|
|
|
63
67
|
!pip install bridgekit
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
Requires an
|
|
70
|
+
Requires an API key for your chosen provider:
|
|
67
71
|
|
|
72
|
+
**Anthropic (default):**
|
|
68
73
|
```bash
|
|
74
|
+
pip install bridgekit
|
|
69
75
|
export ANTHROPIC_API_KEY=your_key_here
|
|
70
76
|
```
|
|
71
77
|
|
|
78
|
+
**OpenAI:**
|
|
79
|
+
```bash
|
|
80
|
+
pip install bridgekit[openai]
|
|
81
|
+
export OPENAI_API_KEY=your_key_here
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Google Gemini:**
|
|
85
|
+
```bash
|
|
86
|
+
pip install bridgekit[gemini]
|
|
87
|
+
export GOOGLE_API_KEY=your_key_here
|
|
88
|
+
```
|
|
89
|
+
|
|
72
90
|
---
|
|
73
91
|
|
|
74
92
|
## Getting Started
|
|
@@ -369,6 +387,45 @@ willing to commit to — and what's your confidence interval on that estimate?"
|
|
|
369
387
|
|
|
370
388
|
---
|
|
371
389
|
|
|
390
|
+
## Multi-Provider Support
|
|
391
|
+
|
|
392
|
+
Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, or Google Gemini models with any tool.
|
|
393
|
+
|
|
394
|
+
**Using different providers:**
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
from bridgekit import evaluate, plan, ask, redteam
|
|
398
|
+
|
|
399
|
+
# Use OpenAI (default model: gpt-4o)
|
|
400
|
+
print(evaluate("Your analysis here", provider="openai"))
|
|
401
|
+
|
|
402
|
+
# Use Google Gemini (default model: gemini-1.5-pro)
|
|
403
|
+
print(plan("Your question here", provider="gemini"))
|
|
404
|
+
|
|
405
|
+
# Use specific model
|
|
406
|
+
print(redteam("Your analysis here", model="gpt-4-turbo"))
|
|
407
|
+
print(ask("Your question here", source="reports/", model="claude-3-opus-20240229"))
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Provider auto-detection:**
|
|
411
|
+
Bridgekit automatically detects the provider from model names:
|
|
412
|
+
- Models starting with "claude" → Anthropic
|
|
413
|
+
- Models starting with "gpt" → OpenAI
|
|
414
|
+
- Models starting with "gemini" → Google Gemini
|
|
415
|
+
|
|
416
|
+
**Default models by provider:**
|
|
417
|
+
- Anthropic: `claude-3-5-sonnet-20241022`
|
|
418
|
+
- OpenAI: `gpt-4o`
|
|
419
|
+
- Gemini: `gemini-1.5-pro`
|
|
420
|
+
|
|
421
|
+
All tools support the same `provider` and `model` parameters:
|
|
422
|
+
- `evaluate(text, provider=None, model=None)`
|
|
423
|
+
- `plan(question, provider=None, model=None, ...)`
|
|
424
|
+
- `ask(question, provider=None, model=None, ...)`
|
|
425
|
+
- `redteam(text, provider=None, model=None, ...)`
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
372
429
|
## Why not just use Claude?
|
|
373
430
|
|
|
374
431
|
You could. But you'd need to know what to ask, how to frame it, and what a good answer looks like. Bridgekit has that baked in — it knows you're a data scientist presenting findings, so it asks the right questions automatically. No prompt engineering required. Just paste your work and run it.
|
|
@@ -389,7 +446,7 @@ Bridgekit is a suite, not a one-off. Four tools are live — more are coming:
|
|
|
389
446
|
|
|
390
447
|
- **Stakeholder translator** — turn your technical findings into a narrative a non-technical audience will actually follow
|
|
391
448
|
- **Assumption checker** — state your analytical assumptions, get the ones you missed
|
|
392
|
-
- **
|
|
449
|
+
- **More specialized tools** — focused on specific data science workflows and challenges
|
|
393
450
|
|
|
394
451
|
Each tool is small, focused, and built for the way data scientists actually work.
|
|
395
452
|
|
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: bridgekit
|
|
3
|
-
Version: 0.3.5
|
|
4
|
-
Summary: AI tools that make you a better data scientist, not a redundant one.
|
|
5
|
-
License: MIT
|
|
6
|
-
Project-URL: Homepage, https://usebridgekit.com
|
|
7
|
-
Project-URL: Issues, https://github.com/getbridgekit/bridgekit/issues
|
|
8
|
-
Keywords: data science,AI,analysis,evaluation,anthropic
|
|
9
|
-
Classifier: Development Status :: 3 - Alpha
|
|
10
|
-
Classifier: Intended Audience :: Science/Research
|
|
11
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
-
Requires-Python: >=3.9
|
|
15
|
-
Description-Content-Type: text/markdown
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
Requires-Dist: anthropic>=0.20.0
|
|
18
|
-
Requires-Dist: chromadb>=0.4.0
|
|
19
|
-
Requires-Dist: sentence-transformers>=2.0.0
|
|
20
|
-
Requires-Dist: pypdf>=3.0.0
|
|
21
|
-
Requires-Dist: python-docx>=1.0.0
|
|
22
|
-
Requires-Dist: python-pptx>=0.6.0
|
|
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"
|
|
27
|
-
Dynamic: license-file
|
|
28
|
-
|
|
29
1
|
<p align="center">
|
|
30
2
|
<img src="https://raw.githubusercontent.com/getbridgekit/bridgekit/main/assets/logo.png" alt="Bridgekit" width="200"/>
|
|
31
3
|
</p>
|
|
@@ -63,12 +35,26 @@ pip install bridgekit
|
|
|
63
35
|
!pip install bridgekit
|
|
64
36
|
```
|
|
65
37
|
|
|
66
|
-
Requires an
|
|
38
|
+
Requires an API key for your chosen provider:
|
|
67
39
|
|
|
40
|
+
**Anthropic (default):**
|
|
68
41
|
```bash
|
|
42
|
+
pip install bridgekit
|
|
69
43
|
export ANTHROPIC_API_KEY=your_key_here
|
|
70
44
|
```
|
|
71
45
|
|
|
46
|
+
**OpenAI:**
|
|
47
|
+
```bash
|
|
48
|
+
pip install bridgekit[openai]
|
|
49
|
+
export OPENAI_API_KEY=your_key_here
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Google Gemini:**
|
|
53
|
+
```bash
|
|
54
|
+
pip install bridgekit[gemini]
|
|
55
|
+
export GOOGLE_API_KEY=your_key_here
|
|
56
|
+
```
|
|
57
|
+
|
|
72
58
|
---
|
|
73
59
|
|
|
74
60
|
## Getting Started
|
|
@@ -369,6 +355,45 @@ willing to commit to — and what's your confidence interval on that estimate?"
|
|
|
369
355
|
|
|
370
356
|
---
|
|
371
357
|
|
|
358
|
+
## Multi-Provider Support
|
|
359
|
+
|
|
360
|
+
Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, or Google Gemini models with any tool.
|
|
361
|
+
|
|
362
|
+
**Using different providers:**
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
from bridgekit import evaluate, plan, ask, redteam
|
|
366
|
+
|
|
367
|
+
# Use OpenAI (default model: gpt-4o)
|
|
368
|
+
print(evaluate("Your analysis here", provider="openai"))
|
|
369
|
+
|
|
370
|
+
# Use Google Gemini (default model: gemini-1.5-pro)
|
|
371
|
+
print(plan("Your question here", provider="gemini"))
|
|
372
|
+
|
|
373
|
+
# Use specific model
|
|
374
|
+
print(redteam("Your analysis here", model="gpt-4-turbo"))
|
|
375
|
+
print(ask("Your question here", source="reports/", model="claude-3-opus-20240229"))
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Provider auto-detection:**
|
|
379
|
+
Bridgekit automatically detects the provider from model names:
|
|
380
|
+
- Models starting with "claude" → Anthropic
|
|
381
|
+
- Models starting with "gpt" → OpenAI
|
|
382
|
+
- Models starting with "gemini" → Google Gemini
|
|
383
|
+
|
|
384
|
+
**Default models by provider:**
|
|
385
|
+
- Anthropic: `claude-3-5-sonnet-20241022`
|
|
386
|
+
- OpenAI: `gpt-4o`
|
|
387
|
+
- Gemini: `gemini-1.5-pro`
|
|
388
|
+
|
|
389
|
+
All tools support the same `provider` and `model` parameters:
|
|
390
|
+
- `evaluate(text, provider=None, model=None)`
|
|
391
|
+
- `plan(question, provider=None, model=None, ...)`
|
|
392
|
+
- `ask(question, provider=None, model=None, ...)`
|
|
393
|
+
- `redteam(text, provider=None, model=None, ...)`
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
372
397
|
## Why not just use Claude?
|
|
373
398
|
|
|
374
399
|
You could. But you'd need to know what to ask, how to frame it, and what a good answer looks like. Bridgekit has that baked in — it knows you're a data scientist presenting findings, so it asks the right questions automatically. No prompt engineering required. Just paste your work and run it.
|
|
@@ -389,7 +414,7 @@ Bridgekit is a suite, not a one-off. Four tools are live — more are coming:
|
|
|
389
414
|
|
|
390
415
|
- **Stakeholder translator** — turn your technical findings into a narrative a non-technical audience will actually follow
|
|
391
416
|
- **Assumption checker** — state your analytical assumptions, get the ones you missed
|
|
392
|
-
- **
|
|
417
|
+
- **More specialized tools** — focused on specific data science workflows and challenges
|
|
393
418
|
|
|
394
419
|
Each tool is small, focused, and built for the way data scientists actually work.
|
|
395
420
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Provider(Enum):
|
|
7
|
+
ANTHROPIC = "anthropic"
|
|
8
|
+
OPENAI = "openai"
|
|
9
|
+
GEMINI = "gemini"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Default models for each provider
|
|
13
|
+
DEFAULT_MODELS = {
|
|
14
|
+
Provider.ANTHROPIC: "claude-opus-4-6",
|
|
15
|
+
Provider.OPENAI: "gpt-4o",
|
|
16
|
+
Provider.GEMINI: "gemini-1.5-pro"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Legacy support
|
|
20
|
+
DEFAULT_MODEL = DEFAULT_MODELS[Provider.ANTHROPIC]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def require_api_key(provider: Provider = Provider.ANTHROPIC) -> str:
|
|
24
|
+
"""Return the API key for the specified provider from the environment, or raise a clear error.
|
|
25
|
+
|
|
26
|
+
Each Bridgekit tool calls this before constructing a client so
|
|
27
|
+
users get the same friendly message instead of whatever the SDK surfaces
|
|
28
|
+
when the key is missing.
|
|
29
|
+
"""
|
|
30
|
+
if provider == Provider.ANTHROPIC:
|
|
31
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
32
|
+
if not api_key:
|
|
33
|
+
raise EnvironmentError(
|
|
34
|
+
"ANTHROPIC_API_KEY not found. Set it with: export ANTHROPIC_API_KEY=your_key_here"
|
|
35
|
+
)
|
|
36
|
+
return api_key
|
|
37
|
+
elif provider == Provider.OPENAI:
|
|
38
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
39
|
+
if not api_key:
|
|
40
|
+
raise EnvironmentError(
|
|
41
|
+
"OPENAI_API_KEY not found. Set it with: export OPENAI_API_KEY=your_key_here"
|
|
42
|
+
)
|
|
43
|
+
return api_key
|
|
44
|
+
elif provider == Provider.GEMINI:
|
|
45
|
+
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
46
|
+
if not api_key:
|
|
47
|
+
raise EnvironmentError(
|
|
48
|
+
"GOOGLE_API_KEY not found. Set it with: export GOOGLE_API_KEY=your_key_here"
|
|
49
|
+
)
|
|
50
|
+
return api_key
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def require_anthropic_api_key() -> str:
|
|
56
|
+
"""Legacy function for backward compatibility."""
|
|
57
|
+
return require_api_key(Provider.ANTHROPIC)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_provider(provider: Optional[str] = None, model: Optional[str] = None) -> Provider:
|
|
61
|
+
"""Parse provider from provider string or model name."""
|
|
62
|
+
if provider:
|
|
63
|
+
try:
|
|
64
|
+
return Provider(provider.lower())
|
|
65
|
+
except ValueError:
|
|
66
|
+
raise ValueError(f"Unsupported provider: {provider}. Supported providers: {[p.value for p in Provider]}")
|
|
67
|
+
|
|
68
|
+
if model:
|
|
69
|
+
# Infer provider from model name
|
|
70
|
+
if model.startswith("claude"):
|
|
71
|
+
return Provider.ANTHROPIC
|
|
72
|
+
elif model.startswith("gpt"):
|
|
73
|
+
return Provider.OPENAI
|
|
74
|
+
elif model.startswith("gemini"):
|
|
75
|
+
return Provider.GEMINI
|
|
76
|
+
|
|
77
|
+
# Default to Anthropic for backward compatibility
|
|
78
|
+
return Provider.ANTHROPIC
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_default_model(provider: Provider) -> str:
|
|
82
|
+
"""Get the default model for a provider."""
|
|
83
|
+
return DEFAULT_MODELS.get(provider, DEFAULT_MODELS[Provider.ANTHROPIC])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
from .
|
|
1
|
+
from .config import DEFAULT_MODEL, parse_provider, get_default_model
|
|
2
|
+
from .providers import create_message
|
|
3
3
|
|
|
4
4
|
SYSTEM_PROMPT = """You are a senior statistician and data scientist advising a colleague on the right analytical approach for their problem.
|
|
5
5
|
|
|
@@ -29,7 +29,7 @@ ALTERNATIVES
|
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def plan(question: str, data_description: str = None, goal: str = None) -> str:
|
|
32
|
+
def plan(question: str, data_description: str = None, goal: str = None, provider: str = None, model: str = None) -> str:
|
|
33
33
|
"""
|
|
34
34
|
Recommend the right analytical approach for your problem.
|
|
35
35
|
|
|
@@ -38,6 +38,9 @@ def plan(question: str, data_description: str = None, goal: str = None) -> str:
|
|
|
38
38
|
data_description: Optional. A plain text description of your available data.
|
|
39
39
|
goal: Optional. The goal of your analysis (e.g. "causal inference",
|
|
40
40
|
"prediction", "segmentation", "hypothesis testing", "exploration").
|
|
41
|
+
provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
|
|
42
|
+
If not specified, defaults to "anthropic" or infers from model.
|
|
43
|
+
model: Optional. The specific model to use. If not specified, uses the provider's default.
|
|
41
44
|
|
|
42
45
|
Returns:
|
|
43
46
|
A structured analytical plan covering the recommended approach, assumptions,
|
|
@@ -46,7 +49,10 @@ def plan(question: str, data_description: str = None, goal: str = None) -> str:
|
|
|
46
49
|
if not question or not question.strip():
|
|
47
50
|
raise ValueError("Question cannot be empty.")
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
# Parse provider and determine model
|
|
53
|
+
provider_enum = parse_provider(provider, model)
|
|
54
|
+
if model is None:
|
|
55
|
+
model = get_default_model(provider_enum)
|
|
50
56
|
|
|
51
57
|
user_message = f"Question: {question}"
|
|
52
58
|
if data_description:
|
|
@@ -54,17 +60,10 @@ def plan(question: str, data_description: str = None, goal: str = None) -> str:
|
|
|
54
60
|
if goal:
|
|
55
61
|
user_message += f"\n\nGoal: {goal}"
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
"role": "user",
|
|
65
|
-
"content": user_message
|
|
66
|
-
}
|
|
67
|
-
]
|
|
63
|
+
return create_message(
|
|
64
|
+
provider=provider_enum,
|
|
65
|
+
system_prompt=SYSTEM_PROMPT,
|
|
66
|
+
user_message=user_message,
|
|
67
|
+
model=model,
|
|
68
|
+
max_tokens=1024
|
|
68
69
|
)
|
|
69
|
-
|
|
70
|
-
return message.content[0].text
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Provider client factory for Bridgekit multi-provider support."""
|
|
2
|
+
|
|
3
|
+
from typing import Union, Dict, Any, List
|
|
4
|
+
from .config import Provider, require_api_key, get_default_model
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseProviderClient:
|
|
8
|
+
"""Base class for provider clients."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, api_key: str):
|
|
11
|
+
self.api_key = api_key
|
|
12
|
+
|
|
13
|
+
def create_message(self, system_prompt: str, user_message: str, model: str, max_tokens: int = 1024) -> str:
|
|
14
|
+
"""Create a message with the given parameters."""
|
|
15
|
+
raise NotImplementedError("Subclasses must implement create_message")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AnthropicClient(BaseProviderClient):
|
|
19
|
+
"""Anthropic provider client."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, api_key: str):
|
|
22
|
+
super().__init__(api_key)
|
|
23
|
+
import anthropic
|
|
24
|
+
self.client = anthropic.Anthropic(api_key=api_key)
|
|
25
|
+
|
|
26
|
+
def create_message(self, system_prompt: str, user_message: str, model: str, max_tokens: int = 1024) -> str:
|
|
27
|
+
"""Create a message using Anthropic's API."""
|
|
28
|
+
message = self.client.messages.create(
|
|
29
|
+
model=model,
|
|
30
|
+
max_tokens=max_tokens,
|
|
31
|
+
system=system_prompt,
|
|
32
|
+
messages=[{
|
|
33
|
+
"role": "user",
|
|
34
|
+
"content": user_message
|
|
35
|
+
}]
|
|
36
|
+
)
|
|
37
|
+
return message.content[0].text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OpenAIClient(BaseProviderClient):
|
|
41
|
+
"""OpenAI provider client."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, api_key: str):
|
|
44
|
+
super().__init__(api_key)
|
|
45
|
+
import openai
|
|
46
|
+
self.client = openai.OpenAI(api_key=api_key)
|
|
47
|
+
|
|
48
|
+
def create_message(self, system_prompt: str, user_message: str, model: str, max_tokens: int = 1024) -> str:
|
|
49
|
+
"""Create a message using OpenAI's API."""
|
|
50
|
+
messages = [
|
|
51
|
+
{"role": "system", "content": system_prompt},
|
|
52
|
+
{"role": "user", "content": user_message}
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
response = self.client.chat.completions.create(
|
|
56
|
+
model=model,
|
|
57
|
+
messages=messages,
|
|
58
|
+
max_tokens=max_tokens
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return response.choices[0].message.content
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GeminiClient(BaseProviderClient):
|
|
65
|
+
"""Google Gemini provider client."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, api_key: str):
|
|
68
|
+
super().__init__(api_key)
|
|
69
|
+
import google.generativeai as genai
|
|
70
|
+
genai.configure(api_key=api_key)
|
|
71
|
+
|
|
72
|
+
def create_message(self, system_prompt: str, user_message: str, model: str, max_tokens: int = 1024) -> str:
|
|
73
|
+
"""Create a message using Google's Gemini API."""
|
|
74
|
+
import google.generativeai as genai
|
|
75
|
+
|
|
76
|
+
# Configure the model with system instruction
|
|
77
|
+
model_instance = genai.GenerativeModel(
|
|
78
|
+
model_name=model,
|
|
79
|
+
system_instruction=system_prompt,
|
|
80
|
+
generation_config=genai.types.GenerationConfig(
|
|
81
|
+
max_output_tokens=max_tokens,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
response = model_instance.generate_content(user_message)
|
|
86
|
+
return response.text
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def create_client(provider: Provider, model: str = None) -> BaseProviderClient:
|
|
90
|
+
"""Create a client for the specified provider."""
|
|
91
|
+
if model is None:
|
|
92
|
+
model = get_default_model(provider)
|
|
93
|
+
|
|
94
|
+
api_key = require_api_key(provider)
|
|
95
|
+
|
|
96
|
+
if provider == Provider.ANTHROPIC:
|
|
97
|
+
return AnthropicClient(api_key)
|
|
98
|
+
elif provider == Provider.OPENAI:
|
|
99
|
+
return OpenAIClient(api_key)
|
|
100
|
+
elif provider == Provider.GEMINI:
|
|
101
|
+
return GeminiClient(api_key)
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def create_message(provider: Provider, system_prompt: str, user_message: str, model: str = None, max_tokens: int = 1024) -> str:
|
|
107
|
+
"""Create a message using the specified provider."""
|
|
108
|
+
if model is None:
|
|
109
|
+
model = get_default_model(provider)
|
|
110
|
+
|
|
111
|
+
client = create_client(provider, model)
|
|
112
|
+
return client.create_message(system_prompt, user_message, model, max_tokens)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
from .
|
|
1
|
+
from .config import DEFAULT_MODEL, parse_provider, get_default_model
|
|
2
|
+
from .providers import create_message
|
|
3
3
|
|
|
4
4
|
DEFAULT_STAKEHOLDER = "a skeptical senior executive with no tolerance for weak methodology, unsupported claims, or vague business impact"
|
|
5
5
|
|
|
@@ -39,7 +39,7 @@ HARDEST QUESTION TO ANSWER
|
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def redteam(text: str, stakeholder: str = None) -> str:
|
|
42
|
+
def redteam(text: str, stakeholder: str = None, provider: str = None, model: str = None) -> str:
|
|
43
43
|
"""
|
|
44
44
|
Red-team a data science analysis writeup from the perspective of a skeptical stakeholder.
|
|
45
45
|
|
|
@@ -48,6 +48,9 @@ def redteam(text: str, stakeholder: str = None) -> str:
|
|
|
48
48
|
stakeholder: Optional. The skeptical stakeholder role (e.g. "VP of Finance",
|
|
49
49
|
"skeptical board member", "Chief Revenue Officer").
|
|
50
50
|
Defaults to a generic skeptical senior executive.
|
|
51
|
+
provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
|
|
52
|
+
If not specified, defaults to "anthropic" or infers from model.
|
|
53
|
+
model: Optional. The specific model to use. If not specified, uses the provider's default.
|
|
51
54
|
|
|
52
55
|
Returns:
|
|
53
56
|
The 3-5 hardest critiques the stakeholder would make, plus the single
|
|
@@ -56,7 +59,10 @@ def redteam(text: str, stakeholder: str = None) -> str:
|
|
|
56
59
|
if not text or not text.strip():
|
|
57
60
|
raise ValueError("Text cannot be empty.")
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
# Parse provider and determine model
|
|
63
|
+
provider_enum = parse_provider(provider, model)
|
|
64
|
+
if model is None:
|
|
65
|
+
model = get_default_model(provider_enum)
|
|
60
66
|
|
|
61
67
|
stakeholder_label = stakeholder if stakeholder else "Skeptical Senior Executive"
|
|
62
68
|
stakeholder_desc = stakeholder if stakeholder else DEFAULT_STAKEHOLDER
|
|
@@ -66,17 +72,12 @@ def redteam(text: str, stakeholder: str = None) -> str:
|
|
|
66
72
|
stakeholder_label=stakeholder_label
|
|
67
73
|
)
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"content": f"Red-team this analysis writeup:\n\n{text}"
|
|
78
|
-
}
|
|
79
|
-
]
|
|
75
|
+
user_message = f"Red-team this analysis writeup:\n\n{text}"
|
|
76
|
+
|
|
77
|
+
return create_message(
|
|
78
|
+
provider=provider_enum,
|
|
79
|
+
system_prompt=system_prompt,
|
|
80
|
+
user_message=user_message,
|
|
81
|
+
model=model,
|
|
82
|
+
max_tokens=1024
|
|
80
83
|
)
|
|
81
|
-
|
|
82
|
-
return message.content[0].text
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
from .
|
|
1
|
+
from .config import DEFAULT_MODEL, parse_provider, get_default_model
|
|
2
|
+
from .providers import create_message
|
|
3
3
|
|
|
4
4
|
SYSTEM_PROMPT = """You are a senior data scientist reviewing a colleague's analysis writeup.
|
|
5
5
|
You are direct, constructive, and specific. You do not flatter — you help people improve.
|
|
@@ -42,12 +42,15 @@ BOTTOM LINE
|
|
|
42
42
|
[one sentence]
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
def evaluate(text: str) -> str:
|
|
45
|
+
def evaluate(text: str, provider: str = None, model: str = None) -> str:
|
|
46
46
|
"""
|
|
47
47
|
Evaluate a data science analysis writeup and return structured feedback.
|
|
48
48
|
|
|
49
49
|
Args:
|
|
50
50
|
text: Your analysis writeup as a plain string.
|
|
51
|
+
provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
|
|
52
|
+
If not specified, defaults to "anthropic" or infers from model.
|
|
53
|
+
model: Optional. The specific model to use. If not specified, uses the provider's default.
|
|
51
54
|
|
|
52
55
|
Returns:
|
|
53
56
|
Structured feedback across four dimensions.
|
|
@@ -55,20 +58,17 @@ def evaluate(text: str) -> str:
|
|
|
55
58
|
if not text or not text.strip():
|
|
56
59
|
raise ValueError("Text cannot be empty.")
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
]
|
|
61
|
+
# Parse provider and determine model
|
|
62
|
+
provider_enum = parse_provider(provider, model)
|
|
63
|
+
if model is None:
|
|
64
|
+
model = get_default_model(provider_enum)
|
|
65
|
+
|
|
66
|
+
user_message = f"Please review this analysis writeup:\n\n{text}"
|
|
67
|
+
|
|
68
|
+
return create_message(
|
|
69
|
+
provider=provider_enum,
|
|
70
|
+
system_prompt=SYSTEM_PROMPT,
|
|
71
|
+
user_message=user_message,
|
|
72
|
+
model=model,
|
|
73
|
+
max_tokens=1024
|
|
72
74
|
)
|
|
73
|
-
|
|
74
|
-
return message.content[0].text
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
import
|
|
3
|
-
from .
|
|
2
|
+
from .config import DEFAULT_MODEL, parse_provider, get_default_model
|
|
3
|
+
from .providers import create_message
|
|
4
4
|
import chromadb
|
|
5
5
|
from chromadb.utils.embedding_functions import SentenceTransformerEmbeddingFunction
|
|
6
6
|
|
|
@@ -49,7 +49,7 @@ def _chunk(text: str) -> list[str]:
|
|
|
49
49
|
return [c for c in chunks if c.strip()]
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def ask(question: str, source: str = None, text: str = None) -> str:
|
|
52
|
+
def ask(question: str, source: str = None, text: str = None, provider: str = None, model: str = None) -> str:
|
|
53
53
|
"""
|
|
54
54
|
Ask a question across a collection of analysis documents or raw text.
|
|
55
55
|
|
|
@@ -57,6 +57,9 @@ def ask(question: str, source: str = None, text: str = None) -> str:
|
|
|
57
57
|
question: The question to answer.
|
|
58
58
|
source: Path to a folder containing .txt, .md, .pdf, .docx, .pptx, or .ipynb files.
|
|
59
59
|
text: A raw text string to search instead of a folder.
|
|
60
|
+
provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
|
|
61
|
+
If not specified, defaults to "anthropic" or infers from model.
|
|
62
|
+
model: Optional. The specific model to use. If not specified, uses the provider's default.
|
|
60
63
|
|
|
61
64
|
Returns:
|
|
62
65
|
An answer grounded in the provided documents.
|
|
@@ -64,7 +67,10 @@ def ask(question: str, source: str = None, text: str = None) -> str:
|
|
|
64
67
|
if not source and not text:
|
|
65
68
|
raise ValueError("Provide either 'source' (folder path) or 'text'.")
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
# Parse provider and determine model
|
|
71
|
+
provider_enum = parse_provider(provider, model)
|
|
72
|
+
if model is None:
|
|
73
|
+
model = get_default_model(provider_enum)
|
|
68
74
|
|
|
69
75
|
# Collect chunks
|
|
70
76
|
chunks = []
|
|
@@ -99,20 +105,17 @@ def ask(question: str, source: str = None, text: str = None) -> str:
|
|
|
99
105
|
results = collection.query(query_texts=[question], n_results=min(8, len(chunks)))
|
|
100
106
|
context = "\n\n".join(results["documents"][0])
|
|
101
107
|
|
|
102
|
-
# Generate answer with
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
# Generate answer with specified provider
|
|
109
|
+
user_message = f"Context from analysis reports:\n\n{context}\n\nQuestion: {question}"
|
|
110
|
+
|
|
111
|
+
return create_message(
|
|
112
|
+
provider=provider_enum,
|
|
113
|
+
system_prompt=(
|
|
108
114
|
"You are a senior data scientist answering questions based on analysis reports. "
|
|
109
115
|
"Answer only from the provided context. Be specific and cite findings where relevant. "
|
|
110
116
|
"If the context does not contain enough information to answer, say so clearly."
|
|
111
117
|
),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}]
|
|
118
|
+
user_message=user_message,
|
|
119
|
+
model=model,
|
|
120
|
+
max_tokens=1024
|
|
116
121
|
)
|
|
117
|
-
|
|
118
|
-
return message.content[0].text
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bridgekit
|
|
3
|
+
Version: 0.3.6
|
|
4
|
+
Summary: AI tools that make you a better data scientist, not a redundant one.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://usebridgekit.com
|
|
7
|
+
Project-URL: Issues, https://github.com/getbridgekit/bridgekit/issues
|
|
8
|
+
Keywords: data science,AI,analysis,evaluation,anthropic
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: anthropic>=0.20.0
|
|
18
|
+
Requires-Dist: chromadb>=0.4.0
|
|
19
|
+
Requires-Dist: sentence-transformers>=2.0.0
|
|
20
|
+
Requires-Dist: pypdf>=3.0.0
|
|
21
|
+
Requires-Dist: python-docx>=1.0.0
|
|
22
|
+
Requires-Dist: python-pptx>=0.6.0
|
|
23
|
+
Requires-Dist: nbformat>=5.0.0
|
|
24
|
+
Provides-Extra: openai
|
|
25
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
26
|
+
Provides-Extra: gemini
|
|
27
|
+
Requires-Dist: google-generativeai>=0.3.0; extra == "gemini"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-mock>=3.0.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
1
33
|
<p align="center">
|
|
2
34
|
<img src="https://raw.githubusercontent.com/getbridgekit/bridgekit/main/assets/logo.png" alt="Bridgekit" width="200"/>
|
|
3
35
|
</p>
|
|
@@ -35,12 +67,26 @@ pip install bridgekit
|
|
|
35
67
|
!pip install bridgekit
|
|
36
68
|
```
|
|
37
69
|
|
|
38
|
-
Requires an
|
|
70
|
+
Requires an API key for your chosen provider:
|
|
39
71
|
|
|
72
|
+
**Anthropic (default):**
|
|
40
73
|
```bash
|
|
74
|
+
pip install bridgekit
|
|
41
75
|
export ANTHROPIC_API_KEY=your_key_here
|
|
42
76
|
```
|
|
43
77
|
|
|
78
|
+
**OpenAI:**
|
|
79
|
+
```bash
|
|
80
|
+
pip install bridgekit[openai]
|
|
81
|
+
export OPENAI_API_KEY=your_key_here
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Google Gemini:**
|
|
85
|
+
```bash
|
|
86
|
+
pip install bridgekit[gemini]
|
|
87
|
+
export GOOGLE_API_KEY=your_key_here
|
|
88
|
+
```
|
|
89
|
+
|
|
44
90
|
---
|
|
45
91
|
|
|
46
92
|
## Getting Started
|
|
@@ -341,6 +387,45 @@ willing to commit to — and what's your confidence interval on that estimate?"
|
|
|
341
387
|
|
|
342
388
|
---
|
|
343
389
|
|
|
390
|
+
## Multi-Provider Support
|
|
391
|
+
|
|
392
|
+
Bridgekit now supports multiple AI providers so you're not locked into one API. You can use Anthropic, OpenAI, or Google Gemini models with any tool.
|
|
393
|
+
|
|
394
|
+
**Using different providers:**
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
from bridgekit import evaluate, plan, ask, redteam
|
|
398
|
+
|
|
399
|
+
# Use OpenAI (default model: gpt-4o)
|
|
400
|
+
print(evaluate("Your analysis here", provider="openai"))
|
|
401
|
+
|
|
402
|
+
# Use Google Gemini (default model: gemini-1.5-pro)
|
|
403
|
+
print(plan("Your question here", provider="gemini"))
|
|
404
|
+
|
|
405
|
+
# Use specific model
|
|
406
|
+
print(redteam("Your analysis here", model="gpt-4-turbo"))
|
|
407
|
+
print(ask("Your question here", source="reports/", model="claude-3-opus-20240229"))
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Provider auto-detection:**
|
|
411
|
+
Bridgekit automatically detects the provider from model names:
|
|
412
|
+
- Models starting with "claude" → Anthropic
|
|
413
|
+
- Models starting with "gpt" → OpenAI
|
|
414
|
+
- Models starting with "gemini" → Google Gemini
|
|
415
|
+
|
|
416
|
+
**Default models by provider:**
|
|
417
|
+
- Anthropic: `claude-3-5-sonnet-20241022`
|
|
418
|
+
- OpenAI: `gpt-4o`
|
|
419
|
+
- Gemini: `gemini-1.5-pro`
|
|
420
|
+
|
|
421
|
+
All tools support the same `provider` and `model` parameters:
|
|
422
|
+
- `evaluate(text, provider=None, model=None)`
|
|
423
|
+
- `plan(question, provider=None, model=None, ...)`
|
|
424
|
+
- `ask(question, provider=None, model=None, ...)`
|
|
425
|
+
- `redteam(text, provider=None, model=None, ...)`
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
344
429
|
## Why not just use Claude?
|
|
345
430
|
|
|
346
431
|
You could. But you'd need to know what to ask, how to frame it, and what a good answer looks like. Bridgekit has that baked in — it knows you're a data scientist presenting findings, so it asks the right questions automatically. No prompt engineering required. Just paste your work and run it.
|
|
@@ -361,7 +446,7 @@ Bridgekit is a suite, not a one-off. Four tools are live — more are coming:
|
|
|
361
446
|
|
|
362
447
|
- **Stakeholder translator** — turn your technical findings into a narrative a non-technical audience will actually follow
|
|
363
448
|
- **Assumption checker** — state your analytical assumptions, get the ones you missed
|
|
364
|
-
- **
|
|
449
|
+
- **More specialized tools** — focused on specific data science workflows and challenges
|
|
365
450
|
|
|
366
451
|
Each tool is small, focused, and built for the way data scientists actually work.
|
|
367
452
|
|
|
@@ -4,6 +4,7 @@ pyproject.toml
|
|
|
4
4
|
bridgekit/__init__.py
|
|
5
5
|
bridgekit/config.py
|
|
6
6
|
bridgekit/planner.py
|
|
7
|
+
bridgekit/providers.py
|
|
7
8
|
bridgekit/redteam.py
|
|
8
9
|
bridgekit/reviewer.py
|
|
9
10
|
bridgekit/search.py
|
|
@@ -14,5 +15,6 @@ bridgekit.egg-info/requires.txt
|
|
|
14
15
|
bridgekit.egg-info/top_level.txt
|
|
15
16
|
tests/test_config.py
|
|
16
17
|
tests/test_planner.py
|
|
18
|
+
tests/test_providers.py
|
|
17
19
|
tests/test_reviewer.py
|
|
18
20
|
tests/test_search.py
|
|
@@ -7,7 +7,7 @@ include = ["bridgekit*"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "bridgekit"
|
|
10
|
-
version = "0.3.
|
|
10
|
+
version = "0.3.6"
|
|
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"
|
|
@@ -31,6 +31,8 @@ dependencies = [
|
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[project.optional-dependencies]
|
|
34
|
+
openai = ["openai>=1.0.0"]
|
|
35
|
+
gemini = ["google-generativeai>=0.3.0"]
|
|
34
36
|
dev = [
|
|
35
37
|
"pytest>=7.0.0",
|
|
36
38
|
"pytest-mock>=3.0.0",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from bridgekit.config import parse_provider, require_api_key, Provider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestParseProvider:
|
|
9
|
+
"""parse_provider() returns the correct Provider enum."""
|
|
10
|
+
|
|
11
|
+
def test_explicit_anthropic(self):
|
|
12
|
+
assert parse_provider(provider="anthropic") == Provider.ANTHROPIC
|
|
13
|
+
|
|
14
|
+
def test_explicit_openai(self):
|
|
15
|
+
assert parse_provider(provider="openai") == Provider.OPENAI
|
|
16
|
+
|
|
17
|
+
def test_explicit_gemini(self):
|
|
18
|
+
assert parse_provider(provider="gemini") == Provider.GEMINI
|
|
19
|
+
|
|
20
|
+
def test_infer_anthropic_from_model(self):
|
|
21
|
+
assert parse_provider(model="claude-opus-4-6") == Provider.ANTHROPIC
|
|
22
|
+
|
|
23
|
+
def test_infer_openai_from_model(self):
|
|
24
|
+
assert parse_provider(model="gpt-4o") == Provider.OPENAI
|
|
25
|
+
|
|
26
|
+
def test_infer_gemini_from_model(self):
|
|
27
|
+
assert parse_provider(model="gemini-1.5-pro") == Provider.GEMINI
|
|
28
|
+
|
|
29
|
+
def test_defaults_to_anthropic(self):
|
|
30
|
+
assert parse_provider() == Provider.ANTHROPIC
|
|
31
|
+
|
|
32
|
+
def test_raises_on_unsupported_provider(self):
|
|
33
|
+
with pytest.raises(ValueError, match="Unsupported provider"):
|
|
34
|
+
parse_provider(provider="cohere")
|
|
35
|
+
|
|
36
|
+
def test_provider_takes_precedence_over_model(self):
|
|
37
|
+
assert parse_provider(provider="openai", model="claude-opus-4-6") == Provider.OPENAI
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestRequireApiKey:
|
|
41
|
+
"""require_api_key() returns the correct key or raises EnvironmentError."""
|
|
42
|
+
|
|
43
|
+
def test_returns_anthropic_key(self):
|
|
44
|
+
with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "sk-ant-test"}, clear=True):
|
|
45
|
+
assert require_api_key(Provider.ANTHROPIC) == "sk-ant-test"
|
|
46
|
+
|
|
47
|
+
def test_returns_openai_key(self):
|
|
48
|
+
with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-openai-test"}, clear=True):
|
|
49
|
+
assert require_api_key(Provider.OPENAI) == "sk-openai-test"
|
|
50
|
+
|
|
51
|
+
def test_returns_gemini_key(self):
|
|
52
|
+
with patch.dict(os.environ, {"GOOGLE_API_KEY": "gemini-test"}, clear=True):
|
|
53
|
+
assert require_api_key(Provider.GEMINI) == "gemini-test"
|
|
54
|
+
|
|
55
|
+
def test_raises_when_anthropic_key_missing(self):
|
|
56
|
+
env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
|
|
57
|
+
with patch.dict(os.environ, env, clear=True):
|
|
58
|
+
with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
|
|
59
|
+
require_api_key(Provider.ANTHROPIC)
|
|
60
|
+
|
|
61
|
+
def test_raises_when_openai_key_missing(self):
|
|
62
|
+
env = {k: v for k, v in os.environ.items() if k != "OPENAI_API_KEY"}
|
|
63
|
+
with patch.dict(os.environ, env, clear=True):
|
|
64
|
+
with pytest.raises(EnvironmentError, match="OPENAI_API_KEY"):
|
|
65
|
+
require_api_key(Provider.OPENAI)
|
|
66
|
+
|
|
67
|
+
def test_raises_when_gemini_key_missing(self):
|
|
68
|
+
env = {k: v for k, v in os.environ.items() if k != "GOOGLE_API_KEY"}
|
|
69
|
+
with patch.dict(os.environ, env, clear=True):
|
|
70
|
+
with pytest.raises(EnvironmentError, match="GOOGLE_API_KEY"):
|
|
71
|
+
require_api_key(Provider.GEMINI)
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
DEFAULT_MODEL = "claude-opus-4-6"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def require_anthropic_api_key() -> str:
|
|
7
|
-
"""Return the Anthropic API key from the environment, or raise a clear error.
|
|
8
|
-
|
|
9
|
-
Each Bridgekit tool calls this before constructing an Anthropic client so
|
|
10
|
-
users get the same friendly message instead of whatever the SDK surfaces
|
|
11
|
-
when the key is missing.
|
|
12
|
-
"""
|
|
13
|
-
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
14
|
-
if not api_key:
|
|
15
|
-
raise EnvironmentError(
|
|
16
|
-
"ANTHROPIC_API_KEY not found. Set it with: export ANTHROPIC_API_KEY=your_key_here"
|
|
17
|
-
)
|
|
18
|
-
return api_key
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|