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.
@@ -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 .log import logger
52
- from .models import (
53
- ChatModels,
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
- CACHE_CONTROL_EPHEMERAL = anthropic.types.CacheControlEphemeralParam(type="ephemeral")
37
+ from .models import ChatModels
38
+ from .utils import to_chat_ctx, to_fnc_ctx
57
39
 
58
40
 
59
41
  @dataclass
60
- class LLMOptions:
42
+ class _LLMOptions:
61
43
  model: str | ChatModels
62
- user: str | None
63
- temperature: float | None
64
- parallel_tool_calls: bool | None
65
- tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]] | None
66
- caching: Literal["ephemeral"] | None = None
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 | None = None,
76
- base_url: str | None = None,
77
- user: str | None = None,
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
- temperature: float | None = None,
80
- parallel_tool_calls: bool | None = None,
81
- tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]] = "auto",
82
- caching: Literal["ephemeral"] | None = None,
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 | None): The Anthropic API key. Defaults to the ANTHROPIC_API_KEY environment variable.
92
- base_url (str | None): The base URL for the Anthropic API. Defaults to None.
93
- user (str | None): The user for the Anthropic API. Defaults to None.
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 | None): The temperature for the Anthropic API. Defaults to None.
96
- parallel_tool_calls (bool | None): Whether to parallelize tool calls. Defaults to None.
97
- tool_choice (Union[ToolChoice, Literal["auto", "required", "none"]] | None): The tool choice for the Anthropic API. Defaults to "auto".
98
- caching (Literal["ephemeral"] | None): If set to "ephemeral", caching will be enabled for the system prompt, tools, and chat history.
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
- # throw an error on our end
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
- self._client = client or anthropic.AsyncClient(
122
- api_key=api_key,
123
- base_url=base_url,
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: llm.ChatContext,
120
+ chat_ctx: ChatContext,
121
+ tools: list[FunctionTool] | None = None,
139
122
  conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
140
- fnc_ctx: llm.FunctionContext | None = None,
141
- temperature: float | None = None,
142
- n: int | None = 1,
143
- parallel_tool_calls: bool | None = None,
144
- tool_choice: Union[ToolChoice, Literal["auto", "required", "none"]]
145
- | None = None,
146
- ) -> "LLMStream":
147
- if temperature is None:
148
- temperature = self._opts.temperature
149
- if parallel_tool_calls is None:
150
- parallel_tool_calls = self._opts.parallel_tool_calls
151
- if tool_choice is None:
152
- tool_choice = self._opts.tool_choice
153
-
154
- opts: dict[str, Any] = dict()
155
- if fnc_ctx and len(fnc_ctx.ai_functions) > 0:
156
- fncs_desc: list[anthropic.types.ToolParam] = []
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] = 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
- opts["tools"] = fncs_desc
173
- if tool_choice is not None:
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, ToolChoice):
176
- if tool_choice.type == "function":
177
- anthropic_tool_choice = {
178
- "type": "tool",
179
- "name": tool_choice.name,
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
- opts["tools"] = []
157
+ extra["tools"] = []
186
158
  anthropic_tool_choice = None
187
- if anthropic_tool_choice is not None:
188
- if parallel_tool_calls is False:
189
- anthropic_tool_choice["disable_parallel_tool_use"] = True
190
- opts["tool_choice"] = anthropic_tool_choice
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
- latest_system_message: anthropic.types.TextBlockParam | None = (
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
- anthropic_ctx = _build_anthropic_context(
199
- chat_ctx.messages,
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
- max_tokens=opts.get("max_tokens", 1024),
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
- **opts,
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
- fnc_ctx=fnc_ctx,
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
- fnc_ctx: llm.FunctionContext | None,
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
- request_id=self._request_id,
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._fnc_ctx is not None:
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
- request_id=self._request_id,
334
- choices=[
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 and self._fnc_ctx:
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
- request_id=self._request_id,
359
- choices=[
360
- llm.Choice(
361
- delta=llm.ChoiceDelta(
362
- role="assistant", tool_calls=[fnc_info]
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
+ )
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "0.2.12"
15
+ __version__ = "1.0.0"
@@ -1,36 +1,26 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: livekit-plugins-anthropic
3
- Version: 0.2.12
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
- Keywords: webrtc,realtime,audio,video,livekit
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: Programming Language :: Python :: 3 :: Only
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
- Dynamic: classifier
25
- Dynamic: description
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,5 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
-
@@ -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