bridgekit 0.3.6__tar.gz → 0.3.7__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 (25) hide show
  1. {bridgekit-0.3.6 → bridgekit-0.3.7}/PKG-INFO +29 -5
  2. {bridgekit-0.3.6 → bridgekit-0.3.7}/README.md +28 -4
  3. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/__init__.py +1 -1
  4. bridgekit-0.3.7/bridgekit/cli.py +105 -0
  5. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/planner.py +3 -2
  6. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/redteam.py +18 -16
  7. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/reviewer.py +4 -3
  8. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/search.py +17 -13
  9. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit.egg-info/PKG-INFO +29 -5
  10. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit.egg-info/SOURCES.txt +3 -0
  11. bridgekit-0.3.7/bridgekit.egg-info/entry_points.txt +2 -0
  12. {bridgekit-0.3.6 → bridgekit-0.3.7}/pyproject.toml +4 -1
  13. {bridgekit-0.3.6 → bridgekit-0.3.7}/tests/test_planner.py +14 -0
  14. bridgekit-0.3.7/tests/test_redteam.py +155 -0
  15. {bridgekit-0.3.6 → bridgekit-0.3.7}/tests/test_reviewer.py +18 -0
  16. {bridgekit-0.3.6 → bridgekit-0.3.7}/tests/test_search.py +21 -0
  17. {bridgekit-0.3.6 → bridgekit-0.3.7}/LICENSE +0 -0
  18. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/config.py +0 -0
  19. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit/providers.py +0 -0
  20. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit.egg-info/dependency_links.txt +0 -0
  21. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit.egg-info/requires.txt +0 -0
  22. {bridgekit-0.3.6 → bridgekit-0.3.7}/bridgekit.egg-info/top_level.txt +0 -0
  23. {bridgekit-0.3.6 → bridgekit-0.3.7}/setup.cfg +0 -0
  24. {bridgekit-0.3.6 → bridgekit-0.3.7}/tests/test_config.py +0 -0
  25. {bridgekit-0.3.6 → bridgekit-0.3.7}/tests/test_providers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.3.6
3
+ Version: 0.3.7
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
@@ -419,10 +419,34 @@ Bridgekit automatically detects the provider from model names:
419
419
  - Gemini: `gemini-1.5-pro`
420
420
 
421
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, ...)`
422
+ - `evaluate(text, provider=None, model=None, system_prompt=None)`
423
+ - `plan(question, provider=None, model=None, ..., system_prompt=None)`
424
+ - `ask(question, provider=None, model=None, ..., system_prompt=None)`
425
+ - `redteam(text, provider=None, model=None, ..., system_prompt=None)`
426
+
427
+ ---
428
+
429
+ ## Custom System Prompts
430
+
431
+ Every tool accepts an optional `system_prompt` parameter to override the default persona. Use this to adapt the tone or focus to a specific domain without changing anything else.
432
+
433
+ ```python
434
+ from bridgekit import evaluate, plan, ask, redteam
435
+
436
+ # Narrow the reviewer to a specific domain
437
+ print(evaluate("my analysis", system_prompt="You are a skeptical PhD statistician focused only on methodology"))
438
+
439
+ # Tailor the planner to a specific industry
440
+ print(plan("my question", system_prompt="You are a data scientist specializing in healthcare analytics"))
441
+
442
+ # Replace the red team persona entirely
443
+ print(redteam("my analysis", system_prompt="You are a hostile regulator looking for compliance violations"))
444
+
445
+ # Change the answering style for ask
446
+ print(ask("my question", text="...", system_prompt="You are a financial analyst. Answer only in terms of revenue impact."))
447
+ ```
448
+
449
+ When `system_prompt` is not provided, each tool uses its built-in default — existing behavior is unchanged.
426
450
 
427
451
  ---
428
452
 
@@ -387,10 +387,34 @@ Bridgekit automatically detects the provider from model names:
387
387
  - Gemini: `gemini-1.5-pro`
388
388
 
389
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, ...)`
390
+ - `evaluate(text, provider=None, model=None, system_prompt=None)`
391
+ - `plan(question, provider=None, model=None, ..., system_prompt=None)`
392
+ - `ask(question, provider=None, model=None, ..., system_prompt=None)`
393
+ - `redteam(text, provider=None, model=None, ..., system_prompt=None)`
394
+
395
+ ---
396
+
397
+ ## Custom System Prompts
398
+
399
+ Every tool accepts an optional `system_prompt` parameter to override the default persona. Use this to adapt the tone or focus to a specific domain without changing anything else.
400
+
401
+ ```python
402
+ from bridgekit import evaluate, plan, ask, redteam
403
+
404
+ # Narrow the reviewer to a specific domain
405
+ print(evaluate("my analysis", system_prompt="You are a skeptical PhD statistician focused only on methodology"))
406
+
407
+ # Tailor the planner to a specific industry
408
+ print(plan("my question", system_prompt="You are a data scientist specializing in healthcare analytics"))
409
+
410
+ # Replace the red team persona entirely
411
+ print(redteam("my analysis", system_prompt="You are a hostile regulator looking for compliance violations"))
412
+
413
+ # Change the answering style for ask
414
+ print(ask("my question", text="...", system_prompt="You are a financial analyst. Answer only in terms of revenue impact."))
415
+ ```
416
+
417
+ When `system_prompt` is not provided, each tool uses its built-in default — existing behavior is unchanged.
394
418
 
