sequa 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sequa-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,132 @@
1
+ Metadata-Version: 2.3
2
+ Name: sequa
3
+ Version: 0.0.1
4
+ Summary: Add your description here
5
+ Author: thetechnoadvisor
6
+ Author-email: thetechnoadvisor <thetechnoadvisor@gmail.com>
7
+ Requires-Dist: python-dotenv>=1.0.0
8
+ Requires-Dist: langchain-groq>=0.3.0
9
+ Requires-Python: >=3.12
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Sequa 📼
13
+
14
+ Sequa is snapshot testing for LLM applications. Record once, replay forever.
15
+
16
+ ---
17
+
18
+ ## The Magic
19
+
20
+ ### Before
21
+ ```python
22
+ from langchain_groq import ChatGroq
23
+
24
+ model = ChatGroq(model_name="llama-3.1-8b-instant")
25
+ response = model.invoke("Write a 3-word slogan for gravity.")
26
+ # ⏱️ Time taken: 2.3 seconds
27
+ ```
28
+
29
+ ### After
30
+ ```python
31
+ from langchain_groq import ChatGroq
32
+ from sequa import cassette
33
+
34
+ model = ChatGroq(model_name="llama-3.1-8b-instant")
35
+
36
+ with cassette("tests/cassettes"):
37
+ response = model.invoke("Write a 3-word slogan for gravity.")
38
+ # ⏱️ First run: 2.3 seconds (recorded to tests/cassettes/)
39
+ # ⏱️ Second run: 12 ms (replayed locally!)
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Features
45
+
46
+ - **Record once, replay forever**: Speed up integration test suites from minutes to milliseconds.
47
+ - **Multiple Execution Modes**: Support `replay`, `record`, `auto`, and `live` modes.
48
+ - **Robust Key Sorting & Hashing**: Recursively sorts request inputs to generate deterministic hashes.
49
+ - **Custom Ignored Fields**: Easily ignore dynamic/unstable fields (e.g. `temperature`, `max_tokens`).
50
+ - **Custom Normalizers**: Redact, replace, or clean requests prior to hashing.
51
+ - **CLI Utilities**: Inspect, format, and calculate statistics of stored cassettes.
52
+
53
+ ---
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ uv pip install -e .
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Configuration & Advanced API
64
+
65
+ ### 1. Execution Modes
66
+
67
+ Control Sequa behavior via the `mode` parameter:
68
+
69
+ ```python
70
+ with cassette("tests/cassettes", mode="replay"):
71
+ # Will raise CassetteNotFoundError if no matching cassette is found.
72
+ # Guaranteed to make zero external network requests.
73
+ ```
74
+
75
+ - `auto` (Default): Replays if a matching cassette exists, otherwise calls the live API and records it.
76
+ - `record`: Always calls the live API and records/overwrites the cassette.
77
+ - `replay`: Never calls the live API. Raises `CassetteNotFoundError` on cache misses.
78
+ - `live`: Direct pass-through to the live API, bypassing cassettes entirely.
79
+
80
+ ### 2. Ignore Fields
81
+
82
+ Strip request parameters before generating hashes:
83
+
84
+ ```python
85
+ with cassette("tests/cassettes", ignore_fields=["temperature", "max_tokens"]):
86
+ # These two calls generate the exact same hash and match the same cassette:
87
+ model.invoke("hello", temperature=0.2)
88
+ model.invoke("hello", temperature=0.9)
89
+ ```
90
+
91
+ ### 3. Custom Normalizers
92
+
93
+ For complex normalization or content redaction:
94
+
95
+ ```python
96
+ def redact_dates(request_dict):
97
+ # Redact dynamic inputs or strip timestamps
98
+ return request_dict
99
+
100
+ with cassette("tests/cassettes", normalizer=redact_dates):
101
+ model.invoke(...)
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Command Line Interface (CLI)
107
+
108
+ Sequa comes with a CLI tool to manage your cassettes.
109
+
110
+ ### Stats
111
+ Show the number of cassettes, total size on disk, and estimated API latency saved:
112
+ ```bash
113
+ sequa stats --path ./tests/cassettes
114
+ ```
115
+
116
+ ### Inspect
117
+ List all stored cassettes, their model, provider, and when they were recorded:
118
+ ```bash
119
+ sequa inspect --path ./tests/cassettes
120
+ ```
121
+
122
+ ### Clean
123
+ Clean dynamic fields (`latency`, `created_at`) from cassettes to prevent noisy git diffs:
124
+ ```bash
125
+ sequa clean --path ./tests/cassettes --remove-latency --remove-timestamps
126
+ ```
127
+
128
+ ---
129
+
130
+ ## License
131
+
132
+ MIT License.
sequa-0.0.1/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # Sequa 📼
2
+
3
+ Sequa is snapshot testing for LLM applications. Record once, replay forever.
4
+
5
+ ---
6
+
7
+ ## The Magic
8
+
9
+ ### Before
10
+ ```python
11
+ from langchain_groq import ChatGroq
12
+
13
+ model = ChatGroq(model_name="llama-3.1-8b-instant")
14
+ response = model.invoke("Write a 3-word slogan for gravity.")
15
+ # ⏱️ Time taken: 2.3 seconds
16
+ ```
17
+
18
+ ### After
19
+ ```python
20
+ from langchain_groq import ChatGroq
21
+ from sequa import cassette
22
+
23
+ model = ChatGroq(model_name="llama-3.1-8b-instant")
24
+
25
+ with cassette("tests/cassettes"):
26
+ response = model.invoke("Write a 3-word slogan for gravity.")
27
+ # ⏱️ First run: 2.3 seconds (recorded to tests/cassettes/)
28
+ # ⏱️ Second run: 12 ms (replayed locally!)
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ - **Record once, replay forever**: Speed up integration test suites from minutes to milliseconds.
36
+ - **Multiple Execution Modes**: Support `replay`, `record`, `auto`, and `live` modes.
37
+ - **Robust Key Sorting & Hashing**: Recursively sorts request inputs to generate deterministic hashes.
38
+ - **Custom Ignored Fields**: Easily ignore dynamic/unstable fields (e.g. `temperature`, `max_tokens`).
39
+ - **Custom Normalizers**: Redact, replace, or clean requests prior to hashing.
40
+ - **CLI Utilities**: Inspect, format, and calculate statistics of stored cassettes.
41
+
42
+ ---
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ uv pip install -e .
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Configuration & Advanced API
53
+
54
+ ### 1. Execution Modes
55
+
56
+ Control Sequa behavior via the `mode` parameter:
57
+
58
+ ```python
59
+ with cassette("tests/cassettes", mode="replay"):
60
+ # Will raise CassetteNotFoundError if no matching cassette is found.
61
+ # Guaranteed to make zero external network requests.
62
+ ```
63
+
64
+ - `auto` (Default): Replays if a matching cassette exists, otherwise calls the live API and records it.
65
+ - `record`: Always calls the live API and records/overwrites the cassette.
66
+ - `replay`: Never calls the live API. Raises `CassetteNotFoundError` on cache misses.
67
+ - `live`: Direct pass-through to the live API, bypassing cassettes entirely.
68
+
69
+ ### 2. Ignore Fields
70
+
71
+ Strip request parameters before generating hashes:
72
+
73
+ ```python
74
+ with cassette("tests/cassettes", ignore_fields=["temperature", "max_tokens"]):
75
+ # These two calls generate the exact same hash and match the same cassette:
76
+ model.invoke("hello", temperature=0.2)
77
+ model.invoke("hello", temperature=0.9)
78
+ ```
79
+
80
+ ### 3. Custom Normalizers
81
+
82
+ For complex normalization or content redaction:
83
+
84
+ ```python
85
+ def redact_dates(request_dict):
86
+ # Redact dynamic inputs or strip timestamps
87
+ return request_dict
88
+
89
+ with cassette("tests/cassettes", normalizer=redact_dates):
90
+ model.invoke(...)
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Command Line Interface (CLI)
96
+
97
+ Sequa comes with a CLI tool to manage your cassettes.
98
+
99
+ ### Stats
100
+ Show the number of cassettes, total size on disk, and estimated API latency saved:
101
+ ```bash
102
+ sequa stats --path ./tests/cassettes
103
+ ```
104
+
105
+ ### Inspect
106
+ List all stored cassettes, their model, provider, and when they were recorded:
107
+ ```bash
108
+ sequa inspect --path ./tests/cassettes
109
+ ```
110
+
111
+ ### Clean
112
+ Clean dynamic fields (`latency`, `created_at`) from cassettes to prevent noisy git diffs:
113
+ ```bash
114
+ sequa clean --path ./tests/cassettes --remove-latency --remove-timestamps
115
+ ```
116
+
117
+ ---
118
+
119
+ ## License
120
+
121
+ MIT License.
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "sequa"
3
+ version = "0.0.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "thetechnoadvisor", email = "thetechnoadvisor@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "python-dotenv>=1.0.0",
12
+ "langchain-groq>=0.3.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ sequa = "sequa.cli.main:main"
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.9.28,<0.10.0"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from sequa!"
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import threading
5
+ from functools import wraps
6
+ from typing import Any, Callable, TypeVar, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from sequa.recorder import RecorderEngine
10
+
11
+ F = TypeVar("F", bound=Callable[..., Any])
12
+
13
+ _local = threading.local()
14
+
15
+
16
+ def get_active_engine() -> Any | None:
17
+ """Retrieve the currently active RecorderEngine from the thread-local stack."""
18
+ if not hasattr(_local, "stack"):
19
+ _local.stack = []
20
+ if _local.stack:
21
+ return _local.stack[-1]
22
+ return None
23
+
24
+
25
+ class cassette:
26
+ def __init__(
27
+ self,
28
+ path: str = "cassettes",
29
+ mode: str = "auto",
30
+ ignore_fields: list[str] | None = None,
31
+ normalizer: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
32
+ adapter: Any | None = None,
33
+ ) -> None:
34
+ self.path = path
35
+ self.mode = mode
36
+ self.ignore_fields = ignore_fields or []
37
+ self.normalizer = normalizer
38
+ self.adapter = adapter
39
+
40
+ if self.adapter is None:
41
+ from sequa.llm.adapters.chat import LangChainGroqAdapter
42
+
43
+ self.adapter = LangChainGroqAdapter()
44
+
45
+ from sequa.recorder import RecorderEngine
46
+ self.engine = RecorderEngine(
47
+ path=self.path,
48
+ mode=self.mode,
49
+ ignore_fields=self.ignore_fields,
50
+ normalizer=self.normalizer,
51
+ )
52
+ self.original_methods: list[tuple[type, str, Any]] = []
53
+
54
+ def __enter__(self) -> cassette:
55
+ # 1. Push self.engine onto thread-local context stack
56
+ if not hasattr(_local, "stack"):
57
+ _local.stack = []
58
+ _local.stack.append(self.engine)
59
+
60
+ # 2. Patch langchain_groq ChatGroq if installed
61
+ try:
62
+ from langchain_groq import ChatGroq
63
+
64
+ # Sync invoke
65
+ if not getattr(ChatGroq.invoke, "__sequa_patched__", False):
66
+ original_invoke = ChatGroq.invoke
67
+ self.original_methods.append((ChatGroq, "invoke", original_invoke))
68
+
69
+ def wrapped_invoke(self_obj: ChatGroq, *args: Any, **kwargs: Any) -> Any:
70
+ active_engine = get_active_engine()
71
+ if active_engine is None:
72
+ return original_invoke(self_obj, *args, **kwargs)
73
+
74
+ def make_live_call(s: Any, *a: Any, **kw: Any) -> Any:
75
+ return original_invoke(s, *a, **kw)
76
+
77
+ return active_engine.handle_call(self.adapter, make_live_call, self_obj, *args, **kwargs)
78
+
79
+ wrapped_invoke.__sequa_patched__ = True
80
+ ChatGroq.invoke = wrapped_invoke
81
+
82
+ # Async ainvoke
83
+ if hasattr(ChatGroq, "ainvoke"):
84
+ if not getattr(ChatGroq.ainvoke, "__sequa_patched__", False):
85
+ original_ainvoke = ChatGroq.ainvoke
86
+ self.original_methods.append((ChatGroq, "ainvoke", original_ainvoke))
87
+
88
+ async def wrapped_ainvoke(self_obj: ChatGroq, *args: Any, **kwargs: Any) -> Any:
89
+ active_engine = get_active_engine()
90
+ if active_engine is None:
91
+ return await original_ainvoke(self_obj, *args, **kwargs)
92
+
93
+ async def make_live_call(s: Any, *a: Any, **kw: Any) -> Any:
94
+ return await original_ainvoke(s, *a, **kw)
95
+
96
+ return await active_engine.handle_call_async(
97
+ self.adapter, make_live_call, self_obj, *args, **kwargs
98
+ )
99
+
100
+ wrapped_ainvoke.__sequa_patched__ = True
101
+ ChatGroq.ainvoke = wrapped_ainvoke
102
+
103
+ except ImportError:
104
+ pass
105
+
106
+ return self
107
+
108
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
109
+ # 1. Pop from stack
110
+ if hasattr(_local, "stack") and _local.stack:
111
+ _local.stack.pop()
112
+
113
+ # 2. Restore original methods
114
+ for cls, attr, original in self.original_methods:
115
+ setattr(cls, attr, original)
116
+ self.original_methods.clear()
117
+
118
+ def intercept(
119
+ self, fn: Callable[..., Any], *args: Any, **kwargs: Any
120
+ ) -> dict[str, Any]:
121
+ """Provides direct interception utility for testing."""
122
+ canonical_request = self.adapter.to_canonical_request(
123
+ request=args[0] if args else None, **kwargs
124
+ )
125
+ result = fn(*args, **kwargs)
126
+ canonical_response = self.adapter.to_canonical_response(result, **kwargs)
127
+ return {
128
+ "request": canonical_request,
129
+ "response": canonical_response,
130
+ "raw": result,
131
+ }
132
+
133
+ def __call__(self, fn: F) -> F:
134
+ if inspect.iscoroutinefunction(fn):
135
+ @wraps(fn)
136
+ async def wrapped_async(*args: Any, **kwargs: Any) -> Any:
137
+ active_engine = get_active_engine()
138
+ if active_engine is not None:
139
+ async def make_live_call(*a: Any, **kw: Any) -> Any:
140
+ return await fn(*a, **kw)
141
+ return await active_engine.handle_call_async(
142
+ self.adapter, make_live_call, *args, **kwargs
143
+ )
144
+ else:
145
+ canonical_request = self.adapter.to_canonical_request(args, **kwargs)
146
+ result = await fn(*args, **kwargs)
147
+ canonical_response = self.adapter.to_canonical_response(result, **kwargs)
148
+ return {
149
+ "request": canonical_request,
150
+ "response": canonical_response,
151
+ "raw": result,
152
+ }
153
+ wrapped_async.__sequa_patched__ = True
154
+ return wrapped_async # type: ignore
155
+ else:
156
+ @wraps(fn)
157
+ def wrapped_sync(*args: Any, **kwargs: Any) -> Any:
158
+ active_engine = get_active_engine()
159
+ if active_engine is not None:
160
+ def make_live_call(*a: Any, **kw: Any) -> Any:
161
+ return fn(*a, **kw)
162
+ return active_engine.handle_call(
163
+ self.adapter, make_live_call, *args, **kwargs
164
+ )
165
+ else:
166
+ canonical_request = self.adapter.to_canonical_request(args, **kwargs)
167
+ result = fn(*args, **kwargs)
168
+ canonical_response = self.adapter.to_canonical_response(result, **kwargs)
169
+ return {
170
+ "request": canonical_request,
171
+ "response": canonical_response,
172
+ "raw": result,
173
+ }
174
+ wrapped_sync.__sequa_patched__ = True
175
+ return wrapped_sync # type: ignore
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import sys
7
+ from typing import Any
8
+
9
+
10
+ def load_all_cassettes(path: str) -> list[tuple[str, dict[str, Any]]]:
11
+ """Finds and loads all JSON cassette files in the given directory or file path."""
12
+ cassettes = []
13
+ if os.path.isfile(path) and path.endswith(".json"):
14
+ try:
15
+ with open(path, "r", encoding="utf-8") as f:
16
+ cassettes.append((path, json.load(f)))
17
+ except Exception as e:
18
+ print(f"Error loading {path}: {e}", file=sys.stderr)
19
+ elif os.path.isdir(path):
20
+ for root, _, files in os.walk(path):
21
+ for file in files:
22
+ if file.endswith(".json"):
23
+ file_path = os.path.join(root, file)
24
+ try:
25
+ with open(file_path, "r", encoding="utf-8") as f:
26
+ data = json.load(f)
27
+ # Simple validation to verify it is a Sequa cassette file
28
+ if "request" in data and "response" in data:
29
+ cassettes.append((file_path, data))
30
+ except Exception as e:
31
+ # Skip files that are not valid JSON or not cassettes
32
+ pass
33
+ return cassettes
34
+
35
+
36
+ def cmd_stats(args: argparse.Namespace) -> int:
37
+ """Calculates and prints statistics for stored cassettes."""
38
+ cassettes = load_all_cassettes(args.path)
39
+ if not cassettes:
40
+ print(f"No cassettes found at path: {args.path}")
41
+ return 0
42
+
43
+ count = len(cassettes)
44
+ total_latency_ms = 0.0
45
+ total_size_bytes = 0
46
+
47
+ for path, data in cassettes:
48
+ # Size
49
+ try:
50
+ total_size_bytes += os.path.getsize(path)
51
+ except Exception:
52
+ pass
53
+
54
+ # Latency
55
+ latency = data.get("metadata", {}).get("latency_ms")
56
+ if latency is None:
57
+ latency = data.get("response", {}).get("latency")
58
+
59
+ if latency is not None:
60
+ try:
61
+ total_latency_ms += float(latency)
62
+ except ValueError:
63
+ pass
64
+
65
+ print("========================================")
66
+ print(" Sequa Statistics")
67
+ print("========================================")
68
+ print(f"Total Cassettes: {count}")
69
+ print(f"Total Size on Disk: {total_size_bytes / 1024:.2f} KB ({total_size_bytes} bytes)")
70
+ print(f"Total Latency Saved: {total_latency_ms / 1000:.2f} seconds ({total_latency_ms:.1f} ms)")
71
+ print("========================================")
72
+ return 0
73
+
74
+
75
+ def cmd_inspect(args: argparse.Namespace) -> int:
76
+ """Lists summary of all stored cassettes."""
77
+ cassettes = load_all_cassettes(args.path)
78
+ if not cassettes:
79
+ print(f"No cassettes found at path: {args.path}")
80
+ return 0
81
+
82
+ print(f"{'Filename / Hash':<40} | {'Provider':<15} | {'Model':<25} | {'Created At':<25}")
83
+ print("-" * 111)
84
+ for path, data in cassettes:
85
+ filename = os.path.basename(path)
86
+ provider = data.get("provider", "unknown")
87
+
88
+ # Get model
89
+ req = data.get("request", {})
90
+ model = req.get("model") or "unknown"
91
+
92
+ created_at = data.get("created_at", "unknown")
93
+
94
+ # Format filename to fit table neatly
95
+ if len(filename) > 38:
96
+ filename_display = filename[:35] + "..."
97
+ else:
98
+ filename_display = filename
99
+
100
+ print(f"{filename_display:<40} | {provider:<15} | {model:<25} | {created_at:<25}")
101
+ return 0
102
+
103
+
104
+ def cmd_clean(args: argparse.Namespace) -> int:
105
+ """Cleans dynamic/volatile fields from cassettes or formats them."""
106
+ cassettes = load_all_cassettes(args.path)
107
+ if not cassettes:
108
+ print(f"No cassettes found at path: {args.path}")
109
+ return 0
110
+
111
+ cleaned_count = 0
112
+ for path, data in cassettes:
113
+ modified = False
114
+
115
+ # 1. Remove latency metadata if requested
116
+ if args.remove_latency:
117
+ if "metadata" in data and "latency_ms" in data["metadata"]:
118
+ del data["metadata"]["latency_ms"]
119
+ modified = True
120
+ if "response" in data and "latency" in data["response"]:
121
+ del data["response"]["latency"]
122
+ modified = True
123
+
124
+ # 2. Remove timestamps if requested
125
+ if args.remove_timestamps:
126
+ if "created_at" in data:
127
+ data["created_at"] = ""
128
+ modified = True
129
+
130
+ # 3. Format/Sort keys cleanly (always done during clean)
131
+ from sequa.utils import sort_dict_keys
132
+ data = sort_dict_keys(data)
133
+ modified = True
134
+
135
+ if modified:
136
+ try:
137
+ with open(path, "w", encoding="utf-8") as f:
138
+ json.dump(data, f, indent=4, ensure_ascii=False)
139
+ cleaned_count += 1
140
+ except Exception as e:
141
+ print(f"Error writing to {path}: {e}", file=sys.stderr)
142
+
143
+ print(f"Successfully formatted/cleaned {cleaned_count} cassettes.")
144
+ return 0
145
+
146
+
147
+ def main() -> None:
148
+ parser = argparse.ArgumentParser(
149
+ description="Sequa CLI - Manage and inspect your LLM snapshot cassettes."
150
+ )
151
+ subparsers = parser.add_subparsers(dest="command", required=True)
152
+
153
+ # stats
154
+ parser_stats = subparsers.add_parser("stats", help="Show summary statistics of stored cassettes.")
155
+ parser_stats.add_argument(
156
+ "--path",
157
+ "-p",
158
+ default="cassettes",
159
+ help="Path to the cassettes directory or file (default: 'cassettes').",
160
+ )
161
+
162
+ # inspect
163
+ parser_inspect = subparsers.add_parser("inspect", help="List summary of all stored cassettes.")
164
+ parser_inspect.add_argument(
165
+ "--path",
166
+ "-p",
167
+ default="cassettes",
168
+ help="Path to the cassettes directory or file (default: 'cassettes').",
169
+ )
170
+
171
+ # clean
172
+ parser_clean = subparsers.add_parser("clean", help="Clean or format stored cassettes.")
173
+ parser_clean.add_argument(
174
+ "--path",
175
+ "-p",
176
+ default="cassettes",
177
+ help="Path to the cassettes directory or file (default: 'cassettes').",
178
+ )
179
+ parser_clean.add_argument(
180
+ "--remove-latency",
181
+ action="store_true",
182
+ help="Remove dynamic latency values from cassettes to avoid git diff noise.",
183
+ )
184
+ parser_clean.add_argument(
185
+ "--remove-timestamps",
186
+ action="store_true",
187
+ help="Redact/clear dynamic timestamps.",
188
+ )
189
+
190
+ args = parser.parse_args()
191
+
192
+ if args.command == "stats":
193
+ sys.exit(cmd_stats(args))
194
+ elif args.command == "inspect":
195
+ sys.exit(cmd_inspect(args))
196
+ elif args.command == "clean":
197
+ sys.exit(cmd_clean(args))
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
@@ -0,0 +1,21 @@
1
+ from .base import AdapterRegistry, ProviderAdapter, CanonicalRequest, CanonicalResponse
2
+ from .chat import OpenAIAdapter, LangChainGroqAdapter
3
+ from .monkey_patch import LangChainGroqMonkeyPatch
4
+ from ...cassette import cassette
5
+
6
+
7
+ class RequestInterceptor(cassette):
8
+ pass
9
+
10
+
11
+ __all__ = [
12
+ "AdapterRegistry",
13
+ "ProviderAdapter",
14
+ "CanonicalRequest",
15
+ "CanonicalResponse",
16
+ "OpenAIAdapter",
17
+ "LangChainGroqAdapter",
18
+ "LangChainGroqMonkeyPatch",
19
+ "cassette",
20
+ "RequestInterceptor",
21
+ ]