sequa 0.0.1__tar.gz → 0.0.2__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.
@@ -1,11 +1,14 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sequa
3
- Version: 0.0.1
3
+ Version: 0.0.2
4
4
  Summary: Add your description here
5
5
  Author: thetechnoadvisor
6
6
  Author-email: thetechnoadvisor <thetechnoadvisor@gmail.com>
7
7
  Requires-Dist: python-dotenv>=1.0.0
8
+ Requires-Dist: langchain-core>=0.1.0
8
9
  Requires-Dist: langchain-groq>=0.3.0
10
+ Requires-Dist: openai>=1.0.0
11
+ Requires-Dist: anthropic>=0.18.0
9
12
  Requires-Python: >=3.12
10
13
  Description-Content-Type: text/markdown
11
14
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sequa"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -9,7 +9,10 @@ authors = [
9
9
  requires-python = ">=3.12"
10
10
  dependencies = [
11
11
  "python-dotenv>=1.0.0",
12
+ "langchain-core>=0.1.0",
12
13
  "langchain-groq>=0.3.0",
14
+ "openai>=1.0.0",
15
+ "anthropic>=0.18.0",
13
16
  ]
14
17
 
15
18
  [project.scripts]
@@ -50,6 +50,7 @@ class cassette:
50
50
  normalizer=self.normalizer,
51
51
  )
52
52
  self.original_methods: list[tuple[type, str, Any]] = []
53
+ self.patchers: list[Any] = []
53
54
 
54
55
  def __enter__(self) -> cassette:
55
56
  # 1. Push self.engine onto thread-local context stack
@@ -103,6 +104,26 @@ class cassette:
103
104
  except ImportError:
104
105
  pass
105
106
 
107
+ # 3. Patch OpenAI if installed
108
+ try:
109
+ import openai
110
+ from sequa.llm.adapters.patch_openai import OpenAIMonkeyPatch
111
+ op_patcher = OpenAIMonkeyPatch()
112
+ op_patcher.patch()
113
+ self.patchers.append(op_patcher)
114
+ except ImportError:
115
+ pass
116
+
117
+ # 4. Patch Anthropic if installed
118
+ try:
119
+ import anthropic
120
+ from sequa.llm.adapters.patch_anthropic import AnthropicMonkeyPatch
121
+ ant_patcher = AnthropicMonkeyPatch()
122
+ ant_patcher.patch()
123
+ self.patchers.append(ant_patcher)
124
+ except ImportError:
125
+ pass
126
+
106
127
  return self
107
128
 
108
129
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
@@ -115,6 +136,11 @@ class cassette:
115
136
  setattr(cls, attr, original)
116
137
  self.original_methods.clear()
117
138
 
139
+ # 3. Restore OpenAI and Anthropic patchers
140
+ for patcher in self.patchers:
141
+ patcher.restore()
142
+ self.patchers.clear()
143
+
118
144
  def intercept(
119
145
  self, fn: Callable[..., Any], *args: Any, **kwargs: Any
120
146
  ) -> dict[str, Any]:
@@ -1,6 +1,9 @@
1
1
  from .base import AdapterRegistry, ProviderAdapter, CanonicalRequest, CanonicalResponse
2
2
  from .chat import OpenAIAdapter, LangChainGroqAdapter
3
+ from .anthropic import AnthropicAdapter
3
4
  from .monkey_patch import LangChainGroqMonkeyPatch
5
+ from .patch_openai import OpenAIMonkeyPatch
6
+ from .patch_anthropic import AnthropicMonkeyPatch
4
7
  from ...cassette import cassette
5
8
 
6
9
 
