livekit-plugins-anthropic 0.2.12__py3-none-any.whl → 1.0.0__py3-none-any.whl
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.
- livekit/plugins/anthropic/llm.py +120 -436
- livekit/plugins/anthropic/utils.py +147 -0
- livekit/plugins/anthropic/version.py +1 -1
- {livekit_plugins_anthropic-0.2.12.dist-info → livekit_plugins_anthropic-1.0.0.dist-info}/METADATA +11 -21
- livekit_plugins_anthropic-1.0.0.dist-info/RECORD +10 -0
- {livekit_plugins_anthropic-0.2.12.dist-info → livekit_plugins_anthropic-1.0.0.dist-info}/WHEEL +1 -2
- livekit_plugins_anthropic-0.2.12.dist-info/RECORD +0 -10
- livekit_plugins_anthropic-0.2.12.dist-info/top_level.txt +0 -1
livekit/plugins/anthropic/llm.py
CHANGED
@@ -14,56 +14,40 @@
|
|
14
14
|
|
15
15
|
from __future__ import annotations
|
16
16
|
|
17
|
-
import base64
|
18
|
-
import inspect
|
19
|
-
import json
|
20
17
|
import os
|
18
|
+
from collections.abc import Awaitable
|
21
19
|
from dataclasses import dataclass
|
22
|
-
from typing import
|
23
|
-
Any,
|
24
|
-
Awaitable,
|
25
|
-
List,
|
26
|
-
Literal,
|
27
|
-
Union,
|
28
|
-
cast,
|
29
|
-
get_args,
|
30
|
-
get_origin,
|
31
|
-
)
|
20
|
+
from typing import Any, Literal
|
32
21
|
|
33
22
|
import httpx
|
34
|
-
from livekit import rtc
|
35
|
-
from livekit.agents import (
|
36
|
-
APIConnectionError,
|
37
|
-
APIStatusError,
|
38
|
-
APITimeoutError,
|
39
|
-
llm,
|
40
|
-
utils,
|
41
|
-
)
|
42
|
-
from livekit.agents.llm import LLMCapabilities, ToolChoice
|
43
|
-
from livekit.agents.llm.function_context import (
|
44
|
-
_create_ai_function_info,
|
45
|
-
_is_optional_type,
|
46
|
-
)
|
47
|
-
from livekit.agents.types import DEFAULT_API_CONNECT_OPTIONS, APIConnectOptions
|
48
23
|
|
49
24
|
import anthropic
|
50
|
-
|
51
|
-
from .
|
52
|
-
from .
|
53
|
-
|
25
|
+
from livekit.agents import APIConnectionError, APIStatusError, APITimeoutError, llm
|
26
|
+
from livekit.agents.llm import ToolChoice
|
27
|
+
from livekit.agents.llm.chat_context import ChatContext
|
28
|
+
from livekit.agents.llm.tool_context import FunctionTool
|
29
|
+
from livekit.agents.types import (
|
30
|
+
DEFAULT_API_CONNECT_OPTIONS,
|
31
|
+
NOT_GIVEN,
|
32
|
+
APIConnectOptions,
|
33
|
+
NotGivenOr,
|
54
34
|
)
|
35
|
+
from livekit.agents.utils import is_given
|
55
36
|
|
56
|
-
|
37
|
+
from .models import ChatModels
|
38
|
+
from .utils import to_chat_ctx, to_fnc_ctx
|
57
39
|
|
58
40
|
|
59
41
|
@dataclass
|
60
|
-
class
|
42
|
+
class _LLMOptions:
|
61
43
|
model: str | ChatModels
|
62
|
-
user: str
|
63
|
-
temperature: float
|
64
|
-
parallel_tool_calls: bool
|
65
|
-
tool_choice:
|
66
|
-
caching: Literal["ephemeral"]
|
44
|
+
user: NotGivenOr[str]
|
45
|
+
temperature: NotGivenOr[float]
|
46
|
+
parallel_tool_calls: NotGivenOr[bool]
|
47
|
+
tool_choice: NotGivenOr[ToolChoice]
|
48
|
+
caching: NotGivenOr[Literal["ephemeral"]]
|
49
|
+
top_k: NotGivenOr[int]
|
50
|
+
max_tokens: NotGivenOr[int]
|
67
51
|
"""If set to "ephemeral", the system prompt, tools, and chat history will be cached."""
|
68
52
|
|
69
53
|
|
@@ -72,14 +56,16 @@ class LLM(llm.LLM):
|
|
72
56
|
self,
|
73
57
|
*,
|
74
58
|
model: str | ChatModels = "claude-3-5-sonnet-20241022",
|
75
|
-
api_key: str
|
76
|
-
base_url: str
|
77
|
-
user: str
|
59
|
+
api_key: NotGivenOr[str] = NOT_GIVEN,
|
60
|
+
base_url: NotGivenOr[str] = NOT_GIVEN,
|
61
|
+
user: NotGivenOr[str] = NOT_GIVEN,
|
78
62
|
client: anthropic.AsyncClient | None = None,
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
63
|
+
top_k: NotGivenOr[int] = NOT_GIVEN,
|
64
|
+
max_tokens: NotGivenOr[int] = NOT_GIVEN,
|
65
|
+
temperature: NotGivenOr[float] = NOT_GIVEN,
|
66
|
+
parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
|
67
|
+
tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
|
68
|
+
caching: NotGivenOr[Literal["ephemeral"]] = NOT_GIVEN,
|
83
69
|
) -> None:
|
84
70
|
"""
|
85
71
|
Create a new instance of Anthropic LLM.
|
@@ -88,39 +74,35 @@ class LLM(llm.LLM):
|
|
88
74
|
the ``ANTHROPIC_API_KEY`` environmental variable.
|
89
75
|
|
90
76
|
model (str | ChatModels): The model to use. Defaults to "claude-3-5-sonnet-20241022".
|
91
|
-
api_key (str
|
92
|
-
base_url (str
|
93
|
-
user (str
|
77
|
+
api_key (str, optional): The Anthropic API key. Defaults to the ANTHROPIC_API_KEY environment variable.
|
78
|
+
base_url (str, optional): The base URL for the Anthropic API. Defaults to None.
|
79
|
+
user (str, optional): The user for the Anthropic API. Defaults to None.
|
94
80
|
client (anthropic.AsyncClient | None): The Anthropic client to use. Defaults to None.
|
95
|
-
temperature (float
|
96
|
-
parallel_tool_calls (bool
|
97
|
-
tool_choice (
|
98
|
-
caching (Literal["ephemeral"]
|
99
|
-
"""
|
81
|
+
temperature (float, optional): The temperature for the Anthropic API. Defaults to None.
|
82
|
+
parallel_tool_calls (bool, optional): Whether to parallelize tool calls. Defaults to None.
|
83
|
+
tool_choice (ToolChoice, optional): The tool choice for the Anthropic API. Defaults to "auto".
|
84
|
+
caching (Literal["ephemeral"], optional): If set to "ephemeral", caching will be enabled for the system prompt, tools, and chat history.
|
85
|
+
""" # noqa: E501
|
100
86
|
|
101
|
-
super().__init__(
|
102
|
-
capabilities=LLMCapabilities(
|
103
|
-
requires_persistent_functions=True,
|
104
|
-
supports_choices_on_int=True,
|
105
|
-
)
|
106
|
-
)
|
87
|
+
super().__init__()
|
107
88
|
|
108
|
-
|
109
|
-
api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
110
|
-
if api_key is None:
|
111
|
-
raise ValueError("Anthropic API key is required")
|
112
|
-
|
113
|
-
self._opts = LLMOptions(
|
89
|
+
self._opts = _LLMOptions(
|
114
90
|
model=model,
|
115
91
|
user=user,
|
116
92
|
temperature=temperature,
|
117
93
|
parallel_tool_calls=parallel_tool_calls,
|
118
94
|
tool_choice=tool_choice,
|
119
95
|
caching=caching,
|
96
|
+
top_k=top_k,
|
97
|
+
max_tokens=max_tokens,
|
120
98
|
)
|
121
|
-
|
122
|
-
|
123
|
-
|
99
|
+
anthropic_api_key = api_key if is_given(api_key) else os.environ.get("ANTHROPIC_API_KEY")
|
100
|
+
if not anthropic_api_key:
|
101
|
+
raise ValueError("Anthropic API key is required")
|
102
|
+
|
103
|
+
self._client = anthropic.AsyncClient(
|
104
|
+
api_key=anthropic_api_key,
|
105
|
+
base_url=base_url if is_given(base_url) else None,
|
124
106
|
http_client=httpx.AsyncClient(
|
125
107
|
timeout=5.0,
|
126
108
|
follow_redirects=True,
|
@@ -135,88 +117,72 @@ class LLM(llm.LLM):
|
|
135
117
|
def chat(
|
136
118
|
self,
|
137
119
|
*,
|
138
|
-
chat_ctx:
|
120
|
+
chat_ctx: ChatContext,
|
121
|
+
tools: list[FunctionTool] | None = None,
|
139
122
|
conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
if
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
if
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
cache_ctrl = (
|
160
|
-
CACHE_CONTROL_EPHEMERAL
|
161
|
-
if (i == len(fnc_ctx.ai_functions) - 1)
|
162
|
-
and self._opts.caching == "ephemeral"
|
163
|
-
else None
|
164
|
-
)
|
165
|
-
fncs_desc.append(
|
166
|
-
_build_function_description(
|
167
|
-
fnc,
|
168
|
-
cache_ctrl=cache_ctrl,
|
169
|
-
)
|
170
|
-
)
|
123
|
+
parallel_tool_calls: NotGivenOr[bool] = NOT_GIVEN,
|
124
|
+
tool_choice: NotGivenOr[ToolChoice] = NOT_GIVEN,
|
125
|
+
extra_kwargs: NotGivenOr[dict[str, Any]] = NOT_GIVEN,
|
126
|
+
) -> LLMStream:
|
127
|
+
extra = {}
|
128
|
+
|
129
|
+
if is_given(extra_kwargs):
|
130
|
+
extra.update(extra_kwargs)
|
131
|
+
|
132
|
+
if is_given(self._opts.user):
|
133
|
+
extra["user"] = self._opts.user
|
134
|
+
|
135
|
+
if is_given(self._opts.temperature):
|
136
|
+
extra["temperature"] = self._opts.temperature
|
137
|
+
|
138
|
+
if is_given(self._opts.top_k):
|
139
|
+
extra["top_k"] = self._opts.top_k
|
140
|
+
|
141
|
+
extra["max_tokens"] = self._opts.max_tokens if is_given(self._opts.max_tokens) else 1024
|
171
142
|
|
172
|
-
|
173
|
-
|
143
|
+
if tools:
|
144
|
+
extra["tools"] = to_fnc_ctx(tools, self._opts.caching)
|
145
|
+
tool_choice = tool_choice if is_given(tool_choice) else self._opts.tool_choice
|
146
|
+
if is_given(tool_choice):
|
174
147
|
anthropic_tool_choice: dict[str, Any] | None = {"type": "auto"}
|
175
|
-
if isinstance(tool_choice,
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
}
|
148
|
+
if isinstance(tool_choice, dict) and tool_choice.get("type") == "function":
|
149
|
+
anthropic_tool_choice = {
|
150
|
+
"type": "tool",
|
151
|
+
"name": tool_choice["function"]["name"],
|
152
|
+
}
|
181
153
|
elif isinstance(tool_choice, str):
|
182
154
|
if tool_choice == "required":
|
183
155
|
anthropic_tool_choice = {"type": "any"}
|
184
156
|
elif tool_choice == "none":
|
185
|
-
|
157
|
+
extra["tools"] = []
|
186
158
|
anthropic_tool_choice = None
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
159
|
+
if anthropic_tool_choice is not None:
|
160
|
+
parallel_tool_calls = (
|
161
|
+
parallel_tool_calls
|
162
|
+
if is_given(parallel_tool_calls)
|
163
|
+
else self._opts.parallel_tool_calls
|
164
|
+
)
|
165
|
+
if is_given(parallel_tool_calls):
|
166
|
+
anthropic_tool_choice["disable_parallel_tool_use"] = not parallel_tool_calls
|
167
|
+
extra["tool_choice"] = anthropic_tool_choice
|
191
168
|
|
192
|
-
|
193
|
-
_latest_system_message(chat_ctx, caching=self._opts.caching)
|
194
|
-
)
|
195
|
-
if latest_system_message:
|
196
|
-
opts["system"] = [latest_system_message]
|
169
|
+
anthropic_ctx, system_message = to_chat_ctx(chat_ctx, id(self), caching=self._opts.caching)
|
197
170
|
|
198
|
-
|
199
|
-
|
200
|
-
id(self),
|
201
|
-
caching=self._opts.caching,
|
202
|
-
)
|
203
|
-
collaped_anthropic_ctx = _merge_messages(anthropic_ctx)
|
171
|
+
if system_message:
|
172
|
+
extra["system"] = [system_message]
|
204
173
|
|
205
174
|
stream = self._client.messages.create(
|
206
|
-
|
207
|
-
messages=collaped_anthropic_ctx,
|
175
|
+
messages=anthropic_ctx,
|
208
176
|
model=self._opts.model,
|
209
|
-
temperature=temperature or anthropic.NOT_GIVEN,
|
210
|
-
top_k=n or anthropic.NOT_GIVEN,
|
211
177
|
stream=True,
|
212
|
-
**
|
178
|
+
**extra,
|
213
179
|
)
|
214
180
|
|
215
181
|
return LLMStream(
|
216
182
|
self,
|
217
183
|
anthropic_stream=stream,
|
218
184
|
chat_ctx=chat_ctx,
|
219
|
-
|
185
|
+
tools=tools,
|
220
186
|
conn_options=conn_options,
|
221
187
|
)
|
222
188
|
|
@@ -226,16 +192,12 @@ class LLMStream(llm.LLMStream):
|
|
226
192
|
self,
|
227
193
|
llm: LLM,
|
228
194
|
*,
|
229
|
-
anthropic_stream: Awaitable[
|
230
|
-
anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]
|
231
|
-
],
|
195
|
+
anthropic_stream: Awaitable[anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]],
|
232
196
|
chat_ctx: llm.ChatContext,
|
233
|
-
|
197
|
+
tools: list[FunctionTool] | None,
|
234
198
|
conn_options: APIConnectOptions,
|
235
199
|
) -> None:
|
236
|
-
super().__init__(
|
237
|
-
llm, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx, conn_options=conn_options
|
238
|
-
)
|
200
|
+
super().__init__(llm, chat_ctx=chat_ctx, tools=tools, conn_options=conn_options)
|
239
201
|
self._awaitable_anthropic_stream = anthropic_stream
|
240
202
|
self._anthropic_stream: (
|
241
203
|
anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent] | None
|
@@ -268,7 +230,7 @@ class LLMStream(llm.LLMStream):
|
|
268
230
|
|
269
231
|
self._event_ch.send_nowait(
|
270
232
|
llm.ChatChunk(
|
271
|
-
|
233
|
+
id=self._request_id,
|
272
234
|
usage=llm.CompletionUsage(
|
273
235
|
completion_tokens=self._output_tokens,
|
274
236
|
prompt_tokens=self._input_tokens,
|
@@ -281,29 +243,25 @@ class LLMStream(llm.LLMStream):
|
|
281
243
|
),
|
282
244
|
)
|
283
245
|
)
|
284
|
-
except anthropic.APITimeoutError:
|
285
|
-
raise APITimeoutError(retryable=retryable)
|
246
|
+
except anthropic.APITimeoutError as e:
|
247
|
+
raise APITimeoutError(retryable=retryable) from e
|
286
248
|
except anthropic.APIStatusError as e:
|
287
249
|
raise APIStatusError(
|
288
250
|
e.message,
|
289
251
|
status_code=e.status_code,
|
290
252
|
request_id=e.request_id,
|
291
253
|
body=e.body,
|
292
|
-
)
|
254
|
+
) from e
|
293
255
|
except Exception as e:
|
294
256
|
raise APIConnectionError(retryable=retryable) from e
|
295
257
|
|
296
|
-
def _parse_event(
|
297
|
-
self, event: anthropic.types.RawMessageStreamEvent
|
298
|
-
) -> llm.ChatChunk | None:
|
258
|
+
def _parse_event(self, event: anthropic.types.RawMessageStreamEvent) -> llm.ChatChunk | None:
|
299
259
|
if event.type == "message_start":
|
300
260
|
self._request_id = event.message.id
|
301
261
|
self._input_tokens = event.message.usage.input_tokens
|
302
262
|
self._output_tokens = event.message.usage.output_tokens
|
303
263
|
if event.message.usage.cache_creation_input_tokens:
|
304
|
-
self._cache_creation_tokens =
|
305
|
-
event.message.usage.cache_creation_input_tokens
|
306
|
-
)
|
264
|
+
self._cache_creation_tokens = event.message.usage.cache_creation_input_tokens
|
307
265
|
if event.message.usage.cache_read_input_tokens:
|
308
266
|
self._cache_read_tokens = event.message.usage.cache_read_input_tokens
|
309
267
|
elif event.type == "message_delta":
|
@@ -318,7 +276,7 @@ class LLMStream(llm.LLMStream):
|
|
318
276
|
if delta.type == "text_delta":
|
319
277
|
text = delta.text
|
320
278
|
|
321
|
-
if self.
|
279
|
+
if self._tools is not None:
|
322
280
|
# anthropic may inject COC when using functions
|
323
281
|
if text.startswith("<thinking>"):
|
324
282
|
self._ignoring_cot = True
|
@@ -330,306 +288,32 @@ class LLMStream(llm.LLMStream):
|
|
330
288
|
return None
|
331
289
|
|
332
290
|
return llm.ChatChunk(
|
333
|
-
|
334
|
-
|
335
|
-
llm.Choice(
|
336
|
-
delta=llm.ChoiceDelta(content=text, role="assistant")
|
337
|
-
)
|
338
|
-
],
|
291
|
+
id=self._request_id,
|
292
|
+
delta=llm.ChoiceDelta(content=text, role="assistant"),
|
339
293
|
)
|
340
294
|
elif delta.type == "input_json_delta":
|
341
295
|
assert self._fnc_raw_arguments is not None
|
342
296
|
self._fnc_raw_arguments += delta.partial_json
|
343
297
|
|
344
298
|
elif event.type == "content_block_stop":
|
345
|
-
if self._tool_call_id is not None
|
299
|
+
if self._tool_call_id is not None:
|
346
300
|
assert self._fnc_name is not None
|
347
301
|
assert self._fnc_raw_arguments is not None
|
348
302
|
|
349
|
-
fnc_info = _create_ai_function_info(
|
350
|
-
self._fnc_ctx,
|
351
|
-
self._tool_call_id,
|
352
|
-
self._fnc_name,
|
353
|
-
self._fnc_raw_arguments,
|
354
|
-
)
|
355
|
-
self._function_calls_info.append(fnc_info)
|
356
|
-
|
357
303
|
chat_chunk = llm.ChatChunk(
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
304
|
+
id=self._request_id,
|
305
|
+
delta=llm.ChoiceDelta(
|
306
|
+
role="assistant",
|
307
|
+
tool_calls=[
|
308
|
+
llm.FunctionToolCall(
|
309
|
+
arguments=self._fnc_raw_arguments or "",
|
310
|
+
name=self._fnc_name or "",
|
311
|
+
call_id=self._tool_call_id or "",
|
312
|
+
)
|
313
|
+
],
|
314
|
+
),
|
366
315
|
)
|
367
316
|
self._tool_call_id = self._fnc_raw_arguments = self._fnc_name = None
|
368
317
|
return chat_chunk
|
369
318
|
|
370
319
|
return None
|
371
|
-
|
372
|
-
|
373
|
-
def _latest_system_message(
|
374
|
-
chat_ctx: llm.ChatContext, caching: Literal["ephemeral"] | None = None
|
375
|
-
) -> anthropic.types.TextBlockParam | None:
|
376
|
-
latest_system_message: llm.ChatMessage | None = None
|
377
|
-
for m in chat_ctx.messages:
|
378
|
-
if m.role == "system":
|
379
|
-
latest_system_message = m
|
380
|
-
continue
|
381
|
-
|
382
|
-
latest_system_str = ""
|
383
|
-
if latest_system_message:
|
384
|
-
if isinstance(latest_system_message.content, str):
|
385
|
-
latest_system_str = latest_system_message.content
|
386
|
-
elif isinstance(latest_system_message.content, list):
|
387
|
-
latest_system_str = " ".join(
|
388
|
-
[c for c in latest_system_message.content if isinstance(c, str)]
|
389
|
-
)
|
390
|
-
if latest_system_str:
|
391
|
-
system_text_block = anthropic.types.TextBlockParam(
|
392
|
-
text=latest_system_str,
|
393
|
-
type="text",
|
394
|
-
cache_control=CACHE_CONTROL_EPHEMERAL if caching == "ephemeral" else None,
|
395
|
-
)
|
396
|
-
return system_text_block
|
397
|
-
return None
|
398
|
-
|
399
|
-
|
400
|
-
def _merge_messages(
|
401
|
-
messages: List[anthropic.types.MessageParam],
|
402
|
-
) -> List[anthropic.types.MessageParam]:
|
403
|
-
# Anthropic enforces alternating messages
|
404
|
-
combined_messages: list[anthropic.types.MessageParam] = []
|
405
|
-
for m in messages:
|
406
|
-
if len(combined_messages) == 0 or m["role"] != combined_messages[-1]["role"]:
|
407
|
-
combined_messages.append(m)
|
408
|
-
continue
|
409
|
-
last_message = combined_messages[-1]
|
410
|
-
if not isinstance(last_message["content"], list) or not isinstance(
|
411
|
-
m["content"], list
|
412
|
-
):
|
413
|
-
logger.error("message content is not a list")
|
414
|
-
continue
|
415
|
-
|
416
|
-
last_message["content"].extend(m["content"])
|
417
|
-
|
418
|
-
if len(combined_messages) == 0 or combined_messages[0]["role"] != "user":
|
419
|
-
combined_messages.insert(
|
420
|
-
0, {"role": "user", "content": [{"type": "text", "text": "(empty)"}]}
|
421
|
-
)
|
422
|
-
|
423
|
-
return combined_messages
|
424
|
-
|
425
|
-
|
426
|
-
def _build_anthropic_context(
|
427
|
-
chat_ctx: List[llm.ChatMessage],
|
428
|
-
cache_key: Any,
|
429
|
-
caching: Literal["ephemeral"] | None,
|
430
|
-
) -> List[anthropic.types.MessageParam]:
|
431
|
-
result: List[anthropic.types.MessageParam] = []
|
432
|
-
for i, msg in enumerate(chat_ctx):
|
433
|
-
# caching last message will cache whole chat history if caching is enabled
|
434
|
-
cache_ctrl = (
|
435
|
-
CACHE_CONTROL_EPHEMERAL
|
436
|
-
if ((i == len(chat_ctx) - 1) and caching == "ephemeral")
|
437
|
-
else None
|
438
|
-
)
|
439
|
-
a_msg = _build_anthropic_message(msg, cache_key, cache_ctrl=cache_ctrl)
|
440
|
-
|
441
|
-
if a_msg:
|
442
|
-
result.append(a_msg)
|
443
|
-
return result
|
444
|
-
|
445
|
-
|
446
|
-
def _build_anthropic_message(
|
447
|
-
msg: llm.ChatMessage,
|
448
|
-
cache_key: Any,
|
449
|
-
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
|
450
|
-
) -> anthropic.types.MessageParam | None:
|
451
|
-
if msg.role == "user" or msg.role == "assistant":
|
452
|
-
a_msg: anthropic.types.MessageParam = {
|
453
|
-
"role": msg.role,
|
454
|
-
"content": [],
|
455
|
-
}
|
456
|
-
assert isinstance(a_msg["content"], list)
|
457
|
-
a_content = a_msg["content"]
|
458
|
-
|
459
|
-
# add content if provided
|
460
|
-
if isinstance(msg.content, str) and msg.content:
|
461
|
-
a_msg["content"].append(
|
462
|
-
anthropic.types.TextBlockParam(
|
463
|
-
text=msg.content,
|
464
|
-
type="text",
|
465
|
-
cache_control=cache_ctrl,
|
466
|
-
)
|
467
|
-
)
|
468
|
-
elif isinstance(msg.content, list):
|
469
|
-
for cnt in msg.content:
|
470
|
-
if isinstance(cnt, str) and cnt:
|
471
|
-
content: anthropic.types.TextBlockParam = (
|
472
|
-
anthropic.types.TextBlockParam(
|
473
|
-
text=cnt,
|
474
|
-
type="text",
|
475
|
-
cache_control=cache_ctrl,
|
476
|
-
)
|
477
|
-
)
|
478
|
-
a_content.append(content)
|
479
|
-
elif isinstance(cnt, llm.ChatImage):
|
480
|
-
a_content.append(
|
481
|
-
_build_anthropic_image_content(cnt, cache_key, cache_ctrl)
|
482
|
-
)
|
483
|
-
if msg.tool_calls is not None:
|
484
|
-
for fnc in msg.tool_calls:
|
485
|
-
tool_use = anthropic.types.ToolUseBlockParam(
|
486
|
-
id=fnc.tool_call_id,
|
487
|
-
type="tool_use",
|
488
|
-
name=fnc.function_info.name,
|
489
|
-
input=fnc.arguments,
|
490
|
-
cache_control=cache_ctrl,
|
491
|
-
)
|
492
|
-
a_content.append(tool_use)
|
493
|
-
|
494
|
-
return a_msg
|
495
|
-
elif msg.role == "tool":
|
496
|
-
if isinstance(msg.content, dict):
|
497
|
-
msg.content = json.dumps(msg.content)
|
498
|
-
if not isinstance(msg.content, str):
|
499
|
-
logger.warning("tool message content is not a string or dict")
|
500
|
-
return None
|
501
|
-
if not msg.tool_call_id:
|
502
|
-
return None
|
503
|
-
|
504
|
-
u_content = anthropic.types.ToolResultBlockParam(
|
505
|
-
tool_use_id=msg.tool_call_id,
|
506
|
-
type="tool_result",
|
507
|
-
content=msg.content,
|
508
|
-
is_error=msg.tool_exception is not None,
|
509
|
-
cache_control=cache_ctrl,
|
510
|
-
)
|
511
|
-
return {
|
512
|
-
"role": "user",
|
513
|
-
"content": [u_content],
|
514
|
-
}
|
515
|
-
|
516
|
-
return None
|
517
|
-
|
518
|
-
|
519
|
-
def _build_anthropic_image_content(
|
520
|
-
image: llm.ChatImage,
|
521
|
-
cache_key: Any,
|
522
|
-
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
|
523
|
-
) -> anthropic.types.ImageBlockParam:
|
524
|
-
if isinstance(image.image, str): # image is a URL
|
525
|
-
if not image.image.startswith("data:"):
|
526
|
-
raise ValueError("LiveKit Anthropic Plugin: Image URLs must be data URLs")
|
527
|
-
|
528
|
-
try:
|
529
|
-
header, b64_data = image.image.split(",", 1)
|
530
|
-
media_type = header.split(";")[0].split(":")[1]
|
531
|
-
|
532
|
-
supported_types = {"image/jpeg", "image/png", "image/webp", "image/gif"}
|
533
|
-
if media_type not in supported_types:
|
534
|
-
raise ValueError(
|
535
|
-
f"LiveKit Anthropic Plugin: Unsupported media type {media_type}. Must be jpeg, png, webp, or gif"
|
536
|
-
)
|
537
|
-
|
538
|
-
return {
|
539
|
-
"type": "image",
|
540
|
-
"source": {
|
541
|
-
"type": "base64",
|
542
|
-
"data": b64_data,
|
543
|
-
"media_type": cast(
|
544
|
-
Literal["image/jpeg", "image/png", "image/gif", "image/webp"],
|
545
|
-
media_type,
|
546
|
-
),
|
547
|
-
},
|
548
|
-
"cache_control": cache_ctrl,
|
549
|
-
}
|
550
|
-
except (ValueError, IndexError) as e:
|
551
|
-
raise ValueError(
|
552
|
-
f"LiveKit Anthropic Plugin: Invalid image data URL {str(e)}"
|
553
|
-
)
|
554
|
-
elif isinstance(image.image, rtc.VideoFrame): # image is a VideoFrame
|
555
|
-
if cache_key not in image._cache:
|
556
|
-
# inside our internal implementation, we allow to put extra metadata to
|
557
|
-
# each ChatImage (avoid to reencode each time we do a chatcompletion request)
|
558
|
-
opts = utils.images.EncodeOptions()
|
559
|
-
if image.inference_width and image.inference_height:
|
560
|
-
opts.resize_options = utils.images.ResizeOptions(
|
561
|
-
width=image.inference_width,
|
562
|
-
height=image.inference_height,
|
563
|
-
strategy="scale_aspect_fit",
|
564
|
-
)
|
565
|
-
|
566
|
-
encoded_data = utils.images.encode(image.image, opts)
|
567
|
-
image._cache[cache_key] = base64.b64encode(encoded_data).decode("utf-8")
|
568
|
-
|
569
|
-
return {
|
570
|
-
"type": "image",
|
571
|
-
"source": {
|
572
|
-
"type": "base64",
|
573
|
-
"data": image._cache[cache_key],
|
574
|
-
"media_type": "image/jpeg",
|
575
|
-
},
|
576
|
-
"cache_control": cache_ctrl,
|
577
|
-
}
|
578
|
-
|
579
|
-
raise ValueError(
|
580
|
-
"LiveKit Anthropic Plugin: ChatImage must be an rtc.VideoFrame or a data URL"
|
581
|
-
)
|
582
|
-
|
583
|
-
|
584
|
-
def _build_function_description(
|
585
|
-
fnc_info: llm.function_context.FunctionInfo,
|
586
|
-
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
|
587
|
-
) -> anthropic.types.ToolParam:
|
588
|
-
def build_schema_field(arg_info: llm.function_context.FunctionArgInfo):
|
589
|
-
def type2str(t: type) -> str:
|
590
|
-
if t is str:
|
591
|
-
return "string"
|
592
|
-
elif t in (int, float):
|
593
|
-
return "number"
|
594
|
-
elif t is bool:
|
595
|
-
return "boolean"
|
596
|
-
|
597
|
-
raise ValueError(f"unsupported type {t} for ai_property")
|
598
|
-
|
599
|
-
p: dict[str, Any] = {}
|
600
|
-
if arg_info.default is inspect.Parameter.empty:
|
601
|
-
p["required"] = True
|
602
|
-
else:
|
603
|
-
p["required"] = False
|
604
|
-
|
605
|
-
if arg_info.description:
|
606
|
-
p["description"] = arg_info.description
|
607
|
-
|
608
|
-
_, inner_th = _is_optional_type(arg_info.type)
|
609
|
-
|
610
|
-
if get_origin(inner_th) is list:
|
611
|
-
inner_type = get_args(inner_th)[0]
|
612
|
-
p["type"] = "array"
|
613
|
-
p["items"] = {}
|
614
|
-
p["items"]["type"] = type2str(inner_type)
|
615
|
-
|
616
|
-
if arg_info.choices:
|
617
|
-
p["items"]["enum"] = arg_info.choices
|
618
|
-
else:
|
619
|
-
p["type"] = type2str(inner_th)
|
620
|
-
if arg_info.choices:
|
621
|
-
p["enum"] = arg_info.choices
|
622
|
-
|
623
|
-
return p
|
624
|
-
|
625
|
-
input_schema: dict[str, object] = {"type": "object"}
|
626
|
-
|
627
|
-
for arg_info in fnc_info.arguments.values():
|
628
|
-
input_schema[arg_info.name] = build_schema_field(arg_info)
|
629
|
-
|
630
|
-
return anthropic.types.ToolParam(
|
631
|
-
name=fnc_info.name,
|
632
|
-
description=fnc_info.description,
|
633
|
-
input_schema=input_schema,
|
634
|
-
cache_control=cache_ctrl,
|
635
|
-
)
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import base64
|
2
|
+
import json
|
3
|
+
from typing import Any, Literal
|
4
|
+
|
5
|
+
import anthropic
|
6
|
+
from livekit.agents import llm
|
7
|
+
from livekit.agents.llm import FunctionTool
|
8
|
+
|
9
|
+
CACHE_CONTROL_EPHEMERAL = anthropic.types.CacheControlEphemeralParam(type="ephemeral")
|
10
|
+
|
11
|
+
__all__ = ["to_fnc_ctx", "to_chat_ctx"]
|
12
|
+
|
13
|
+
|
14
|
+
def to_fnc_ctx(
|
15
|
+
fncs: list[FunctionTool], caching: Literal["ephemeral"] | None
|
16
|
+
) -> list[anthropic.types.ToolParam]:
|
17
|
+
tools: list[anthropic.types.ToolParam] = []
|
18
|
+
for i, fnc in enumerate(fncs):
|
19
|
+
cache_ctrl = (
|
20
|
+
CACHE_CONTROL_EPHEMERAL if (i == len(fncs) - 1) and caching == "ephemeral" else None
|
21
|
+
)
|
22
|
+
tools.append(_build_anthropic_schema(fnc, cache_ctrl=cache_ctrl))
|
23
|
+
|
24
|
+
return tools
|
25
|
+
|
26
|
+
|
27
|
+
def to_chat_ctx(
|
28
|
+
chat_ctx: llm.ChatContext,
|
29
|
+
cache_key: Any,
|
30
|
+
caching: Literal["ephemeral"] | None,
|
31
|
+
) -> list[anthropic.types.MessageParam]:
|
32
|
+
messages: list[anthropic.types.MessageParam] = []
|
33
|
+
system_message: anthropic.types.TextBlockParam | None = None
|
34
|
+
current_role: str | None = None
|
35
|
+
content: list[anthropic.types.TextBlockParam] = []
|
36
|
+
for i, msg in enumerate(chat_ctx.items):
|
37
|
+
if msg.type == "message" and msg.role == "system":
|
38
|
+
for content in msg.content:
|
39
|
+
if content and isinstance(content, str):
|
40
|
+
system_message = anthropic.types.TextBlockParam(
|
41
|
+
text=content,
|
42
|
+
type="text",
|
43
|
+
cache_control=CACHE_CONTROL_EPHEMERAL if caching == "ephemeral" else None,
|
44
|
+
)
|
45
|
+
continue
|
46
|
+
|
47
|
+
cache_ctrl = (
|
48
|
+
CACHE_CONTROL_EPHEMERAL
|
49
|
+
if (i == len(chat_ctx.items) - 1) and caching == "ephemeral"
|
50
|
+
else None
|
51
|
+
)
|
52
|
+
if msg.type == "message":
|
53
|
+
role = "assistant" if msg.role == "assistant" else "user"
|
54
|
+
elif msg.type == "function_call":
|
55
|
+
role = "assistant"
|
56
|
+
elif msg.type == "function_call_output":
|
57
|
+
role = "user"
|
58
|
+
|
59
|
+
if role != current_role:
|
60
|
+
if current_role is not None and content:
|
61
|
+
messages.append(anthropic.types.MessageParam(role=current_role, content=content))
|
62
|
+
content = []
|
63
|
+
current_role = role
|
64
|
+
|
65
|
+
if msg.type == "message":
|
66
|
+
for c in msg.content:
|
67
|
+
if c and isinstance(c, str):
|
68
|
+
content.append(
|
69
|
+
anthropic.types.TextBlockParam(
|
70
|
+
text=c, type="text", cache_control=cache_ctrl
|
71
|
+
)
|
72
|
+
)
|
73
|
+
elif isinstance(c, llm.ImageContent):
|
74
|
+
content.append(_to_image_content(c, cache_key, cache_ctrl=cache_ctrl))
|
75
|
+
elif msg.type == "function_call":
|
76
|
+
content.append(
|
77
|
+
anthropic.types.ToolUseBlockParam(
|
78
|
+
id=msg.call_id,
|
79
|
+
type="tool_use",
|
80
|
+
name=msg.name,
|
81
|
+
input=json.loads(msg.arguments or "{}"),
|
82
|
+
cache_control=cache_ctrl,
|
83
|
+
)
|
84
|
+
)
|
85
|
+
elif msg.type == "function_call_output":
|
86
|
+
content.append(
|
87
|
+
anthropic.types.ToolResultBlockParam(
|
88
|
+
tool_use_id=msg.call_id,
|
89
|
+
type="tool_result",
|
90
|
+
content=msg.output,
|
91
|
+
cache_control=cache_ctrl,
|
92
|
+
)
|
93
|
+
)
|
94
|
+
|
95
|
+
if current_role is not None and content:
|
96
|
+
messages.append(anthropic.types.MessageParam(role=current_role, content=content))
|
97
|
+
|
98
|
+
# ensure the messages starts with a "user" message
|
99
|
+
if not messages or messages[0]["role"] != "user":
|
100
|
+
messages.insert(
|
101
|
+
0,
|
102
|
+
anthropic.types.MessageParam(
|
103
|
+
role="user",
|
104
|
+
content=[anthropic.types.TextBlockParam(text="(empty)", type="text")],
|
105
|
+
),
|
106
|
+
)
|
107
|
+
|
108
|
+
return messages, system_message
|
109
|
+
|
110
|
+
|
111
|
+
def _to_image_content(
|
112
|
+
image: llm.ImageContent,
|
113
|
+
cache_key: Any,
|
114
|
+
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
|
115
|
+
) -> anthropic.types.ImageBlockParam:
|
116
|
+
img = llm.utils.serialize_image(image)
|
117
|
+
if img.external_url:
|
118
|
+
return {
|
119
|
+
"type": "image",
|
120
|
+
"source": {"type": "url", "url": img.external_url},
|
121
|
+
"cache_control": cache_ctrl,
|
122
|
+
}
|
123
|
+
if cache_key not in image._cache:
|
124
|
+
image._cache[cache_key] = img.data_bytes
|
125
|
+
b64_data = base64.b64encode(image._cache[cache_key]).decode("utf-8")
|
126
|
+
return {
|
127
|
+
"type": "image",
|
128
|
+
"source": {
|
129
|
+
"type": "base64",
|
130
|
+
"data": f"data:{img.mime_type};base64,{b64_data}",
|
131
|
+
"media_type": img.mime_type,
|
132
|
+
},
|
133
|
+
"cache_control": cache_ctrl,
|
134
|
+
}
|
135
|
+
|
136
|
+
|
137
|
+
def _build_anthropic_schema(
|
138
|
+
function_tool: FunctionTool,
|
139
|
+
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None = None,
|
140
|
+
) -> anthropic.types.ToolParam:
|
141
|
+
fnc = llm.utils.build_legacy_openai_schema(function_tool, internally_tagged=True)
|
142
|
+
return anthropic.types.ToolParam(
|
143
|
+
name=fnc["name"],
|
144
|
+
description=fnc["description"] or "",
|
145
|
+
input_schema=fnc["parameters"],
|
146
|
+
cache_control=cache_ctrl,
|
147
|
+
)
|
{livekit_plugins_anthropic-0.2.12.dist-info → livekit_plugins_anthropic-1.0.0.dist-info}/METADATA
RENAMED
@@ -1,36 +1,26 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: livekit-plugins-anthropic
|
3
|
-
Version: 0.
|
3
|
+
Version: 1.0.0
|
4
4
|
Summary: Agent Framework plugin for services from Anthropic
|
5
|
-
Home-page: https://github.com/livekit/agents
|
6
|
-
License: Apache-2.0
|
7
5
|
Project-URL: Documentation, https://docs.livekit.io
|
8
6
|
Project-URL: Website, https://livekit.io/
|
9
7
|
Project-URL: Source, https://github.com/livekit/agents
|
10
|
-
|
8
|
+
Author-email: LiveKit <hello@livekit.io>
|
9
|
+
License-Expression: Apache-2.0
|
10
|
+
Keywords: audio,livekit,realtime,video,webrtc
|
11
11
|
Classifier: Intended Audience :: Developers
|
12
12
|
Classifier: License :: OSI Approved :: Apache Software License
|
13
|
-
Classifier: Topic :: Multimedia :: Sound/Audio
|
14
|
-
Classifier: Topic :: Multimedia :: Video
|
15
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
16
13
|
Classifier: Programming Language :: Python :: 3
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
17
15
|
Classifier: Programming Language :: Python :: 3.9
|
18
16
|
Classifier: Programming Language :: Python :: 3.10
|
19
|
-
Classifier:
|
17
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
18
|
+
Classifier: Topic :: Multimedia :: Video
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
20
20
|
Requires-Python: >=3.9.0
|
21
|
-
Description-Content-Type: text/markdown
|
22
|
-
Requires-Dist: livekit-agents>=0.12.3
|
23
21
|
Requires-Dist: anthropic>=0.34
|
24
|
-
|
25
|
-
|
26
|
-
Dynamic: description-content-type
|
27
|
-
Dynamic: home-page
|
28
|
-
Dynamic: keywords
|
29
|
-
Dynamic: license
|
30
|
-
Dynamic: project-url
|
31
|
-
Dynamic: requires-dist
|
32
|
-
Dynamic: requires-python
|
33
|
-
Dynamic: summary
|
22
|
+
Requires-Dist: livekit-agents>=1.0.0
|
23
|
+
Description-Content-Type: text/markdown
|
34
24
|
|
35
25
|
# LiveKit Plugins Anthropic
|
36
26
|
|
@@ -0,0 +1,10 @@
|
|
1
|
+
livekit/plugins/anthropic/__init__.py,sha256=1WCyNEaR6qBsX54qJQM0SeY-QHIucww16PLXcSnMqRo,1175
|
2
|
+
livekit/plugins/anthropic/llm.py,sha256=0O0ed5GZsTrEy_tWgrVadbA9IaEZfBm-oKjicT69l34,12885
|
3
|
+
livekit/plugins/anthropic/log.py,sha256=fG1pYSY88AnT738gZrmzF9FO4l4BdGENj3VKHMQB3Yo,72
|
4
|
+
livekit/plugins/anthropic/models.py,sha256=wyTr2nl6SL4ylN6s4mHJcqtmgV2mjJysZo89FknWdhI,213
|
5
|
+
livekit/plugins/anthropic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
livekit/plugins/anthropic/utils.py,sha256=Nfl9dGCZGDEJAHj_f-TmePr8bKJrc8IwM6Houjev4DE,5158
|
7
|
+
livekit/plugins/anthropic/version.py,sha256=nW89L_U9N4ukT3wAO3BeTqOaa87zLUOsEFz8TkiKIP8,600
|
8
|
+
livekit_plugins_anthropic-1.0.0.dist-info/METADATA,sha256=sL54QxwWIz99WcPxcrJfxbppUN7eDknJguwU_-8Y_sw,1271
|
9
|
+
livekit_plugins_anthropic-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
livekit_plugins_anthropic-1.0.0.dist-info/RECORD,,
|
@@ -1,10 +0,0 @@
|
|
1
|
-
livekit/plugins/anthropic/__init__.py,sha256=1WCyNEaR6qBsX54qJQM0SeY-QHIucww16PLXcSnMqRo,1175
|
2
|
-
livekit/plugins/anthropic/llm.py,sha256=dtIA1qWxMPWFxG4QbAeQ-xztmJZxRxBzYxqLFty59dA,23374
|
3
|
-
livekit/plugins/anthropic/log.py,sha256=fG1pYSY88AnT738gZrmzF9FO4l4BdGENj3VKHMQB3Yo,72
|
4
|
-
livekit/plugins/anthropic/models.py,sha256=wyTr2nl6SL4ylN6s4mHJcqtmgV2mjJysZo89FknWdhI,213
|
5
|
-
livekit/plugins/anthropic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
-
livekit/plugins/anthropic/version.py,sha256=L9v54yZpBzq0Hizz-thIscGPt87Ydvf01GZclaT0Yuw,601
|
7
|
-
livekit_plugins_anthropic-0.2.12.dist-info/METADATA,sha256=ZVjHKrbkK6a81xnO_oH-6-8gBrX_R_esQqJb15s_lhI,1481
|
8
|
-
livekit_plugins_anthropic-0.2.12.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
9
|
-
livekit_plugins_anthropic-0.2.12.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
|
10
|
-
livekit_plugins_anthropic-0.2.12.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
livekit
|