395
419
  ---
396
420
 
@@ -3,5 +3,5 @@ from .search import ask
3
3
  from .planner import plan
4
4
  from .redteam import redteam
5
5
 
6
- __version__ = "0.3.4"
6
+ __version__ = "0.3.7"
7
7
  __all__ = ["evaluate", "ask", "plan", "redteam"]
@@ -0,0 +1,105 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .planner import plan
5
+ from .reviewer import evaluate
6
+ from .redteam import redteam
7
+ from .search import ask
8
+
9
+
10
+ def _add_provider_args(parser: argparse.ArgumentParser) -> None:
11
+ parser.add_argument("--provider", help='AI provider: "anthropic", "openai", or "gemini"')
12
+ parser.add_argument("--model", help="Specific model to use (e.g. claude-opus-4-6, gpt-4o)")
13
+
14
+
15
+ def _cmd_plan(args: argparse.Namespace) -> None:
16
+ result = plan(
17
+ question=args.question,
18
+ data_description=args.data,
19
+ goal=args.goal,
20
+ provider=args.provider,
21
+ model=args.model,
22
+ )
23
+ print(result)
24
+
25
+
26
+ def _cmd_review(args: argparse.Namespace) -> None:
27
+ result = evaluate(
28
+ text=args.text,
29
+ provider=args.provider,
30
+ model=args.model,
31
+ )
32
+ print(result)
33
+
34
+
35
+ def _cmd_redteam(args: argparse.Namespace) -> None:
36
+ result = redteam(
37
+ text=args.text,
38
+ stakeholder=args.stakeholder,
39
+ provider=args.provider,
40
+ model=args.model,
41
+ )
42
+ print(result)
43
+
44
+
45
+ def _cmd_search(args: argparse.Namespace) -> None:
46
+ if not args.source and not args.text:
47
+ print("error: provide --source or --text", file=sys.stderr)
48
+ sys.exit(1)
49
+ result = ask(
50
+ question=args.question,
51
+ source=args.source,
52
+ text=args.text,
53
+ provider=args.provider,
54
+ model=args.model,
55
+ )
56
+ print(result)
57
+
58
+
59
+ def main() -> None:
60
+ parser = argparse.ArgumentParser(
61
+ prog="bridgekit",
62
+ description="AI tools for data scientists",
63
+ )
64
+ sub = parser.add_subparsers(dest="command", metavar="COMMAND")
65
+ sub.required = True
66
+
67
+ # plan
68
+ p_plan = sub.add_parser("plan", help="Recommend the right analytical approach")
69
+ p_plan.add_argument("question", help="The analytical question you want to answer")
70
+ p_plan.add_argument("--data", metavar="DESCRIPTION", help="Description of your available data")
71
+ p_plan.add_argument("--goal", help='Goal of the analysis (e.g. "prediction", "hypothesis testing")')
72
+ _add_provider_args(p_plan)
73
+ p_plan.set_defaults(func=_cmd_plan)
74
+
75
+ # review
76
+ p_review = sub.add_parser("review", help="Evaluate a data science analysis writeup")
77
+ p_review.add_argument("text", help="The analysis text to review")
78
+ _add_provider_args(p_review)
79
+ p_review.set_defaults(func=_cmd_review)
80
+
81
+ # redteam
82
+ p_redteam = sub.add_parser("redteam", help="Red-team an analysis from a skeptical stakeholder")
83
+ p_redteam.add_argument("text", help="The analysis text to red-team")
84
+ p_redteam.add_argument("--stakeholder", help='Stakeholder role (e.g. "VP of Finance")')
85
+ _add_provider_args(p_redteam)
86
+ p_redteam.set_defaults(func=_cmd_redteam)
87
+
88
+ # search
89
+ p_search = sub.add_parser("search", help="Ask a question across documents or text")
90
+ p_search.add_argument("question", help="The question to answer")
91
+ p_search.add_argument("--source", metavar="PATH", help="Folder of documents to search")
92
+ p_search.add_argument("--text", help="Raw text to search instead of a folder")
93
+ _add_provider_args(p_search)
94
+ p_search.set_defaults(func=_cmd_search)
95
+
96
+ args = parser.parse_args()
97
+ try:
98
+ args.func(args)
99
+ except (ValueError, EnvironmentError) as e:
100
+ print(f"error: {e}", file=sys.stderr)
101
+ sys.exit(1)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -29,7 +29,7 @@ ALTERNATIVES
29
29
  """
30
30
 
31
31
 
32
- def plan(question: str, data_description: str = None, goal: str = None, provider: str = None, model: str = None) -> str:
32
+ def plan(question: str, data_description: str = None, goal: str = None, provider: str = None, model: str = None, system_prompt: str = None) -> str:
33
33
  """