@@ -15,7 +18,10 @@ __all__ = [
15
18
  "CanonicalResponse",
16
19
  "OpenAIAdapter",
17
20
  "LangChainGroqAdapter",
21
+ "AnthropicAdapter",
18
22
  "LangChainGroqMonkeyPatch",
23
+ "OpenAIMonkeyPatch",
24
+ "AnthropicMonkeyPatch",
19
25
  "cassette",
20
26
  "RequestInterceptor",
21
27
  ]
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from sequa.llm.adapters.base import CanonicalRequest, CanonicalResponse, ProviderAdapter
5
+
6
+
7
+ class AnthropicAdapter(ProviderAdapter):
8
+ provider_name: str = "anthropic"
9
+
10
+ def to_canonical_request(self, request: Any, **kwargs: Any) -> CanonicalRequest:
11
+ model = kwargs.get("model")
12
+ messages = kwargs.get("messages") or []
13
+ temperature = kwargs.get("temperature")
14
+
15
+ params = {k: v for k, v in kwargs.items() if k not in ("model", "messages", "temperature")}
16
+
17
+ normalized_messages = []
18
+ for msg in messages:
19
+ if isinstance(msg, dict):
20
+ normalized_messages.append({"role": msg.get("role"), "content": msg.get("content")})
21
+ else:
22
+ role = getattr(msg, "role", "user")
23
+ content = getattr(msg, "content", "")
24
+ normalized_messages.append({"role": role, "content": content})
25
+
26
+ return CanonicalRequest(
27
+ provider=self.provider_name,
28
+ model=model,
29
+ temperature=temperature,
30
+ messages=normalized_messages,
31
+ params=params,
32
+ metadata={"raw_request": kwargs},
33
+ )
34
+
35
+ def to_canonical_response(self, response: Any, **kwargs: Any) -> CanonicalResponse:
36
+ # Check if response is a dict or Anthropic Message object
37
+ if isinstance(response, dict):
38
+ resp_id = response.get("id")
39
+ model = response.get("model") or kwargs.get("model")
40
+ role = response.get("role", "assistant")
41
+ stop_reason = response.get("stop_reason")
42
+ stop_sequence = response.get("stop_sequence")
43
+ raw_content = response.get("content") or []
44
+
45
+ # Extract content string
46
+ content_text = ""
47
+ for item in raw_content:
48
+ if isinstance(item, dict):
49
+ if item.get("type") == "text":
50
+ content_text += item.get("text", "")
51
+ else:
52
+ content_text += getattr(item, "text", "")
53
+
54
+ # Extract usage
55
+ raw_usage = response.get("usage") or {}
56
+ usage = {
57
+ "input_tokens": raw_usage.get("input_tokens") or raw_usage.get("prompt_tokens") or 0,
58
+ "output_tokens": raw_usage.get("output_tokens") or raw_usage.get("completion_tokens") or 0,
59
+ }
60
+ else:
61
+ # It's an Anthropic Message object
62
+ resp_id = getattr(response, "id", None)
63
+ model = getattr(response, "model", None) or kwargs.get("model")
64
+ role = getattr(response, "role", "assistant")
65
+ stop_reason = getattr(response, "stop_reason", None)
66
+ stop_sequence = getattr(response, "stop_sequence", None)
67
+ raw_content = getattr(response, "content", [])
68
+
69
+ content_text = ""
70
+ for item in raw_content:
71
+ content_text += getattr(item, "text", "")
72
+
73
+ usage_obj = getattr(response, "usage", None)
74
+ usage = None
75
+ if usage_obj:
76
+ usage = {
77
+ "input_tokens": getattr(usage_obj, "input_tokens", 0),
78
+ "output_tokens": getattr(usage_obj, "output_tokens", 0),
79
+ }
80
+
81
+ # Format content block list for serialization
82
+ content_serializable = []
83
+ if isinstance(raw_content, list):
84
+ for item in raw_content:
85
+ if isinstance(item, dict):
86
+ content_serializable.append({
87
+ "type": item.get("type", "text"),
88
+ "text": item.get("text", ""),
89
+ })
90
+ else:
91
+ content_serializable.append({
92
+ "type": getattr(item, "type", "text"),
93
+ "text": getattr(item, "text", ""),
94
+ })
95
+ else:
96
+ content_serializable.append({
97
+ "type": "text",
98
+ "text": content_text,
99
+ })
100
+
101
+ return CanonicalResponse(
102
+ provider=self.provider_name,
103
+ model=model,
104
+ output=content_text,
105
+ usage=usage,
106
+ metadata={
107
+ "raw_response": {
108
+ "id": resp_id,
109
+ "model": model,
110
+ "role": role,
111
+ "type": "message",
112
+ "stop_reason": stop_reason,
113
+ "stop_sequence": stop_sequence,
114
+ "content": content_serializable,
115
+ }
116
+ }
117
+ )
118
+
119
+ def from_canonical_response(self, response: CanonicalResponse, request: Any) -> Any:
120
+ raw_resp = response.metadata.get("raw_response", {})
121
+ resp_id = raw_resp.get("id") or "replayed-response"
122
+ content_data = raw_resp.get("content") or []
123
+ usage_data = response.usage or {}
124
+
125
+ try:
126
+ from anthropic.types import Message, TextBlock, Usage
127
+
128
+ content_blocks = []
129
+ for item in content_data:
130
+ content_blocks.append(TextBlock(
131
+ text=item.get("text", response.output or ""),
132
+ type="text"
133
+ ))
134
+
135
+ usage_obj = None
136
+ if usage_data:
137
+ usage_obj = Usage(
138
+ input_tokens=usage_data.get("input_tokens", 0),
139
+ output_tokens=usage_data.get("output_tokens", 0),
140
+ )
141
+
142
+ return Message(
143
+ id=resp_id,
144
+ content=content_blocks,
145
+ model=response.model or "replayed-model",
146
+ role=raw_resp.get("role", "assistant"),
147
+ stop_reason=raw_resp.get("stop_reason", "end_turn"),
148
+ stop_sequence=raw_resp.get("stop_sequence"),
149
+ type="message",
150
+ usage=usage_obj,
151
+ )
152
+ except ImportError:
153
+ class MockTextBlock:
154
+ def __init__(self, text: str):
155
+ self.text = text
156
+ self.type = "text"
157
+
158
+ class MockUsage:
159
+ def __init__(self, input_t: int, output_t: int):
160
+ self.input_tokens = input_t
161
+ self.output_tokens = output_t
162
+
163
+ class MockMessage:
164
+ def __init__(self, id: str, content: list[MockTextBlock], model: str, role: str, stop_reason: str, stop_sequence: str | None, usage: MockUsage | None):
165
+ self.id = id
166
+ self.content = content
167
+ self.model = model
168
+ self.role = role
169
+ self.stop_reason = stop_reason
170
+ self.stop_sequence = stop_sequence
171
+ self.type = "message"
172
+ self.usage = usage
173
+
174
+ content_blocks = []
175
+ for item in content_data:
176
+ content_blocks.append(MockTextBlock(text=item.get("text", response.output or "")))
177
+
178
+ usage_obj = None
179
+ if usage_data:
180
+ usage_obj = MockUsage(
181
+ usage_data.get("input_tokens") or usage_data.get("prompt_tokens") or 0,
182
+ usage_data.get("output_tokens") or usage_data.get("completion_tokens") or 0,
183
+ )
184
+
185
+ return MockMessage(
186
+ id=resp_id,
187
+ content=content_blocks,
188
+ model=response.model or "replayed-model",
189
+ role=raw_resp.get("role", "assistant"),
190
+ stop_reason=raw_resp.get("stop_reason", "end_turn"),
191
+ stop_sequence=raw_resp.get("stop_sequence"),
192
+ usage=usage_obj,
193
+ )
@@ -0,0 +1,393 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .base import CanonicalRequest, CanonicalResponse, ProviderAdapter
6
+
7
+
8
+ class LangChainGroqAdapter(ProviderAdapter):
9
+ """A generic chat-completions adapter that can be configured per provider."""
10
+
11
+ def __init__(
12
+ self, provider_name: str = "langchain_groq", response_object: str | None = None
13
+ ) -> None:
14
+ self.provider_name = provider_name
15
+ self._response_object = response_object
16
+
17
+ def to_canonical_request(self, request: Any, **kwargs: Any) -> CanonicalRequest:
18
+ if isinstance(request, (tuple, list)) and len(request) > 0:
19
+ first_arg = request[0]
20
+ if hasattr(first_arg, "model_name"):
21
+ payload = first_arg
22
+ messages = request[1] if len(request) > 1 else []
23
+ else:
24
+ payload = first_arg
25
+ messages = []
26
+ else:
27
+ payload = request or {}
28
+ messages = []
29
+
30
+ model = getattr(payload, "model_name", None) or kwargs.get("model")
31
+ temperature = getattr(payload, "temperature", None) or kwargs.get("temperature")
32
+
33
+ if isinstance(payload, dict):
34
+ raw_payload = payload
35
+ if not messages:
36
+ messages = payload.get("messages") or kwargs.get("messages") or []
37
+ else:
38
+ raw_payload = {"input": payload}
39
+ if not messages:
40
+ messages = kwargs.get("messages") or []
41
+
42
+ if isinstance(messages, (list, tuple)):
43
+ messages_list = list(messages)
44
+ elif messages:
45
+ messages_list = [messages]
46
+ else:
47
+ messages_list = []
48
+
49
+ return CanonicalRequest(
50
+ provider=self.provider_name,
51
+ model=model,
52
+ temperature=temperature,
53
+ messages=messages_list,
54
+ params=kwargs,
55
+ metadata={"raw_request": raw_payload},
56
+ )
57
+
58
+ def to_canonical_response(self, response: Any, **kwargs: Any) -> CanonicalResponse:
59
+ payload = response or {}
60
+ if hasattr(payload, "content") and not isinstance(payload, dict):
61
+ content = getattr(payload, "content", None)
62
+ return CanonicalResponse(
63
+ provider=self.provider_name,
64
+ output=content,
65
+ model=payload.response_metadata.get("model_name"),
66
+ tool_calls=payload.tool_calls,
67
+ invalid_tool_calls=payload.invalid_tool_calls,
68
+ latency=payload.response_metadata.get("total_time"),
69
+ reasoning=(
70
+ payload.response_metadata.get("token_usage", {}).get("reasoning_content")
71
+ if payload.response_metadata.get("token_usage")
72
+ else None
73
+ ),
74
+ usage=payload.usage_metadata,
75
+ metadata={
76
+ "raw_response": {
77
+ "id": getattr(payload, "id", None),
78
+ "response_metadata": getattr(payload, "response_metadata", {}),
79
+ }
80
+ },
81
+ )
82
+
83
+ choices = payload.get("choices", [])
84
+ first_choice = choices[0] if choices else {}
85
+ message = first_choice.get("message", {})
86
+
87
+ return CanonicalResponse(
88
+ provider=self.provider_name,
89
+ model=kwargs.get("model"),
90
+ output=message.get("content"),
91
+ usage=payload.get("usage"),
92
+ metadata={"raw_response": payload},
93
+ )
94
+
95
+ def from_canonical_response(self, response: CanonicalResponse, request: Any) -> Any:
96
+ raw_resp = response.metadata.get("raw_response")
97
+ resp_id = "replayed-response"
98
+ finish_reason = "stop"
99
+ if raw_resp:
100
+ if isinstance(raw_resp, dict):
101
+ resp_id = raw_resp.get("id", "replayed-response")
102
+ choices = raw_resp.get("choices", [])
103
+ if choices:
104
+ finish_reason = choices[0].get("finish_reason", "stop")
105
+ else:
106
+ resp_id = getattr(raw_resp, "id", "replayed-response")
107
+ response_metadata = getattr(raw_resp, "response_metadata", {})
108
+ finish_reason = response_metadata.get("finish_reason", "stop")
109
+
110
+ # Determine if we should return a LangChain AIMessage object
111
+ is_langchain = False
112
+ if request and isinstance(request, (tuple, list)) and len(request) > 0:
113
+ first_arg = request[0]
114
+ # If the class represents ChatGroq or a LangChain chat model
115
+ if hasattr(first_arg, "model_name") or hasattr(first_arg, "invoke"):
116
+ is_langchain = True
117
+
118
+ if raw_resp and hasattr(raw_resp, "content") and not isinstance(raw_resp, dict):
119
+ is_langchain = True
120
+
121
+ if is_langchain:
122
+ from langchain_core.messages import AIMessage
123
+
124
+ response_metadata = {}
125
+ if raw_resp:
126
+ if isinstance(raw_resp, dict):
127
+ response_metadata = dict(raw_resp.get("response_metadata", {}))
128
+ elif hasattr(raw_resp, "response_metadata"):
129
+ response_metadata = dict(getattr(raw_resp, "response_metadata", {}))
130
+
131
+ if not response_metadata:
132
+ response_metadata = {
133
+ "model_name": response.model,
134
+ "finish_reason": finish_reason,
135
+ }
136
+ if response.latency is not None:
137
+ response_metadata["total_time"] = response.latency
138
+
139
+ aimsg_kwargs: dict[str, Any] = {
140
+ "content": response.output or "",
141
+ "response_metadata": response_metadata,
142
+ "id": resp_id,
143
+ "tool_calls": response.tool_calls or [],
144
+ "invalid_tool_calls": response.invalid_tool_calls or [],
145
+ }
146
+
147
+ if response.usage and isinstance(response.usage, dict):
148
+ input_t = response.usage.get("input_tokens") or response.usage.get("prompt_tokens") or 0
149
+ output_t = response.usage.get("output_tokens") or response.usage.get("completion_tokens") or 0
150
+ total_t = response.usage.get("total_tokens") or (input_t + output_t)
151
+ aimsg_kwargs["usage_metadata"] = {
152
+ "input_tokens": input_t,
153
+ "output_tokens": output_t,
154
+ "total_tokens": total_t,
155
+ }
156
+
157
+ return AIMessage(**aimsg_kwargs)
158
+
159
+ payload: dict[str, Any] = {
160
+ "id": resp_id,
161
+ "choices": [
162
+ {
163
+ "message": {
164
+ "role": "assistant",
165
+ "content": response.output,
166
+ },
167
+ "finish_reason": finish_reason,
168
+ }
169
+ ],
170
+ "usage": response.usage or {},
171
+ "model": response.model or "replayed-model",
172
+ }
173
+
174
+ if self._response_object is not None:
175
+ payload["object"] = self._response_object
176
+
177
+ return payload
178
+
179
+
180
+ class OpenAIAdapter(ProviderAdapter):
181
+ """An OpenAI chat completions adapter."""
182
+
183
+ def __init__(self, response_object: str | None = "chat.completion") -> None:
184
+ self.provider_name = "openai"
185
+ self._response_object = response_object
186
+
187
+ def to_canonical_request(self, request: Any, **kwargs: Any) -> CanonicalRequest:
188
+ payload = request or {}
189
+ if not isinstance(payload, dict):
190
+ if isinstance(payload, (list, tuple)) and len(payload) > 0:
191
+ payload = payload[0] if isinstance(payload[0], dict) else {}
192
+ else:
193
+ payload = {}
194
+
195
+ model = kwargs.get("model") or payload.get("model")
196
+ messages = kwargs.get("messages") or payload.get("messages") or []
197
+ temperature = kwargs.get("temperature") or payload.get("temperature")
198
+
199
+ params = {k: v for k, v in kwargs.items() if k not in ("model", "messages")}
200
+ for k, v in payload.items():
201
+ if k not in ("model", "messages") and k not in params:
202
+ params[k] = v
203
+
204
+ normalized_messages = []
205
+ for msg in messages:
206
+ if isinstance(msg, dict):
207
+ normalized_messages.append({"role": msg.get("role"), "content": msg.get("content")})
208
+ else:
209
+ role = getattr(msg, "role", "user")
210
+ content = getattr(msg, "content", "")
211
+ normalized_messages.append({"role": role, "content": content})
212
+
213
+ return CanonicalRequest(
214
+ provider=self.provider_name,
215
+ model=model,
216
+ temperature=temperature,
217
+ messages=normalized_messages,
218
+ params=params,
219
+ metadata={"raw_request": kwargs},
220
+ )
221
+
222
+ def to_canonical_response(self, response: Any, **kwargs: Any) -> CanonicalResponse:
223
+ if isinstance(response, dict):
224
+ payload = response
225
+ choices = payload.get("choices", [])
226
+ first_choice = choices[0] if choices else {}
227
+ message = first_choice.get("message", {})
228
+ output = message.get("content")
229
+ model = payload.get("model") or kwargs.get("model")
230
+ usage = payload.get("usage")
231
+ resp_id = payload.get("id")
232
+ tool_calls = message.get("tool_calls") or []
233
+ else:
234
+ # It's a ChatCompletion object
235
+ resp_id = getattr(response, "id", None)
236
+ model = getattr(response, "model", None) or kwargs.get("model")
237
+ usage_obj = getattr(response, "usage", None)
238
+ usage = None
239
+ if usage_obj:
240
+ usage = {
241
+ "prompt_tokens": getattr(usage_obj, "prompt_tokens", 0),
242
+ "completion_tokens": getattr(usage_obj, "completion_tokens", 0),
243
+ "total_tokens": getattr(usage_obj, "total_tokens", 0),
244
+ }
245
+
246
+ choices = getattr(response, "choices", [])
247
+ first_choice = choices[0] if choices else None
248
+ output = None
249
+ tool_calls = []
250
+ if first_choice:
251
+ message_obj = getattr(first_choice, "message", None)
252
+ if message_obj:
253
+ output = getattr(message_obj, "content", None)
254
+ tool_calls = getattr(message_obj, "tool_calls", None) or []
255
+
256
+ # Convert choices list to serializable dictionary format
257
+ choices_serializable = []
258
+ for c in choices:
259
+ if isinstance(c, dict):
260
+ msg = c.get("message") or {}
261
+ choices_serializable.append({
262
+ "finish_reason": c.get("finish_reason", "stop"),
263
+ "message": {
264
+ "role": msg.get("role", "assistant"),
265
+ "content": msg.get("content", ""),
266
+ }
267
+ })
268
+ else:
269
+ msg_obj = getattr(c, "message", None)
270
+ choices_serializable.append({
271
+ "finish_reason": getattr(c, "finish_reason", "stop"),
272
+ "message": {
273
+ "role": getattr(msg_obj, "role", "assistant") if msg_obj else "assistant",
274
+ "content": getattr(msg_obj, "content", "") if msg_obj else "",
275
+ }
276
+ })
277
+
278
+ return CanonicalResponse(
279
+ provider=self.provider_name,
280
+ model=model,
281
+ output=output,
282
+ usage=usage,
283
+ tool_calls=list(tool_calls),
284
+ metadata={
285
+ "raw_response": {
286
+ "id": resp_id,
287
+ "model": model,
288
+ "choices": choices_serializable,
289
+ }
290
+ }
291
+ )
292
+
293
+ def from_canonical_response(self, response: CanonicalResponse, request: Any) -> Any:
294
+ raw_resp = response.metadata.get("raw_response", {})
295
+ resp_id = raw_resp.get("id") or "replayed-response"
296
+ choices_data = raw_resp.get("choices") or []
297
+
298
+ choices_list = []
299
+ for i, choice in enumerate(choices_data):
300
+ msg_data = choice.get("message", {})
301
+ choices_list.append({
302
+ "index": i,
303
+ "finish_reason": choice.get("finish_reason", "stop"),
304
+ "message": {
305
+ "role": msg_data.get("role", "assistant"),
306
+ "content": msg_data.get("content", response.output),
307
+ }
308
+ })
309
+
310
+ usage_data = response.usage or {}
311
+
312
+ try:
313
+ from openai.types.chat import ChatCompletion
314
+ from openai.types.chat.chat_completion import Choice, ChoiceMessage
315
+ from openai.types import CompletionUsage
316
+
317
+ choices = []
318
+ for item in choices_list:
319
+ msg = item["message"]
320
+ choices.append(Choice(
321
+ finish_reason=item["finish_reason"],
322
+ index=item["index"],
323
+ message=ChoiceMessage(
324
+ content=msg["content"],
325
+ role=msg["role"],
326
+ )
327
+ ))
328
+
329
+ usage = None
330
+ if usage_data:
331
+ usage = CompletionUsage(
332
+ prompt_tokens=usage_data.get("prompt_tokens", 0),
333
+ completion_tokens=usage_data.get("completion_tokens", 0),
334
+ total_tokens=usage_data.get("total_tokens", 0),
335
+ )
336
+
337
+ return ChatCompletion(
338
+ id=resp_id,
339
+ choices=choices,
340
+ created=123456789,
341
+ model=response.model or "replayed-model",
342
+ object="chat.completion",
343
+ usage=usage,
344
+ )
345
+ except ImportError:
346
+ class MockMessage:
347
+ def __init__(self, role: str, content: str):
348
+ self.role = role
349
+ self.content = content
350
+
351
+ class MockChoice:
352
+ def __init__(self, index: int, finish_reason: str, message: MockMessage):
353
+ self.index = index
354
+ self.finish_reason = finish_reason
355
+ self.message = message
356
+
357
+ class MockUsage:
358
+ def __init__(self, prompt: int, completion: int, total: int):
359
+ self.prompt_tokens = prompt
360
+ self.completion_tokens = completion
361
+ self.total_tokens = total
362
+
363
+ class MockChatCompletion:
364
+ def __init__(self, id: str, choices: list[MockChoice], model: str, usage: MockUsage | None):
365
+ self.id = id
366
+ self.choices = choices
367
+ self.model = model
368
+ self.usage = usage
369
+ self.object = "chat.completion"
370
+
371
+ choices = []
372
+ for item in choices_list:
373
+ msg = item["message"]
374
+ choices.append(MockChoice(
375
+ index=item["index"],
376
+ finish_reason=item["finish_reason"],
377
+ message=MockMessage(role=msg["role"], content=msg["content"])
378
+ ))
379
+
380
+ usage = None
381
+ if usage_data:
382
+ usage = MockUsage(
383
+ usage_data.get("prompt_tokens") or usage_data.get("input_tokens") or 0,
384
+ usage_data.get("completion_tokens") or usage_data.get("output_tokens") or 0,
385
+ usage_data.get("total_tokens") or 0,
386
+ )
387
+
388
+ return MockChatCompletion(
389
+ id=resp_id,
390
+ choices=choices,
391
+ model=response.model or "replayed-model",
392
+ usage=usage,
393
+ )
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from sequa.llm.adapters.anthropic import AnthropicAdapter
5
+ from sequa.cassette import get_active_engine
6
+
7
+
8
+ class AnthropicMonkeyPatch:
9
+ def __init__(self) -> None:
10
+ self.adapter = AnthropicAdapter()
11
+ self.original_create = None
12
+ self.original_async_create = None
13
+ self.patched = False
14
+
15
+ def patch(self) -> None:
16
+ if self.patched:
17
+ return
18
+
19
+ try:
20
+ from anthropic.resources.messages import Messages, AsyncMessages
21
+
22
+ # Sync Patch
23
+ self.original_create = Messages.create
24
+
25
+ def wrapped_create(self_obj: Messages, *args: Any, **kwargs: Any) -> Any:
26
+ active_engine = get_active_engine()
27
+ if active_engine is None:
28
+ return self.original_create(self_obj, *args, **kwargs)
29
+
30
+ def make_live_call(*a: Any, **kw: Any) -> Any:
31
+ return self.original_create(self_obj, *a, **kw)
32
+
33
+ return active_engine.handle_call(self.adapter, make_live_call, self_obj, *args, **kwargs)
34
+
35
+ wrapped_create.__llmcassette_patched__ = True
36
+ Messages.create = wrapped_create
37
+
38
+ # Async Patch
39
+ self.original_async_create = AsyncMessages.create
40
+
41
+ async def wrapped_async_create(self_obj: AsyncMessages, *args: Any, **kwargs: Any) -> Any:
42
+ active_engine = get_active_engine()
43
+ if active_engine is None:
44
+ return await self.original_async_create(self_obj, *args, **kwargs)
45
+
46
+ async def make_live_call(*a: Any, **kw: Any) -> Any:
47
+ return await self.original_async_create(self_obj, *a, **kw)
48
+
49
+ return await active_engine.handle_call_async(self.adapter, make_live_call, self_obj, *args, **kwargs)
50
+
51
+ wrapped_async_create.__llmcassette_patched__ = True
52
+ AsyncMessages.create = wrapped_async_create
53
+
54
+ self.patched = True
55
+ except ImportError:
56
+ pass
57
+
58
+ def restore(self) -> None:
59
+ if not self.patched:
60
+ return
61
+
62
+ try:
63
+ from anthropic.resources.messages import Messages, AsyncMessages
64
+ if self.original_create:
65
+ Messages.create = self.original_create
66
+ if self.original_async_create:
67
+ AsyncMessages.create = self.original_async_create
68
+ self.patched = False
69
+ except ImportError:
70
+ pass
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from sequa.llm.adapters.chat import OpenAIAdapter
5
+ from sequa.cassette import get_active_engine
6
+
7
+
8
+ class OpenAIMonkeyPatch:
9
+ def __init__(self) -> None:
10
+ self.adapter = OpenAIAdapter()
11
+ self.original_create = None
12
+ self.original_async_create = None
13
+ self.patched = False
14
+
15
+ def patch(self) -> None:
16
+ if self.patched:
17
+ return
18
+
19
+ try:
20
+ from openai.resources.chat.completions import Completions, AsyncCompletions
21
+
22
+ # Sync Patch
23
+ self.original_create = Completions.create
24
+
25
+ def wrapped_create(self_obj: Completions, *args: Any, **kwargs: Any) -> Any:
26
+ active_engine = get_active_engine()
27
+ if active_engine is None:
28
+ return self.original_create(self_obj, *args, **kwargs)
29
+
30
+ def make_live_call(*a: Any, **kw: Any) -> Any:
31
+ return self.original_create(self_obj, *a, **kw)
32
+
33
+ return active_engine.handle_call(self.adapter, make_live_call, self_obj, *args, **kwargs)
34
+
35
+ wrapped_create.__llmcassette_patched__ = True
36
+ Completions.create = wrapped_create
37
+
38
+ # Async Patch
39
+ self.original_async_create = AsyncCompletions.create
40
+
41
+ async def wrapped_async_create(self_obj: AsyncCompletions, *args: Any, **kwargs: Any) -> Any:
42
+ active_engine = get_active_engine()
43
+ if active_engine is None:
44
+ return await self.original_async_create(self_obj, *args, **kwargs)
45
+
46
+ async def make_live_call(*a: Any, **kw: Any) -> Any:
47
+ return await self.original_async_create(self_obj, *a, **kw)
48
+
49
+ return await active_engine.handle_call_async(self.adapter, make_live_call, self_obj, *args, **kwargs)
50
+
51
+ wrapped_async_create.__llmcassette_patched__ = True
52
+ AsyncCompletions.create = wrapped_async_create
53
+
54
+ self.patched = True
55
+ except ImportError:
56
+ pass
57
+
58
+ def restore(self) -> None:
59
+ if not self.patched:
60
+ return
61
+
62
+ try:
63
+ from openai.resources.chat.completions import Completions, AsyncCompletions
64
+ if self.original_create:
65
+ Completions.create = self.original_create
66
+ if self.original_async_create:
67
+ AsyncCompletions.create = self.original_async_create
68
+ self.patched = False
69
+ except ImportError:
70
+ pass
@@ -1,238 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from .base import CanonicalRequest, CanonicalResponse, ProviderAdapter
6
-
7
-
8
- class LangChainGroqAdapter(ProviderAdapter):
9
- """A generic chat-completions adapter that can be configured per provider."""
10
-
11
- def __init__(
12
- self, provider_name: str = "langchain_groq", response_object: str | None = None
13
- ) -> None:
14
- self.provider_name = provider_name
15
- self._response_object = response_object
16
-
17
- def to_canonical_request(self, request: Any, **kwargs: Any) -> CanonicalRequest:
18
- if isinstance(request, (tuple, list)) and len(request) > 0:
19
- first_arg = request[0]
20
- if hasattr(first_arg, "model_name"):
21
- payload = first_arg
22
- messages = request[1] if len(request) > 1 else []
23
- else:
24
- payload = first_arg
25
- messages = []
26
- else:
27
- payload = request or {}
28
- messages = []
29
-
30
- model = getattr(payload, "model_name", None) or kwargs.get("model")
31
- temperature = getattr(payload, "temperature", None) or kwargs.get("temperature")
32
-
33
- if isinstance(payload, dict):
34
- raw_payload = payload
35
- if not messages:
36
- messages = payload.get("messages") or kwargs.get("messages") or []
37
- else:
38
- raw_payload = {"input": payload}
39
- if not messages:
40
- messages = kwargs.get("messages") or []
41
-
42
- if isinstance(messages, (list, tuple)):
43
- messages_list = list(messages)
44
- elif messages:
45
- messages_list = [messages]
46
- else:
47
- messages_list = []
48
-
49
- return CanonicalRequest(
50
- provider=self.provider_name,
51
- model=model,
52
- temperature=temperature,
53
- messages=messages_list,
54
- params=kwargs,
55
- metadata={"raw_request": raw_payload},
56
- )
57
-
58
- def to_canonical_response(self, response: Any, **kwargs: Any) -> CanonicalResponse:
59
- payload = response or {}
60
- if hasattr(payload, "content") and not isinstance(payload, dict):
61
- content = getattr(payload, "content", None)
62
- return CanonicalResponse(
63
- provider=self.provider_name,
64
- output=content,
65
- model=payload.response_metadata.get("model_name"),
66
- tool_calls=payload.tool_calls,
67
- invalid_tool_calls=payload.invalid_tool_calls,
68
- latency=payload.response_metadata.get("total_time"),
69
- reasoning=(
70
- payload.response_metadata.get("token_usage", {}).get("reasoning_content")
71
- if payload.response_metadata.get("token_usage")
72
- else None
73
- ),
74
- usage=payload.usage_metadata,
75
- metadata={
76
- "raw_response": {
77
- "id": getattr(payload, "id", None),
78
- "response_metadata": getattr(payload, "response_metadata", {}),
79
- }
80
- },
81
- )
82
-
83
- choices = payload.get("choices", [])
84
- first_choice = choices[0] if choices else {}
85
- message = first_choice.get("message", {})
86
-
87
- return CanonicalResponse(
88
- provider=self.provider_name,
89
- model=kwargs.get("model"),
90
- output=message.get("content"),
91
- usage=payload.get("usage"),
92
- metadata={"raw_response": payload},
93
- )
94
-
95
- def from_canonical_response(self, response: CanonicalResponse, request: Any) -> Any:
96
- raw_resp = response.metadata.get("raw_response")
97
- resp_id = "replayed-response"
98
- finish_reason = "stop"
99
- if raw_resp:
100
- if isinstance(raw_resp, dict):
101
- resp_id = raw_resp.get("id", "replayed-response")
102
- choices = raw_resp.get("choices", [])
103
- if choices:
104
- finish_reason = choices[0].get("finish_reason", "stop")
105
- else:
106
- resp_id = getattr(raw_resp, "id", "replayed-response")
107
- response_metadata = getattr(raw_resp, "response_metadata", {})
108
- finish_reason = response_metadata.get("finish_reason", "stop")
109
-
110
- # Determine if we should return a LangChain AIMessage object
111
- is_langchain = False
112
- if request and isinstance(request, (tuple, list)) and len(request) > 0:
113
- first_arg = request[0]
114
- # If the class represents ChatGroq or a LangChain chat model
115
- if hasattr(first_arg, "model_name") or hasattr(first_arg, "invoke"):
116
- is_langchain = True
117
-
118
- if raw_resp and hasattr(raw_resp, "content") and not isinstance(raw_resp, dict):
119
- is_langchain = True
120
-
121
- if is_langchain:
122
- from langchain_core.messages import AIMessage
123
-
124
- response_metadata = {}
125
- if raw_resp:
126
- if isinstance(raw_resp, dict):
127
- response_metadata = dict(raw_resp.get("response_metadata", {}))
128
- elif hasattr(raw_resp, "response_metadata"):
129
- response_metadata = dict(getattr(raw_resp, "response_metadata", {}))
130
-
131
- if not response_metadata:
132
- response_metadata = {
133
- "model_name": response.model,
134
- "finish_reason": finish_reason,
135
- }
136
- if response.latency is not None:
137
- response_metadata["total_time"] = response.latency
138
-
139
- aimsg_kwargs: dict[str, Any] = {
140
- "content": response.output or "",
141
- "response_metadata": response_metadata,
142
- "id": resp_id,
143
- "tool_calls": response.tool_calls or [],
144
- "invalid_tool_calls": response.invalid_tool_calls or [],
145
- }
146
-
147
- if response.usage and isinstance(response.usage, dict):
148
- input_t = response.usage.get("input_tokens") or response.usage.get("prompt_tokens") or 0
149
- output_t = response.usage.get("output_tokens") or response.usage.get("completion_tokens") or 0
150
- total_t = response.usage.get("total_tokens") or (input_t + output_t)
151
- aimsg_kwargs["usage_metadata"] = {
152
- "input_tokens": input_t,
153
- "output_tokens": output_t,
154
- "total_tokens": total_t,
155
- }
156
-
157
- return AIMessage(**aimsg_kwargs)
158
-
159
- payload: dict[str, Any] = {
160
- "id": resp_id,
161
- "choices": [
162
- {
163
- "message": {
164
- "role": "assistant",
165
- "content": response.output,
166
- },
167
- "finish_reason": finish_reason,
168
- }
169
- ],
170
- "usage": response.usage or {},
171
- "model": response.model or "replayed-model",
172
- }
173
-
174
- if self._response_object is not None:
175
- payload["object"] = self._response_object
176
-
177
- return payload
178
-
179
-
180
- class OpenAIAdapter(ProviderAdapter):
181
- """An OpenAI chat completions adapter."""
182
-
183
- def __init__(self, response_object: str | None = "chat.completion") -> None:
184
- self.provider_name = "openai"
185
- self._response_object = response_object
186
-
187
- def to_canonical_request(self, request: Any, **kwargs: Any) -> CanonicalRequest:
188
- payload = request or {}
189
- messages = payload.get("messages", [])
190
- return CanonicalRequest(
191
- provider=self.provider_name,
192
- model=kwargs.get("model"),
193
- messages=list(messages),
194
- params=kwargs,
195
- )
196
-
197
- def to_canonical_response(self, response: Any, **kwargs: Any) -> CanonicalResponse:
198
- payload = response or {}
199
- choices = payload.get("choices", [])
200
- first_choice = choices[0] if choices else {}
201
- message = first_choice.get("message", {})
202
- return CanonicalResponse(
203
- provider=self.provider_name,
204
- model=kwargs.get("model"),
205
- output=message.get("content"),
206
- usage=payload.get("usage"),
207
- metadata={"raw_response": payload},
208
- )
209
-
210
- def from_canonical_response(self, response: CanonicalResponse, request: Any) -> Any:
211
- raw_resp = response.metadata.get("raw_response")
212
- resp_id = "replayed-response"
213
- finish_reason = "stop"
214
- if raw_resp and isinstance(raw_resp, dict):
215
- resp_id = raw_resp.get("id", "replayed-response")
216
- choices = raw_resp.get("choices", [])
217
- if choices:
218
- finish_reason = choices[0].get("finish_reason", "stop")
219
-
220
- payload: dict[str, Any] = {
221
- "id": resp_id,
222
- "choices": [
223
- {
224
- "message": {
225
- "role": "assistant",
226
- "content": response.output,
227
- },
228
- "finish_reason": finish_reason,
229
- }
230
- ],
231
- "usage": response.usage or {},
232
- "model": response.model or "replayed-model",
233
- }
234
-
235
- if self._response_object is not None:
236
- payload["object"] = self._response_object
237
-
238
- return payload
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes