bridgekit 0.3.8__tar.gz → 0.3.9__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 (28) hide show
  1. {bridgekit-0.3.8 → bridgekit-0.3.9}/PKG-INFO +67 -1
  2. {bridgekit-0.3.8 → bridgekit-0.3.9}/README.md +66 -0
  3. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/__init__.py +3 -2
  4. bridgekit-0.3.9/bridgekit/compare.py +128 -0
  5. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit.egg-info/PKG-INFO +67 -1
  6. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit.egg-info/SOURCES.txt +2 -0
  7. {bridgekit-0.3.8 → bridgekit-0.3.9}/pyproject.toml +1 -1
  8. bridgekit-0.3.9/tests/test_compare.py +275 -0
  9. {bridgekit-0.3.8 → bridgekit-0.3.9}/LICENSE +0 -0
  10. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/cli.py +0 -0
  11. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/config.py +0 -0
  12. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/planner.py +0 -0
  13. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/providers.py +0 -0
  14. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/redteam.py +0 -0
  15. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/reviewer.py +0 -0
  16. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit/search.py +0 -0
  17. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit.egg-info/dependency_links.txt +0 -0
  18. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit.egg-info/entry_points.txt +0 -0
  19. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit.egg-info/requires.txt +0 -0
  20. {bridgekit-0.3.8 → bridgekit-0.3.9}/bridgekit.egg-info/top_level.txt +0 -0
  21. {bridgekit-0.3.8 → bridgekit-0.3.9}/setup.cfg +0 -0
  22. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_cli.py +0 -0
  23. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_config.py +0 -0
  24. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_planner.py +0 -0
  25. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_providers.py +0 -0
  26. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_redteam.py +0 -0
  27. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_reviewer.py +0 -0
  28. {bridgekit-0.3.8 → bridgekit-0.3.9}/tests/test_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.3.8
3
+ Version: 0.3.9
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
@@ -396,6 +396,72 @@ willing to commit to — and what's your confidence interval on that estimate?"
396
396
 
397
397
  ---
398
398
 
399
+ ## Tool #5: Compare
400
+
401
+ Run the same tool through two providers and see both outputs side by side. Useful for evaluating which model works best for your use case — as a one-liner.
402
+
403
+ ```python
404
+ from bridgekit import compare
405
+
406
+ text = """
407
+ I analyzed 90 days of user behavior data to understand what drives subscription
408
+ upgrades. Users who engaged with the reporting feature within their first week
409
+ were 3x more likely to upgrade within 30 days. I recommend we prioritize
410
+ onboarding users to reporting as a growth lever.
411
+ """
412
+
413
+ # Compare evaluate across Anthropic and OpenAI (default)
414
+ print(compare(text, tool="evaluate"))
415
+
416
+ # Compare plan across two providers
417
+ print(compare("Did our onboarding flow reduce churn?", tool="plan"))
418
+
419
+ # Compare redteam with a specific stakeholder
420
+ print(compare(text, tool="redteam", stakeholder="VP of Finance"))
421
+
422
+ # Override models for each provider
423
+ print(compare(text, model_a="claude-haiku-4-5-20251001", model_b="gpt-4-turbo"))
424
+
425
+ # Compare two specific providers
426
+ print(compare(text, providers=["anthropic", "gemini"]))
427
+ ```
428
+
429
+ **Parameters:**
430
+ - `tool` - which tool to run: `"evaluate"`, `"plan"`, or `"redteam"` (defaults to `"evaluate"`)
431
+ - `providers` - list of exactly two providers to compare (defaults to `["anthropic", "openai"]`)
432
+ - `model_a`, `model_b` - optional model overrides for the first and second provider
433
+ - `**kwargs` - additional arguments passed through to the underlying tool (e.g. `max_tokens`, `stakeholder`, `data_description`)
434
+
435
+ **Output:**
436
+ ```
437
+ BRIDGEKIT COMPARE: EVALUATE
438
+ ─────────────────────────────────────────
439
+
440
+ ANTHROPIC claude-opus-4-8
441
+ ─────────────────────────────────────────
442
+ BRIDGEKIT ANALYSIS REVIEW
443
+ ─────────────────────────────────────────
444
+
445
+ 1. CLARITY
446
+ ✅ STRONG — Clean and jargon-free.
447
+
448
+ ...
449
+
450
+ OPENAI gpt-4o
451
+ ─────────────────────────────────────────
452
+ BRIDGEKIT ANALYSIS REVIEW
453
+ ─────────────────────────────────────────
454
+
455
+ 1. CLARITY
456
+ ⚠️ NEEDS WORK — The phrase "engagement feature" needs more context.
457
+
458
+ ...
459
+ ```
460
+
461
+ Both providers are called in parallel, so the total wait time is the slower of the two — not the sum.
462
+
463
+ ---
464
+
399
465
  ## Multi-Provider Support
