livekit-plugins-anthropic 0.2.13__py3-none-any.whl → 1.0.0.dev5__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 +107 -422
- livekit/plugins/anthropic/utils.py +140 -0
- livekit/plugins/anthropic/version.py +1 -1
- {livekit_plugins_anthropic-0.2.13.dist-info → livekit_plugins_anthropic-1.0.0.dev5.dist-info}/METADATA +11 -21
- livekit_plugins_anthropic-1.0.0.dev5.dist-info/RECORD +10 -0
- {livekit_plugins_anthropic-0.2.13.dist-info → livekit_plugins_anthropic-1.0.0.dev5.dist-info}/WHEEL +1 -2
- livekit_plugins_anthropic-0.2.13.dist-info/RECORD +0 -10
- livekit_plugins_anthropic-0.2.13.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 | Literal["auto", "required", "none"]]
|
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 | Literal["auto", "required", "none"]] = NOT_GIVEN,
|
68
|
+
caching: NotGivenOr[Literal["ephemeral"]] = NOT_GIVEN,
|
83
69
|
) -> None:
|
84
70
|
"""
|
85
71
|
Create a new instance of Anthropic LLM.
|
@@ -98,29 +84,25 @@ class LLM(llm.LLM):
|
|
98
84
|
caching (Literal["ephemeral"] | None): If set to "ephemeral", caching will be enabled for the system prompt, tools, and chat history.
|
99
85
|
"""
|
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
|
+
api_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
|
100
|
+
if not is_given(api_key):
|
101
|
+
raise ValueError("Anthropic API key is required")
|
102
|
+
|
103
|
+
self._client = anthropic.AsyncClient(
|
104
|
+
api_key=api_key or None,
|
105
|
+
base_url=base_url or None,
|
124
106
|
http_client=httpx.AsyncClient(
|
125
107
|
timeout=5.0,
|
126
108
|
follow_redirects=True,
|
@@ -135,42 +117,33 @@ 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
|
-
for i, fnc in enumerate(fnc_ctx.ai_functions.values()):
|
158
|
-
# caching last tool will cache all the tools if caching is enabled
|
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 | Literal["auto", "required", "none"]] = 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
|
171
140
|
|
172
|
-
|
173
|
-
|
141
|
+
extra["max_tokens"] = self._opts.max_tokens if is_given(self._opts.max_tokens) else 1024
|
142
|
+
|
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
148
|
if isinstance(tool_choice, ToolChoice):
|
176
149
|
if tool_choice.type == "function":
|
@@ -182,41 +155,35 @@ class LLM(llm.LLM):
|
|
182
155
|
if tool_choice == "required":
|
183
156
|
anthropic_tool_choice = {"type": "any"}
|
184
157
|
elif tool_choice == "none":
|
185
|
-
|
158
|
+
extra["tools"] = []
|
186
159
|
anthropic_tool_choice = None
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
160
|
+
if anthropic_tool_choice is not None:
|
161
|
+
parallel_tool_calls = (
|
162
|
+
parallel_tool_calls
|
163
|
+
if is_given(parallel_tool_calls)
|
164
|
+
else self._opts.parallel_tool_calls
|
165
|
+
)
|
166
|
+
if is_given(parallel_tool_calls):
|
167
|
+
anthropic_tool_choice["disable_parallel_tool_use"] = not parallel_tool_calls
|
168
|
+
extra["tool_choice"] = anthropic_tool_choice
|
191
169
|
|
192
|
-
|
193
|
-
_latest_system_message(chat_ctx, caching=self._opts.caching)
|
194
|
-
)
|
195
|
-
if latest_system_message:
|
196
|
-
opts["system"] = [latest_system_message]
|
170
|
+
anthropic_ctx, system_message = to_chat_ctx(chat_ctx, id(self), caching=self._opts.caching)
|
197
171
|
|
198
|
-
|
199
|
-
|
200
|
-
id(self),
|
201
|
-
caching=self._opts.caching,
|
202
|
-
)
|
203
|
-
collaped_anthropic_ctx = _merge_messages(anthropic_ctx)
|
172
|
+
if system_message:
|
173
|
+
extra["system"] = [system_message]
|
204
174
|
|
205
175
|
stream = self._client.messages.create(
|
206
|
-
|
207
|
-
messages=collaped_anthropic_ctx,
|
176
|
+
messages=anthropic_ctx,
|
208
177
|
model=self._opts.model,
|
209
|
-
temperature=temperature or anthropic.NOT_GIVEN,
|
210
|
-
top_k=n or anthropic.NOT_GIVEN,
|
211
178
|
stream=True,
|
212
|
-
**
|
179
|
+
**extra,
|
213
180
|
)
|
214
181
|
|
215
182
|
return LLMStream(
|
216
183
|
self,
|
217
184
|
anthropic_stream=stream,
|
218
185
|
chat_ctx=chat_ctx,
|
219
|
-
|
186
|
+
tools=tools,
|
220
187
|
conn_options=conn_options,
|
221
188
|
)
|
222
189
|
|
@@ -226,16 +193,12 @@ class LLMStream(llm.LLMStream):
|
|
226
193
|
self,
|
227
194
|
llm: LLM,
|
228
195
|
*,
|
229
|
-
anthropic_stream: Awaitable[
|
230
|
-
anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]
|
231
|
-
],
|
196
|
+
anthropic_stream: Awaitable[anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent]],
|
232
197
|
chat_ctx: llm.ChatContext,
|
233
|
-
|
198
|
+
tools: list[FunctionTool] | None,
|
234
199
|
conn_options: APIConnectOptions,
|
235
200
|
) -> None:
|
236
|
-
super().__init__(
|
237
|
-
llm, chat_ctx=chat_ctx, fnc_ctx=fnc_ctx, conn_options=conn_options
|
238
|
-
)
|
201
|
+
super().__init__(llm, chat_ctx=chat_ctx, tools=tools, conn_options=conn_options)
|
239
202
|
self._awaitable_anthropic_stream = anthropic_stream
|
240
203
|
self._anthropic_stream: (
|
241
204
|
anthropic.AsyncStream[anthropic.types.RawMessageStreamEvent] | None
|
@@ -268,7 +231,7 @@ class LLMStream(llm.LLMStream):
|
|
268
231
|
|
269
232
|
self._event_ch.send_nowait(
|
270
233
|
llm.ChatChunk(
|
271
|
-
|
234
|
+
id=self._request_id,
|
272
235
|
usage=llm.CompletionUsage(
|
273
236
|
completion_tokens=self._output_tokens,
|
274
237
|
prompt_tokens=self._input_tokens,
|
@@ -281,29 +244,25 @@ class LLMStream(llm.LLMStream):
|
|
281
244
|
),
|
282
245
|
)
|
283
246
|
)
|
284
|
-
except anthropic.APITimeoutError:
|
285
|
-
raise APITimeoutError(retryable=retryable)
|
247
|
+
except anthropic.APITimeoutError as e:
|
248
|
+
raise APITimeoutError(retryable=retryable) from e
|
286
249
|
except anthropic.APIStatusError as e:
|
287
250
|
raise APIStatusError(
|
288
251
|
e.message,
|
289
252
|
status_code=e.status_code,
|
290
253
|
request_id=e.request_id,
|
291
254
|
body=e.body,
|
292
|
-
)
|
255
|
+
) from e
|
293
256
|
except Exception as e:
|
294
257
|
raise APIConnectionError(retryable=retryable) from e
|
295
258
|
|
296
|
-
def _parse_event(
|
297
|
-
self, event: anthropic.types.RawMessageStreamEvent
|
298
|
-
) -> llm.ChatChunk | None:
|
259
|
+
def _parse_event(self, event: anthropic.types.RawMessageStreamEvent) -> llm.ChatChunk | None:
|
299
260
|
if event.type == "message_start":
|
300
261
|
self._request_id = event.message.id
|
301
262
|
self._input_tokens = event.message.usage.input_tokens
|
302
263
|
self._output_tokens = event.message.usage.output_tokens
|
303
264
|
if event.message.usage.cache_creation_input_tokens:
|
304
|
-
self._cache_creation_tokens =
|
305
|
-
event.message.usage.cache_creation_input_tokens
|
306
|
-
)
|
265
|
+
self._cache_creation_tokens = event.message.usage.cache_creation_input_tokens
|
307
266
|
if event.message.usage.cache_read_input_tokens:
|
308
267
|
self._cache_read_tokens = event.message.usage.cache_read_input_tokens
|
309
268
|
elif event.type == "message_delta":
|
@@ -318,7 +277,7 @@ class LLMStream(llm.LLMStream):
|
|
318
277
|
if delta.type == "text_delta":
|
319
278
|
text = delta.text
|
320
279
|
|
321
|
-
if self.
|
280
|
+
if self._tools is not None:
|
322
281
|
# anthropic may inject COC when using functions
|
323
282
|
if text.startswith("<thinking>"):
|
324
283
|
self._ignoring_cot = True
|
@@ -330,306 +289,32 @@ class LLMStream(llm.LLMStream):
|
|
330
289
|
return None
|
331
290
|
|
332
291
|
return llm.ChatChunk(
|
333
|
-
|
334
|
-
|
335
|
-
llm.Choice(
|
336
|
-
delta=llm.ChoiceDelta(content=text, role="assistant")
|
337
|
-
)
|
338
|
-
],
|
292
|
+
id=self._request_id,
|
293
|
+
delta=llm.ChoiceDelta(content=text, role="assistant"),
|
339
294
|
)
|
340
295
|
elif delta.type == "input_json_delta":
|
341
296
|
assert self._fnc_raw_arguments is not None
|
342
297
|
self._fnc_raw_arguments += delta.partial_json
|
343
298
|
|
344
299
|
elif event.type == "content_block_stop":
|
345
|
-
if self._tool_call_id is not None
|
300
|
+
if self._tool_call_id is not None:
|
346
301
|
assert self._fnc_name is not None
|
347
302
|
assert self._fnc_raw_arguments is not None
|
348
303
|
|
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
304
|
chat_chunk = llm.ChatChunk(
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
305
|
+
id=self._request_id,
|
306
|
+
delta=llm.ChoiceDelta(
|
307
|
+
role="assistant",
|
308
|
+
tool_calls=[
|
309
|
+
llm.FunctionToolCall(
|
310
|
+
arguments=self._fnc_raw_arguments or "",
|
311
|
+
name=self._fnc_name or "",
|
312
|
+
call_id=self._tool_call_id or "",
|
313
|
+
)
|
314
|
+
],
|
315
|
+
),
|
366
316
|
)
|
367
317
|
self._tool_call_id = self._fnc_raw_arguments = self._fnc_name = None
|
368
318
|
return chat_chunk
|
369
319
|
|
370
320
|
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,140 @@
|
|
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 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 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", content=[anthropic.types.TextBlockParam(text="(empty)", type="text")]
|
104
|
+
),
|
105
|
+
)
|
106
|
+
|
107
|
+
return messages, system_message
|
108
|
+
|
109
|
+
|
110
|
+
def _to_image_content(
|
111
|
+
image: llm.ImageContent,
|
112
|
+
cache_key: Any,
|
113
|
+
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None,
|
114
|
+
) -> anthropic.types.ImageBlockParam:
|
115
|
+
img = llm.utils.serialize_image(image)
|
116
|
+
if cache_key not in image._cache:
|
117
|
+
image._cache[cache_key] = img.data_bytes
|
118
|
+
b64_data = base64.b64encode(image._cache[cache_key]).decode("utf-8")
|
119
|
+
return {
|
120
|
+
"type": "image",
|
121
|
+
"source": {
|
122
|
+
"type": "base64",
|
123
|
+
"data": f"data:{img.media_type};base64,{b64_data}",
|
124
|
+
"media_type": img.media_type,
|
125
|
+
},
|
126
|
+
"cache_control": cache_ctrl,
|
127
|
+
}
|
128
|
+
|
129
|
+
|
130
|
+
def _build_anthropic_schema(
|
131
|
+
function_tool: FunctionTool,
|
132
|
+
cache_ctrl: anthropic.types.CacheControlEphemeralParam | None = None,
|
133
|
+
) -> anthropic.types.ToolParam:
|
134
|
+
fnc = llm.utils.build_legacy_openai_schema(function_tool, internally_tagged=True)
|
135
|
+
return anthropic.types.ToolParam(
|
136
|
+
name=fnc["name"],
|
137
|
+
description=fnc["description"] or "",
|
138
|
+
input_schema=fnc["parameters"],
|
139
|
+
cache_control=cache_ctrl,
|
140
|
+
)
|
@@ -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.dev5
|
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 <support@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<1.0.0,>=0.12.16
|
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.dev5
|
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=T22doOXWfmw-xSbY2Ts6jCQxENOA9pQTcbPQn-_3Eao,12969
|
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=tr7lQwIWP5hzNQl1XE6L91htsd7x8fPzfXfh9eVoWH0,4939
|
7
|
+
livekit/plugins/anthropic/version.py,sha256=pXgCpV03nQI-5Kk-74NFyAdw1htj2cx6unwQHipEcfE,605
|
8
|
+
livekit_plugins_anthropic-1.0.0.dev5.dist-info/METADATA,sha256=N8A6WjOyZxgFXr3LQeBhfA6LEkZEtwOIWKeqV0XoA6s,1283
|
9
|
+
livekit_plugins_anthropic-1.0.0.dev5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
livekit_plugins_anthropic-1.0.0.dev5.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=DC7zV-PQt-EaJK0DNm_kMuF_1mO7oPlaMVvGD0eDMwA,601
|
7
|
-
livekit_plugins_anthropic-0.2.13.dist-info/METADATA,sha256=M4I1liRz8JcHrEGBAIKqKGzm49s9_6BNLHCzVTFxzsk,1489
|
8
|
-
livekit_plugins_anthropic-0.2.13.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
9
|
-
livekit_plugins_anthropic-0.2.13.dist-info/top_level.txt,sha256=OoDok3xUmXbZRvOrfvvXB-Juu4DX79dlq188E19YHoo,8
|
10
|
-
livekit_plugins_anthropic-0.2.13.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
livekit
|