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.
Files changed (23) hide show
  1. {bridgekit-0.3.5/bridgekit.egg-info → bridgekit-0.3.6}/PKG-INFO +60 -3
  2. bridgekit-0.3.5/PKG-INFO → bridgekit-0.3.6/README.md +55 -30
  3. bridgekit-0.3.6/bridgekit/config.py +83 -0
  4. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/planner.py +16 -17
  5. bridgekit-0.3.6/bridgekit/providers.py +112 -0
  6. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/redteam.py +18 -17
  7. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/reviewer.py +19 -19
  8. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/search.py +19 -16
  9. bridgekit-0.3.5/README.md → bridgekit-0.3.6/bridgekit.egg-info/PKG-INFO +87 -2
  10. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/SOURCES.txt +2 -0
  11. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/requires.txt +6 -0
  12. {bridgekit-0.3.5 → bridgekit-0.3.6}/pyproject.toml +3 -1
  13. bridgekit-0.3.6/tests/test_providers.py +71 -0
  14. bridgekit-0.3.5/bridgekit/config.py +0 -18
  15. {bridgekit-0.3.5 → bridgekit-0.3.6}/LICENSE +0 -0
  16. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit/__init__.py +0 -0
  17. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/dependency_links.txt +0 -0
  18. {bridgekit-0.3.5 → bridgekit-0.3.6}/bridgekit.egg-info/top_level.txt +0 -0
  19. {bridgekit-0.3.5 → bridgekit-0.3.6}/setup.cfg +0 -0
  20. {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_config.py +0 -0
  21. {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_planner.py +0 -0
  22. {bridgekit-0.3.5 → bridgekit-0.3.6}/tests/test_reviewer.py +0 -0
  23. {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.5
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 Anthropic API key:
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
- - **Multi-model support** — use any LLM provider (OpenAI, Gemini, open source models via OpenRouter) instead of being tied to Anthropic
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 Anthropic API key:
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
- - **Multi-model support** — use any LLM provider (OpenAI, Gemini, open source models via OpenRouter) instead of being tied to Anthropic
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 anthropic
2
- from .config import DEFAULT_MODEL, require_anthropic_api_key
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
- api_key = require_anthropic_api_key()
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
- client = anthropic.Anthropic(api_key=api_key)
58
- message = client.messages.create(
59
- model=DEFAULT_MODEL,
60
- max_tokens=1024,
61
- system=SYSTEM_PROMPT,
62
- messages=[
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 anthropic
2
- from .config import DEFAULT_MODEL, require_anthropic_api_key
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
- api_key = require_anthropic_api_key()
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
- client = anthropic.Anthropic(api_key=api_key)
70
- message = client.messages.create(
71
- model=DEFAULT_MODEL,
72
- max_tokens=1024,
73
- system=system_prompt,
74
- messages=[
75
- {
76
- "role": "user",
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 anthropic
2
- from .config import DEFAULT_MODEL, require_anthropic_api_key
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
- api_key = require_anthropic_api_key()
59
-
60
- client = anthropic.Anthropic(api_key=api_key)
61
-
62
- message = client.messages.create(
63
- model=DEFAULT_MODEL,
64
- max_tokens=1024,
65
- system=SYSTEM_PROMPT,
66
- messages=[
67
- {
68
- "role": "user",
69
- "content": f"Please review this analysis writeup:\n\n{text}"
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 anthropic
3
- from .config import DEFAULT_MODEL, require_anthropic_api_key
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
- api_key = require_anthropic_api_key()
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 Claude
103
- anthropic_client = anthropic.Anthropic(api_key=api_key)
104
- message = anthropic_client.messages.create(
105
- model=DEFAULT_MODEL,
106
- max_tokens=1024,
107
- system=(
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
- messages=[{
113
- "role": "user",
114
- "content": f"Context from analysis reports:\n\n{context}\n\nQuestion: {question}"
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 Anthropic API key:
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
- - **Multi-model support** — use any LLM provider (OpenAI, Gemini, open source models via OpenRouter) instead of being tied to Anthropic
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
@@ -9,3 +9,9 @@ nbformat>=5.0.0
9
9
  [dev]
10
10
  pytest>=7.0.0
11
11
  pytest-mock>=3.0.0
12
+
13
+ [gemini]
14
+ google-generativeai>=0.3.0
15
+
16
+ [openai]
17
+ openai>=1.0.0
@@ -7,7 +7,7 @@ include = ["bridgekit*"]
7
7
 
8
8
  [project]
9
9
  name = "bridgekit"
10
- version = "0.3.5"
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