34
34
  Recommend the right analytical approach for your problem.
35
35
 
@@ -41,6 +41,7 @@ def plan(question: str, data_description: str = None, goal: str = None, provider
41
41
  provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
42
42
  If not specified, defaults to "anthropic" or infers from model.
43
43
  model: Optional. The specific model to use. If not specified, uses the provider's default.
44
+ system_prompt: Optional. A custom system prompt to override the default planner persona.
44
45
 
45
46
  Returns:
46
47
  A structured analytical plan covering the recommended approach, assumptions,
@@ -62,7 +63,7 @@ def plan(question: str, data_description: str = None, goal: str = None, provider
62
63
 
63
64
  return create_message(
64
65
  provider=provider_enum,
65
- system_prompt=SYSTEM_PROMPT,
66
+ system_prompt=system_prompt or SYSTEM_PROMPT,
66
67
  user_message=user_message,
67
68
  model=model,
68
69
  max_tokens=1024
@@ -39,18 +39,20 @@ HARDEST QUESTION TO ANSWER
39
39
  """
40
40
 
41
41
 
42
- def redteam(text: str, stakeholder: str = None, provider: str = None, model: str = None) -> str:
42
+ def redteam(text: str, stakeholder: str = None, provider: str = None, model: str = None, system_prompt: str = None) -> str:
43
43
  """
44
44
  Red-team a data science analysis writeup from the perspective of a skeptical stakeholder.
45
45
 
46
46
  Args:
47
- text: Your analysis writeup as a plain string.
48
- stakeholder: Optional. The skeptical stakeholder role (e.g. "VP of Finance",
49
- "skeptical board member", "Chief Revenue Officer").
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.
47
+ text: Your analysis writeup as a plain string.
48
+ stakeholder: Optional. The skeptical stakeholder role (e.g. "VP of Finance",
49
+ "skeptical board member", "Chief Revenue Officer").
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.
54
+ system_prompt: Optional. A custom system prompt to fully override the default red team persona.
55
+ When provided, the stakeholder parameter is ignored.
54
56
 
55
57
  Returns:
56
58
  The 3-5 hardest critiques the stakeholder would make, plus the single
@@ -64,16 +66,16 @@ def redteam(text: str, stakeholder: str = None, provider: str = None, model: str
64
66
  if model is None:
65
67
  model = get_default_model(provider_enum)
66
68
 
67
- stakeholder_label = stakeholder if stakeholder else "Skeptical Senior Executive"
68
- stakeholder_desc = stakeholder if stakeholder else DEFAULT_STAKEHOLDER
69
-
70
- system_prompt = SYSTEM_PROMPT_TEMPLATE.format(
71
- stakeholder=stakeholder_desc,
72
- stakeholder_label=stakeholder_label
73
- )
69
+ if system_prompt is None:
70
+ stakeholder_label = stakeholder if stakeholder else "Skeptical Senior Executive"
71
+ stakeholder_desc = stakeholder if stakeholder else DEFAULT_STAKEHOLDER
72
+ system_prompt = SYSTEM_PROMPT_TEMPLATE.format(
73
+ stakeholder=stakeholder_desc,
74
+ stakeholder_label=stakeholder_label
75
+ )
74
76
 
75
77
  user_message = f"Red-team this analysis writeup:\n\n{text}"
76
-
78
+
77
79
  return create_message(
78
80
  provider=provider_enum,
79
81
  system_prompt=system_prompt,
@@ -42,7 +42,7 @@ BOTTOM LINE
42
42
  [one sentence]
43
43
  """
44
44
 
45
- def evaluate(text: str, provider: str = None, model: str = None) -> str:
45
+ def evaluate(text: str, provider: str = None, model: str = None, system_prompt: str = None) -> str:
46
46
  """
47
47
  Evaluate a data science analysis writeup and return structured feedback.
48
48
 
@@ -51,6 +51,7 @@ def evaluate(text: str, provider: str = None, model: str = None) -> str:
51
51
  provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
52
52
  If not specified, defaults to "anthropic" or infers from model.
53
53
  model: Optional. The specific model to use. If not specified, uses the provider's default.
54
+ system_prompt: Optional. A custom system prompt to override the default reviewer persona.
54
55
 
55
56
  Returns:
56
57
  Structured feedback across four dimensions.
@@ -64,10 +65,10 @@ def evaluate(text: str, provider: str = None, model: str = None) -> str:
64
65
  model = get_default_model(provider_enum)
65
66
 
66
67
  user_message = f"Please review this analysis writeup:\n\n{text}"
67
-
68
+
68
69
  return create_message(
69
70
  provider=provider_enum,
70
- system_prompt=SYSTEM_PROMPT,
71
+ system_prompt=system_prompt or SYSTEM_PROMPT,
71
72
  user_message=user_message,
72
73
  model=model,
73
74
  max_tokens=1024
@@ -49,17 +49,25 @@ 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, provider: str = None, model: str = None) -> str:
52
+ DEFAULT_SYSTEM_PROMPT = (
53
+ "You are a senior data scientist answering questions based on analysis reports. "
54
+ "Answer only from the provided context. Be specific and cite findings where relevant. "
55
+ "If the context does not contain enough information to answer, say so clearly."
56
+ )
57
+
58
+
59
+ def ask(question: str, source: str = None, text: str = None, provider: str = None, model: str = None, system_prompt: str = None) -> str:
53
60
  """
54
61
  Ask a question across a collection of analysis documents or raw text.
55
62
 
56
63
  Args:
57
- question: The question to answer.
58
- source: Path to a folder containing .txt, .md, .pdf, .docx, .pptx, or .ipynb files.
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.
64
+ question: The question to answer.
65
+ source: Path to a folder containing .txt, .md, .pdf, .docx, .pptx, or .ipynb files.
66
+ text: A raw text string to search instead of a folder.
67
+ provider: Optional. The AI provider to use ("anthropic", "openai", "gemini").
68
+ If not specified, defaults to "anthropic" or infers from model.
69
+ model: Optional. The specific model to use. If not specified, uses the provider's default.
70
+ system_prompt: Optional. A custom system prompt to override the default answering persona.
63
71
 
64
72
  Returns:
65
73
  An answer grounded in the provided documents.
@@ -107,14 +115,10 @@ def ask(question: str, source: str = None, text: str = None, provider: str = Non
107
115
 
108
116
  # Generate answer with specified provider
109
117
  user_message = f"Context from analysis reports:\n\n{context}\n\nQuestion: {question}"
110
-
118
+
111
119
  return create_message(
112
120
  provider=provider_enum,
113
- system_prompt=(
114
- "You are a senior data scientist answering questions based on analysis reports. "
115
- "Answer only from the provided context. Be specific and cite findings where relevant. "
116
- "If the context does not contain enough information to answer, say so clearly."
117
- ),
121
+ system_prompt=system_prompt or DEFAULT_SYSTEM_PROMPT,
118
122
  user_message=user_message,
119
123
  model=model,
120
124
  max_tokens=1024
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.3.6
3
+ Version: 0.3.7
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
@@ -419,10 +419,34 @@ Bridgekit automatically detects the provider from model names:
419
419
  - Gemini: `gemini-1.5-pro`
420
420
 
421
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, ...)`
422
+ - `evaluate(text, provider=None, model=None, system_prompt=None)`
423
+ - `plan(question, provider=None, model=None, ..., system_prompt=None)`
424
+ - `ask(question, provider=None, model=None, ..., system_prompt=None)`
425
+ - `redteam(text, provider=None, model=None, ..., system_prompt=None)`
426
+
427
+ ---
428
+
429
+ ## Custom System Prompts
430
+
431
+ Every tool accepts an optional `system_prompt` parameter to override the default persona. Use this to adapt the tone or focus to a specific domain without changing anything else.
432
+
433
+ ```python
434
+ from bridgekit import evaluate, plan, ask, redteam
435
+
436
+ # Narrow the reviewer to a specific domain
437
+ print(evaluate("my analysis", system_prompt="You are a skeptical PhD statistician focused only on methodology"))
438
+
439
+ # Tailor the planner to a specific industry
440
+ print(plan("my question", system_prompt="You are a data scientist specializing in healthcare analytics"))
441
+
442
+ # Replace the red team persona entirely
443
+ print(redteam("my analysis", system_prompt="You are a hostile regulator looking for compliance violations"))
444
+
445
+ # Change the answering style for ask
446
+ print(ask("my question", text="...", system_prompt="You are a financial analyst. Answer only in terms of revenue impact."))
447
+ ```
448
+
449
+ When `system_prompt` is not provided, each tool uses its built-in default — existing behavior is unchanged.
426
450
 
427
451
  ---
428
452
 
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  bridgekit/__init__.py
5
+ bridgekit/cli.py
5
6
  bridgekit/config.py
6
7
  bridgekit/planner.py
7
8
  bridgekit/providers.py
@@ -11,10 +12,12 @@ bridgekit/search.py
11
12
  bridgekit.egg-info/PKG-INFO
12
13
  bridgekit.egg-info/SOURCES.txt
13
14
  bridgekit.egg-info/dependency_links.txt
15
+ bridgekit.egg-info/entry_points.txt
14
16
  bridgekit.egg-info/requires.txt
15
17
  bridgekit.egg-info/top_level.txt
16
18
  tests/test_config.py
17
19
  tests/test_planner.py
18
20
  tests/test_providers.py
21
+ tests/test_redteam.py
19
22
  tests/test_reviewer.py
20
23
  tests/test_search.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bridgekit = bridgekit.cli:main
@@ -7,7 +7,7 @@ include = ["bridgekit*"]
7
7
 
8
8
  [project]
9
9
  name = "bridgekit"
10
- version = "0.3.6"
10
+ version = "0.3.7"
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"
@@ -38,6 +38,9 @@ dev = [
38
38
  "pytest-mock>=3.0.0",
39
39
  ]
40
40
 
41
+ [project.scripts]
42
+ bridgekit = "bridgekit.cli:main"
43
+
41
44
  [project.urls]
42
45
  Homepage = "https://usebridgekit.com"
43
46
  Issues = "https://github.com/getbridgekit/bridgekit/issues"
@@ -159,6 +159,20 @@ class TestPlanOptionalParameters:
159
159
 
160
160
  assert isinstance(result, str)
161
161
 
162
+ def test_custom_system_prompt_reaches_api(self):
163
+ custom_prompt = "You are a data scientist specializing in healthcare analytics."
164
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
165
+ with patch("anthropic.Anthropic") as MockAnthropic:
166
+ mock_client = MagicMock()
167
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
168
+ MockAnthropic.return_value = mock_client
169
+
170
+ from bridgekit.planner import plan
171
+ plan("Should I use a t-test or ANOVA?", system_prompt=custom_prompt)
172
+
173
+ call_kwargs = mock_client.messages.create.call_args
174
+ assert call_kwargs.kwargs.get("system") == custom_prompt
175
+
162
176
  def test_all_parameters_included_in_api_call(self):
163
177
  with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
164
178
  with patch("anthropic.Anthropic") as MockAnthropic:
@@ -0,0 +1,155 @@
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
+ content_block = MagicMock()
12
+ content_block.text = text
13
+ message = MagicMock()
14
+ message.content = [content_block]
15
+ return message
16
+
17
+
18
+ FAKE_RESPONSE = (
19
+ "BRIDGEKIT RED TEAM\n"
20
+ "─────────────────────────────────────────\n"
21
+ "STAKEHOLDER: Skeptical Senior Executive\n\n"
22
+ "CRITIQUE 1: Sample Size\n"
23
+ '❯ "How many users was this actually tested on?"\n'
24
+ "WHY IT LANDS: No sample size is mentioned anywhere.\n"
25
+ "TO ADDRESS: Report n for each group with a power calculation.\n\n"
26
+ "CRITIQUE 2: Causation vs Correlation\n"
27
+ '❯ "You\'re assuming the feature caused this lift — prove it."\n'
28
+ "WHY IT LANDS: No control group is described.\n"
29
+ "TO ADDRESS: Show the experimental design with random assignment.\n\n"
30
+ "CRITIQUE 3: Business Impact\n"
31
+ '❯ "What does a 5% lift actually mean in dollars?"\n'
32
+ "WHY IT LANDS: Directional claims are not quantified.\n"
33
+ "TO ADDRESS: Translate the metric into revenue or cost terms.\n\n"
34
+ "─────────────────────────────────────────\n"
35
+ "HARDEST QUESTION TO ANSWER\n"
36
+ "What is the p-value and did you correct for multiple comparisons?"
37
+ )
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Tests
42
+ # ---------------------------------------------------------------------------
43
+
44
+ class TestRedteamReturnsString:
45
+ """redteam() should return a non-empty string."""
46
+
47
+ def test_returns_string(self):
48
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
49
+ with patch("anthropic.Anthropic") as MockAnthropic:
50
+ mock_client = MagicMock()
51
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
52
+ MockAnthropic.return_value = mock_client
53
+
54
+ from bridgekit.redteam import redteam
55
+ result = redteam("We ran an A/B test and saw a 5% lift in conversions.")
56
+
57
+ assert isinstance(result, str)
58
+ assert len(result) > 0
59
+
60
+
61
+ class TestRedteamOutputStructure:
62
+ """redteam() output should contain the required section headers."""
63
+
64
+ def test_output_contains_critique(self):
65
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
66
+ with patch("anthropic.Anthropic") as MockAnthropic:
67
+ mock_client = MagicMock()
68
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
69
+ MockAnthropic.return_value = mock_client
70
+
71
+ from bridgekit.redteam import redteam
72
+ result = redteam("We ran an A/B test and saw a 5% lift in conversions.")
73
+
74
+ assert "CRITIQUE" in result
75
+
76
+ def test_output_contains_hardest_question(self):
77
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
78
+ with patch("anthropic.Anthropic") as MockAnthropic:
79
+ mock_client = MagicMock()
80
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
81
+ MockAnthropic.return_value = mock_client
82
+
83
+ from bridgekit.redteam import redteam
84
+ result = redteam("We ran an A/B test and saw a 5% lift in conversions.")
85
+
86
+ assert "HARDEST QUESTION" in result
87
+
88
+
89
+ class TestRedteamMissingApiKey:
90
+ """redteam() should raise EnvironmentError when ANTHROPIC_API_KEY is absent."""
91
+
92
+ def test_raises_environment_error_when_key_missing(self):
93
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
94
+ with patch.dict(os.environ, env, clear=True):
95
+ from bridgekit.redteam import redteam
96
+ with pytest.raises(EnvironmentError):
97
+ redteam("Some analysis text.")
98
+
99
+ def test_error_message_mentions_key(self):
100
+ env = {k: v for k, v in os.environ.items() if k != "ANTHROPIC_API_KEY"}
101
+ with patch.dict(os.environ, env, clear=True):
102
+ from bridgekit.redteam import redteam
103
+ with pytest.raises(EnvironmentError, match="ANTHROPIC_API_KEY"):
104
+ redteam("Some analysis text.")
105
+
106
+
107
+ class TestRedteamEmptyInput:
108
+ """redteam() should raise ValueError for empty or whitespace-only input."""
109
+
110
+ def test_empty_string_raises_value_error(self):
111
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
112
+ from bridgekit.redteam import redteam
113
+ with pytest.raises(ValueError, match="empty"):
114
+ redteam("")
115
+
116
+ def test_whitespace_only_raises_value_error(self):
117
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
118
+ from bridgekit.redteam import redteam
119
+ with pytest.raises(ValueError, match="empty"):
120
+ redteam(" ")
121
+
122
+
123
+ class TestRedteamStakeholder:
124
+ """redteam() should include a custom stakeholder in the system prompt."""
125
+
126
+ def test_custom_stakeholder_reaches_system_prompt(self):
127
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
128
+ with patch("anthropic.Anthropic") as MockAnthropic:
129
+ mock_client = MagicMock()
130
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
131
+ MockAnthropic.return_value = mock_client
132
+
133
+ from bridgekit.redteam import redteam
134
+ redteam("Some analysis text.", stakeholder="VP of Finance")
135
+
136
+ call_kwargs = mock_client.messages.create.call_args
137
+ assert "VP of Finance" in call_kwargs.kwargs.get("system", "")
138
+
139
+
140
+ class TestRedteamCustomSystemPrompt:
141
+ """redteam() should forward a custom system_prompt to the API, ignoring stakeholder."""
142
+
143
+ def test_custom_system_prompt_reaches_api(self):
144
+ custom_prompt = "You are a hostile regulator looking for compliance violations."
145
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
146
+ with patch("anthropic.Anthropic") as MockAnthropic:
147
+ mock_client = MagicMock()
148
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
149
+ MockAnthropic.return_value = mock_client
150
+
151
+ from bridgekit.redteam import redteam
152
+ redteam("Some analysis text.", system_prompt=custom_prompt)
153
+
154
+ call_kwargs = mock_client.messages.create.call_args
155
+ assert call_kwargs.kwargs.get("system") == custom_prompt
@@ -158,3 +158,21 @@ class TestEvaluateApiCallShape:
158
158
  messages_arg = call_kwargs.kwargs.get("messages") or call_kwargs.args[0]
159
159
  content = str(messages_arg)
160
160
  assert user_text in content
161
+
162
+
163
+ class TestEvaluateCustomSystemPrompt:
164
+ """evaluate() should forward a custom system_prompt to the API."""
165
+
166
+ def test_custom_system_prompt_reaches_api(self):
167
+ custom_prompt = "You are a skeptical PhD statistician focused only on methodology."
168
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
169
+ with patch("anthropic.Anthropic") as MockAnthropic:
170
+ mock_client = MagicMock()
171
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_RESPONSE)
172
+ MockAnthropic.return_value = mock_client
173
+
174
+ from bridgekit.reviewer import evaluate
175
+ evaluate("Some analysis text.", system_prompt=custom_prompt)
176
+
177
+ call_kwargs = mock_client.messages.create.call_args
178
+ assert call_kwargs.kwargs.get("system") == custom_prompt
@@ -222,6 +222,27 @@ class TestAskWithSourceFolder:
222
222
 
223
223
  assert mock_client.messages.create.call_count == 1
224
224
 
225
+ def test_custom_system_prompt_reaches_api(self):
226
+ custom_prompt = "You are a financial analyst. Answer only in terms of revenue impact."
227
+ mock_chromadb, mock_ef = _make_mock_chromadb()
228
+
229
+ with patch.dict(os.environ, {"ANTHROPIC_API_KEY": "test-key"}):
230
+ with patch("anthropic.Anthropic") as MockAnthropic, \
231
+ patch("chromadb.Client", mock_chromadb.Client), \
232
+ patch(
233
+ "chromadb.utils.embedding_functions.SentenceTransformerEmbeddingFunction",
234
+ mock_ef,
235
+ ):
236
+ mock_client = MagicMock()
237
+ mock_client.messages.create.return_value = _make_mock_message(FAKE_ANSWER)
238
+ MockAnthropic.return_value = mock_client
239
+
240
+ from bridgekit.search import ask
241
+ ask("What was revenue?", text="Revenue was $5M.", system_prompt=custom_prompt)
242
+
243
+ call_kwargs = mock_client.messages.create.call_args
244
+ assert call_kwargs.kwargs.get("system") == custom_prompt
245
+
225
246
  def test_source_folder_empty_raises_value_error(self):
226
247
  with tempfile.TemporaryDirectory() as tmpdir:
227
248
  # Folder exists but has no supported files
File without changes
File without changes
File without changes