400
466
 
401
467
  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.
@@ -364,6 +364,72 @@ willing to commit to — and what's your confidence interval on that estimate?"
364
364
 
365
365
  ---
366
366
 
367
+ ## Tool #5: Compare
368
+
369
+ Run the same tool through two providers and see both outputs side by side. Useful for evaluating which model works best for your use case — as a one-liner.
370
+
371
+ ```python
372
+ from bridgekit import compare
373
+
374
+ text = """
375
+ I analyzed 90 days of user behavior data to understand what drives subscription
376
+ upgrades. Users who engaged with the reporting feature within their first week
377
+ were 3x more likely to upgrade within 30 days. I recommend we prioritize
378
+ onboarding users to reporting as a growth lever.
379
+ """
380
+
381
+ # Compare evaluate across Anthropic and OpenAI (default)
382
+ print(compare(text, tool="evaluate"))
383
+
384
+ # Compare plan across two providers
385
+ print(compare("Did our onboarding flow reduce churn?", tool="plan"))
386
+
387
+ # Compare redteam with a specific stakeholder
388
+ print(compare(text, tool="redteam", stakeholder="VP of Finance"))
389
+
390
+ # Override models for each provider
391
+ print(compare(text, model_a="claude-haiku-4-5-20251001", model_b="gpt-4-turbo"))
392
+
393
+ # Compare two specific providers
394
+ print(compare(text, providers=["anthropic", "gemini"]))
395
+ ```
396
+
397
+ **Parameters:**
398
+ - `tool` - which tool to run: `"evaluate"`, `"plan"`, or `"redteam"` (defaults to `"evaluate"`)
399
+ - `providers` - list of exactly two providers to compare (defaults to `["anthropic", "openai"]`)
400
+ - `model_a`, `model_b` - optional model overrides for the first and second provider
401
+ - `**kwargs` - additional arguments passed through to the underlying tool (e.g. `max_tokens`, `stakeholder`, `data_description`)
402
+
403
+ **Output:**
404
+ ```
405
+ BRIDGEKIT COMPARE: EVALUATE
406
+ ─────────────────────────────────────────
407
+
408
+ ANTHROPIC claude-opus-4-8
409
+ ─────────────────────────────────────────
410
+ BRIDGEKIT ANALYSIS REVIEW
411
+ ─────────────────────────────────────────
412
+
413
+ 1. CLARITY
414
+ ✅ STRONG — Clean and jargon-free.
415
+
416
+ ...
417
+
418
+ OPENAI gpt-4o
419
+ ─────────────────────────────────────────
420
+ BRIDGEKIT ANALYSIS REVIEW
421
+ ─────────────────────────────────────────
422
+
423
+ 1. CLARITY
424
+ ⚠️ NEEDS WORK — The phrase "engagement feature" needs more context.
425
+
426
+ ...
427
+ ```
428
+
429
+ Both providers are called in parallel, so the total wait time is the slower of the two — not the sum.
430
+
431
+ ---
432
+
367
433
  ## Multi-Provider Support
368
434
 
369
435
  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.
@@ -2,6 +2,7 @@ from .reviewer import evaluate
2
2
  from .search import ask
3
3
  from .planner import plan
4
4
  from .redteam import redteam
5
+ from .compare import compare
5
6
 
6
- __version__ = "0.3.8"
7
- __all__ = ["evaluate", "ask", "plan", "redteam"]
7
+ __version__ = "0.3.9"
8
+ __all__ = ["evaluate", "ask", "plan", "redteam", "compare"]
@@ -0,0 +1,128 @@
1
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ from .config import parse_provider, get_default_model, Provider
3
+ from .providers import create_message
4
+
5
+ SUPPORTED_TOOLS = ["evaluate", "plan", "redteam"]
6
+
7
+ SYNTHESIS_PROMPT = """You are comparing two AI-generated outputs for the same analysis task.
8
+
9
+ Your job is to write a short, direct summary (4-6 sentences) covering:
10
+ - Where both outputs agreed
11
+ - Where they differed — including any cases where one rated a dimension more harshly than the other
12
+ - Which output gave more specific or actionable feedback, and why
13
+
14
+ Be concrete. Reference specific dimensions or findings. No fluff."""
15
+
16
+
17
+ def _get_tool_fn(tool_name: str):
18
+ if tool_name == "evaluate":
19
+ from .reviewer import evaluate
20
+ return evaluate
21
+ elif tool_name == "plan":
22
+ from .planner import plan
23
+ return plan
24
+ elif tool_name == "redteam":
25
+ from .redteam import redteam
26
+ return redteam
27
+ raise ValueError(f"Unknown tool: {tool_name!r}. Supported tools: {SUPPORTED_TOOLS}")
28
+
29
+
30
+ def _call_tool(tool_name: str, text: str, provider: str, model, kwargs: dict) -> str:
31
+ fn = _get_tool_fn(tool_name)
32
+ if tool_name == "plan":
33
+ return fn(question=text, provider=provider, model=model, **kwargs)
34
+ return fn(text=text, provider=provider, model=model, **kwargs)
35
+
36
+
37
+ def _synthesize(results: list) -> str:
38
+ (provider_a, model_a, output_a), (provider_b, model_b, output_b) = results
39
+ user_message = (
40
+ f"OUTPUT 1 ({provider_a.upper()} / {model_a}):\n{output_a}\n\n"
41
+ f"OUTPUT 2 ({provider_b.upper()} / {model_b}):\n{output_b}"
42
+ )
43
+ return create_message(
44
+ provider=Provider.ANTHROPIC,
45
+ system_prompt=SYNTHESIS_PROMPT,
46
+ user_message=user_message,
47
+ max_tokens=512,
48
+ )
49
+
50
+
51
+ def _format_output(tool: str, summary: str, results: list) -> str:
52
+ divider = "─" * 41
53
+ lines = [
54
+ f"BRIDGEKIT COMPARE: {tool.upper()}",
55
+ divider,
56
+ "",
57
+ "SUMMARY",
58
+ divider,
59
+ summary,
60
+ "",
61
+ "",
62
+ ]
63
+ for i, (provider_name, model, output) in enumerate(results):
64
+ lines.append(f"{provider_name.upper()} {model}")
65
+ lines.append(divider)
66
+ lines.append(output)
67
+ if i < len(results) - 1:
68
+ lines.append("")
69
+ lines.append("")
70
+ return "\n".join(lines)
71
+
72
+
73
+ def compare(
74
+ text: str,
75
+ tool: str = "evaluate",
76
+ providers: list = None,
77
+ model_a: str = None,
78
+ model_b: str = None,
79
+ **kwargs
80
+ ) -> str:
81
+ """
82
+ Run the same tool through two providers and return both outputs with a summary.
83
+
84
+ Args:
85
+ text: The input text or question to analyze.
86
+ tool: Which tool to run: "evaluate", "plan", or "redteam". Defaults to "evaluate".
87
+ providers: List of exactly two providers to compare. Defaults to ["anthropic", "openai"].
88
+ model_a: Optional model override for the first provider.
89
+ model_b: Optional model override for the second provider.
90
+ **kwargs: Additional arguments passed through to the underlying tool
91
+ (e.g. max_tokens, stakeholder for redteam, data_description/goal for plan).
92
+
93
+ Returns:
94
+ A summary of key differences followed by both full outputs with provider headers.
95
+ """
96
+ if not text or not text.strip():
97
+ raise ValueError("Text cannot be empty.")
98
+
99
+ if tool not in SUPPORTED_TOOLS:
100
+ raise ValueError(f"Unknown tool: {tool!r}. Supported tools: {SUPPORTED_TOOLS}")
101
+
102
+ if providers is None:
103
+ providers = ["anthropic", "openai"]
104
+
105
+ if len(providers) != 2:
106
+ raise ValueError(f"providers must contain exactly 2 providers, got {len(providers)}.")
107
+
108
+ models = [model_a, model_b]
109
+ resolved_models = []
110
+ for provider, model in zip(providers, models):
111
+ provider_enum = parse_provider(provider)
112
+ resolved_models.append(model if model else get_default_model(provider_enum))
113
+
114
+ results_map = {}
115
+
116
+ def run_one(idx):
117
+ output = _call_tool(tool, text, providers[idx], models[idx], kwargs)
118
+ return idx, providers[idx], resolved_models[idx], output
119
+
120
+ with ThreadPoolExecutor(max_workers=2) as executor:
121
+ futures = {executor.submit(run_one, i): i for i in range(2)}
122
+ for future in as_completed(futures):
123
+ idx, provider, model, output = future.result()
124
+ results_map[idx] = (provider, model, output)
125
+
126
+ results = [results_map[0], results_map[1]]
127
+ summary = _synthesize(results)
128
+ return _format_output(tool, summary, results)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bridgekit
3
- Version: 0.3.8
3
+ Version: 0.3.9
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
@@ -396,6 +396,72 @@ willing to commit to — and what's your confidence interval on that estimate?"
396
396
 
397
397
  ---
398
398
 
399
+ ## Tool #5: Compare
400
+
401
+ Run the same tool through two providers and see both outputs side by side. Useful for evaluating which model works best for your use case — as a one-liner.
402
+
403
+ ```python
404
+ from bridgekit import compare
405
+
406
+ text = """
407
+ I analyzed 90 days of user behavior data to understand what drives subscription
408
+ upgrades. Users who engaged with the reporting feature within their first week
409
+ were 3x more likely to upgrade within 30 days. I recommend we prioritize
410
+ onboarding users to reporting as a growth lever.
411
+ """
412
+
413
+ # Compare evaluate across Anthropic and OpenAI (default)
414
+ print(compare(text, tool="evaluate"))
415
+
416
+ # Compare plan across two providers
417
+ print(compare("Did our onboarding flow reduce churn?", tool="plan"))
418
+
419
+ # Compare redteam with a specific stakeholder
420
+ print(compare(text, tool="redteam", stakeholder="VP of Finance"))
421
+
422
+ # Override models for each provider
423
+ print(compare(text, model_a="claude-haiku-4-5-20251001", model_b="gpt-4-turbo"))
424
+
425
+ # Compare two specific providers
426
+ print(compare(text, providers=["anthropic", "gemini"]))
427
+ ```
428
+
429
+ **Parameters:**
430
+ - `tool` - which tool to run: `"evaluate"`, `"plan"`, or `"redteam"` (defaults to `"evaluate"`)
431
+ - `providers` - list of exactly two providers to compare (defaults to `["anthropic", "openai"]`)
432
+ - `model_a`, `model_b` - optional model overrides for the first and second provider
433
+ - `**kwargs` - additional arguments passed through to the underlying tool (e.g. `max_tokens`, `stakeholder`, `data_description`)
434
+
435
+ **Output:**
436
+ ```
437
+ BRIDGEKIT COMPARE: EVALUATE
438
+ ─────────────────────────────────────────
439
+
440
+ ANTHROPIC claude-opus-4-8
441
+ ─────────────────────────────────────────
442
+ BRIDGEKIT ANALYSIS REVIEW
443
+ ─────────────────────────────────────────
444
+
445
+ 1. CLARITY
446
+ ✅ STRONG — Clean and jargon-free.
447
+
448
+ ...
449
+
450
+ OPENAI gpt-4o
451
+ ─────────────────────────────────────────
452
+ BRIDGEKIT ANALYSIS REVIEW
453
+ ─────────────────────────────────────────
454
+
455
+ 1. CLARITY
456
+ ⚠️ NEEDS WORK — The phrase "engagement feature" needs more context.
457
+
458
+ ...
459
+ ```
460
+
461
+ Both providers are called in parallel, so the total wait time is the slower of the two — not the sum.
462
+
463
+ ---
464
+
399
465
  ## Multi-Provider Support
400
466
 
401
467
  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.
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  bridgekit/__init__.py
5
5
  bridgekit/cli.py
6
+ bridgekit/compare.py
6
7
  bridgekit/config.py
7
8
  bridgekit/planner.py
8
9
  bridgekit/providers.py
@@ -16,6 +17,7 @@ bridgekit.egg-info/entry_points.txt
16
17
  bridgekit.egg-info/requires.txt
17
18
  bridgekit.egg-info/top_level.txt
18
19
  tests/test_cli.py
20
+ tests/test_compare.py
19
21
  tests/test_config.py
20
22
  tests/test_planner.py
21
23
  tests/test_providers.py
@@ -7,7 +7,7 @@ include = ["bridgekit*"]
7
7
 
8
8
  [project]
9
9
  name = "bridgekit"
10
- version = "0.3.8"
10
+ version = "0.3.9"
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"
@@ -0,0 +1,275 @@
1
+ import os
2
+ import pytest
3
+ from unittest.mock import patch
4
+
5
+
6
+ # ---------------------------------------------------------------------------
7
+ # Helpers
8
+ # ---------------------------------------------------------------------------
9
+
10
+ FAKE_ANTHROPIC = (
11
+ "BRIDGEKIT ANALYSIS REVIEW\n"
12
+ "─────────────────────────────────────────\n\n"
13
+ "1. CLARITY\n"
14
+ "✅ STRONG — Clear and jargon-free.\n\n"
15
+ "─────────────────────────────────────────\n"
16
+ "BOTTOM LINE\n"
17
+ "Add quantified business impact."
18
+ )
19
+
20
+ FAKE_OPENAI = (
21
+ "BRIDGEKIT ANALYSIS REVIEW\n"
22
+ "─────────────────────────────────────────\n\n"
23
+ "1. CLARITY\n"
24
+ "⚠️ NEEDS WORK — Too much jargon.\n\n"
25
+ "─────────────────────────────────────────\n"
26
+ "BOTTOM LINE\n"
27
+ "Define your methodology more clearly."
28
+ )
29
+
30
+ FAKE_SUMMARY = "Both agreed on Clarity. Anthropic was harsher on Statistical Rigor."
31
+
32
+ DEFAULT_RESPONSES = {
33
+ "anthropic": FAKE_ANTHROPIC,
34
+ "openai": FAKE_OPENAI,
35
+ }
36
+
37
+
38
+ def _make_call_tool_side_effect(responses: dict):
39
+ def side_effect(tool_name, text, provider, model, kwargs):
40
+ return responses.get(provider, "fallback output")
41
+ return side_effect
42
+
43
+
44
+ def _patches(responses=None):
45
+ """Context manager stacking _call_tool and _synthesize patches."""
46
+ if responses is None:
47
+ responses = DEFAULT_RESPONSES
48
+
49
+ class _Ctx:
50
+ def __enter__(self):
51
+ self._p1 = patch("bridgekit.compare._call_tool", side_effect=_make_call_tool_side_effect(responses))
52
+ self._p2 = patch("bridgekit.compare._synthesize", return_value=FAKE_SUMMARY)
53
+ self._p1.__enter__()
54
+ self._p2.__enter__()
55
+ return self
56
+
57
+ def __exit__(self, *args):
58
+ self._p2.__exit__(*args)
59
+ self._p1.__exit__(*args)
60
+
61
+ return _Ctx()
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Tests
66
+ # ---------------------------------------------------------------------------
67
+
68
+ class TestCompareReturnsString:
69
+ def test_returns_string(self):
70
+ with _patches():
71
+ from bridgekit.compare import compare
72
+ result = compare("Some analysis.")
73
+ assert isinstance(result, str)
74
+
75
+ def test_returns_non_empty_string(self):
76
+ with _patches():
77
+ from bridgekit.compare import compare
78
+ result = compare("Some analysis.")
79
+ assert len(result) > 0
80
+
81
+
82
+ class TestCompareOutputStructure:
83
+ def test_output_contains_tool_header(self):
84
+ with _patches():
85
+ from bridgekit.compare import compare
86
+ result = compare("Some analysis.")
87
+ assert "BRIDGEKIT COMPARE: EVALUATE" in result
88
+
89
+ def test_output_contains_summary_section(self):
90
+ with _patches():
91
+ from bridgekit.compare import compare
92
+ result = compare("Some analysis.")
93
+ assert "SUMMARY" in result
94
+ assert FAKE_SUMMARY in result
95
+
96
+ def test_summary_appears_before_provider_outputs(self):
97
+ with _patches():
98
+ from bridgekit.compare import compare
99
+ result = compare("Some analysis.")
100
+ assert result.index("SUMMARY") < result.index(FAKE_ANTHROPIC)
101
+ assert result.index("SUMMARY") < result.index(FAKE_OPENAI)
102
+
103
+ def test_output_contains_both_provider_labels(self):
104
+ with _patches():
105
+ from bridgekit.compare import compare
106
+ result = compare("Some analysis.")
107
+ assert "ANTHROPIC" in result
108
+ assert "OPENAI" in result
109
+
110
+ def test_output_contains_both_responses(self):
111
+ with _patches():
112
+ from bridgekit.compare import compare
113
+ result = compare("Some analysis.")
114
+ assert FAKE_ANTHROPIC in result
115
+ assert FAKE_OPENAI in result
116
+
117
+ def test_anthropic_appears_before_openai(self):
118
+ with _patches():
119
+ from bridgekit.compare import compare
120
+ result = compare("Some analysis.")
121
+ assert result.index(FAKE_ANTHROPIC) < result.index(FAKE_OPENAI)
122
+
123
+ def test_plan_tool_header(self):
124
+ with _patches():
125
+ from bridgekit.compare import compare
126
+ result = compare("What caused churn?", tool="plan")
127
+ assert "BRIDGEKIT COMPARE: PLAN" in result
128
+
129
+ def test_redteam_tool_header(self):
130
+ with _patches():
131
+ from bridgekit.compare import compare
132
+ result = compare("Some analysis.", tool="redteam")
133
+ assert "BRIDGEKIT COMPARE: REDTEAM" in result
134
+
135
+
136
+ class TestCompareValidation:
137
+ def test_empty_text_raises_value_error(self):
138
+ from bridgekit.compare import compare
139
+ with pytest.raises(ValueError, match="empty"):
140
+ compare("")
141
+
142
+ def test_whitespace_text_raises_value_error(self):
143
+ from bridgekit.compare import compare
144
+ with pytest.raises(ValueError, match="empty"):
145
+ compare(" ")
146
+
147
+ def test_invalid_tool_raises_value_error(self):
148
+ from bridgekit.compare import compare
149
+ with pytest.raises(ValueError, match="Unknown tool"):
150
+ compare("Some analysis.", tool="summarize")
151
+
152
+ def test_too_few_providers_raises_value_error(self):
153
+ from bridgekit.compare import compare
154
+ with pytest.raises(ValueError, match="exactly 2"):
155
+ compare("Some analysis.", providers=["anthropic"])
156
+
157
+ def test_too_many_providers_raises_value_error(self):
158
+ from bridgekit.compare import compare
159
+ with pytest.raises(ValueError, match="exactly 2"):
160
+ compare("Some analysis.", providers=["anthropic", "openai", "gemini"])
161
+
162
+
163
+ class TestCompareModelOverrides:
164
+ def test_model_a_appears_in_output(self):
165
+ with _patches():
166
+ from bridgekit.compare import compare
167
+ result = compare("Some analysis.", model_a="claude-haiku-4-5-20251001")
168
+ assert "claude-haiku-4-5-20251001" in result
169
+
170
+ def test_model_b_appears_in_output(self):
171
+ with _patches():
172
+ from bridgekit.compare import compare
173
+ result = compare("Some analysis.", model_b="gpt-4-turbo")
174
+ assert "gpt-4-turbo" in result
175
+
176
+
177
+ class TestCompareDefaultProviders:
178
+ def test_defaults_to_anthropic_and_openai(self):
179
+ with _patches():
180
+ from bridgekit.compare import compare
181
+ result = compare("Some analysis.")
182
+ assert "ANTHROPIC" in result
183
+ assert "OPENAI" in result
184
+
185
+ def test_default_models_shown(self):
186
+ with _patches():
187
+ from bridgekit.compare import compare
188
+ result = compare("Some analysis.")
189
+ assert "claude-opus-4-8" in result
190
+ assert "gpt-4o" in result
191
+
192
+
193
+ class TestCompareSynthesis:
194
+ def test_synthesize_called_once(self):
195
+ with patch("bridgekit.compare._call_tool", side_effect=_make_call_tool_side_effect(DEFAULT_RESPONSES)):
196
+ with patch("bridgekit.compare._synthesize", return_value=FAKE_SUMMARY) as mock_syn:
197
+ from bridgekit.compare import compare
198
+ compare("Some analysis.")
199
+ mock_syn.assert_called_once()
200
+
201
+ def test_synthesize_receives_both_outputs(self):
202
+ captured = []
203
+
204
+ def capture_synth(results):
205
+ captured.extend(results)
206
+ return FAKE_SUMMARY
207
+
208
+ with patch("bridgekit.compare._call_tool", side_effect=_make_call_tool_side_effect(DEFAULT_RESPONSES)):
209
+ with patch("bridgekit.compare._synthesize", side_effect=capture_synth):
210
+ from bridgekit.compare import compare
211
+ compare("Some analysis.")
212
+
213
+ providers_seen = {r[0] for r in captured}
214
+ assert "anthropic" in providers_seen
215
+ assert "openai" in providers_seen
216
+
217
+
218
+ class TestCompareApiCallShape:
219
+ def test_both_providers_called(self):
220
+ calls = []
221
+
222
+ def capture(tool_name, text, provider, model, kwargs):
223
+ calls.append(provider)
224
+ return "output"
225
+
226
+ with patch("bridgekit.compare._call_tool", side_effect=capture):
227
+ with patch("bridgekit.compare._synthesize", return_value=FAKE_SUMMARY):
228
+ from bridgekit.compare import compare
229
+ compare("Some analysis.")
230
+
231
+ assert sorted(calls) == ["anthropic", "openai"]
232
+
233
+ def test_user_text_passed_to_both_calls(self):
234
+ user_text = "Our conversion rate improved after the campaign."
235
+ seen_texts = []
236
+
237
+ def capture(tool_name, text, provider, model, kwargs):
238
+ seen_texts.append(text)
239
+ return "output"
240
+
241
+ with patch("bridgekit.compare._call_tool", side_effect=capture):
242
+ with patch("bridgekit.compare._synthesize", return_value=FAKE_SUMMARY):
243
+ from bridgekit.compare import compare
244
+ compare(user_text)
245
+
246
+ assert len(seen_texts) == 2
247
+ assert all(t == user_text for t in seen_texts)
248
+
249
+ def test_tool_name_passed_to_both_calls(self):
250
+ tool_names = []
251
+
252
+ def capture(tool_name, text, provider, model, kwargs):
253
+ tool_names.append(tool_name)
254
+ return "output"
255
+
256
+ with patch("bridgekit.compare._call_tool", side_effect=capture):
257
+ with patch("bridgekit.compare._synthesize", return_value=FAKE_SUMMARY):
258
+ from bridgekit.compare import compare
259
+ compare("Some analysis.", tool="redteam")
260
+
261
+ assert all(n == "redteam" for n in tool_names)
262
+
263
+ def test_kwargs_forwarded_to_tool(self):
264
+ received_kwargs = []
265
+
266
+ def capture(tool_name, text, provider, model, kwargs):
267
+ received_kwargs.append(kwargs)
268
+ return "output"
269
+
270
+ with patch("bridgekit.compare._call_tool", side_effect=capture):
271
+ with patch("bridgekit.compare._synthesize", return_value=FAKE_SUMMARY):
272
+ from bridgekit.compare import compare
273
+ compare("Some analysis.", max_tokens=2048)
274
+
275
+ assert all(kw.get("max_tokens") == 2048 for kw in received_kwargs)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes